@senso-ai/cli 0.8.1 → 0.9.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.
- package/README.md +1 -1
- package/dist/cli.js +508 -75
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -141,7 +141,7 @@ Options: `content list` supports `--limit`, `--offset`, `--search`, `--sort`. `c
|
|
|
141
141
|
```
|
|
142
142
|
senso generate settings Get generation settings
|
|
143
143
|
senso generate update-settings Update generation settings (--data)
|
|
144
|
-
senso generate sample Generate sample for a prompt (--prompt-id, --content-type-id)
|
|
144
|
+
senso generate sample Generate sample for a prompt; waits for async job by default (--prompt-id, --content-type-id, --no-wait)
|
|
145
145
|
senso generate run Trigger a content engine run (--prompt-ids)
|
|
146
146
|
```
|
|
147
147
|
|
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 =
|
|
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
|
|
238
|
+
const text5 = await res.text();
|
|
208
239
|
try {
|
|
209
|
-
body = JSON.parse(
|
|
240
|
+
body = JSON.parse(text5);
|
|
210
241
|
} catch {
|
|
211
|
-
body =
|
|
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
|
|
249
|
+
const text4 = await res.text();
|
|
219
250
|
try {
|
|
220
|
-
return JSON.parse(
|
|
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(` ${
|
|
384
|
+
console.log(` ${pc4.bold("Welcome to Senso CLI!")}
|
|
288
385
|
`);
|
|
289
|
-
console.log(` ${
|
|
290
|
-
console.log(` ${
|
|
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 ${
|
|
318
|
-
success(`Config saved to ${
|
|
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(` ${
|
|
354
|
-
console.log(` ${
|
|
355
|
-
console.log(` ${
|
|
356
|
-
console.log(` ${
|
|
357
|
-
console.log(` ${
|
|
358
|
-
console.log(` ${
|
|
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(` ${
|
|
365
|
-
console.log(` ${
|
|
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
|
|
715
|
+
import pc6 from "picocolors";
|
|
619
716
|
|
|
620
717
|
// src/lib/output.ts
|
|
621
|
-
import
|
|
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(
|
|
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) =>
|
|
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 ? ` ${
|
|
802
|
+
res.answer ? ` ${pc6.bold("Answer:")} ${res.answer}` : "",
|
|
706
803
|
"",
|
|
707
804
|
...(res.results || []).map(
|
|
708
|
-
(r, i) => ` ${
|
|
805
|
+
(r, i) => ` ${pc6.dim(`${i + 1}.`)} ${pc6.bold(String(r.title || "Untitled"))}
|
|
709
806
|
${String(r.chunk_text || "").slice(0, 120)}
|
|
710
|
-
${
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1244
|
+
uploadSpin.stop(`Uploaded ${item.filename}`);
|
|
865
1245
|
} catch (uploadErr) {
|
|
866
|
-
|
|
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
|
-
|
|
1253
|
+
failed.push({
|
|
1254
|
+
filename: item.filename,
|
|
1255
|
+
reason: uploadStatusToReason(item.status, item.error)
|
|
1256
|
+
});
|
|
870
1257
|
}
|
|
871
1258
|
}
|
|
872
|
-
|
|
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
|
-
|
|
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
|
|
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) => ` ${
|
|
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) {
|
|
@@ -1047,6 +1433,8 @@ function registerContentCommands(program2) {
|
|
|
1047
1433
|
}
|
|
1048
1434
|
|
|
1049
1435
|
// src/commands/generate.ts
|
|
1436
|
+
var SAMPLE_JOB_POLL_INTERVAL_MS = 2e3;
|
|
1437
|
+
var SAMPLE_JOB_TIMEOUT_MS = 18e4;
|
|
1050
1438
|
function registerGenerateCommands(program2) {
|
|
1051
1439
|
const gen = program2.command("generate").description("AI content generation. Configure settings, generate content samples from prompts, or trigger full content engine runs.");
|
|
1052
1440
|
gen.command("settings").description("Get content generation settings. Shows whether generation and auto-publish are enabled, the content schedule, and configured publishers.").action(async () => {
|
|
@@ -1071,7 +1459,7 @@ function registerGenerateCommands(program2) {
|
|
|
1071
1459
|
process.exit(1);
|
|
1072
1460
|
}
|
|
1073
1461
|
});
|
|
1074
|
-
gen.command("sample").description("Generate an ad hoc content sample for a specific prompt and content type.
|
|
1462
|
+
gen.command("sample").description("Generate an ad hoc content sample for a specific prompt and content type. Submits an async job, waits for completion by default, then 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.").option("--no-wait", "Return the accepted sample job immediately instead of polling for the generated content.").action(async (cmdOpts) => {
|
|
1075
1463
|
const opts = program2.opts();
|
|
1076
1464
|
try {
|
|
1077
1465
|
const body = {
|
|
@@ -1088,7 +1476,25 @@ function registerGenerateCommands(program2) {
|
|
|
1088
1476
|
apiKey: opts.apiKey,
|
|
1089
1477
|
baseUrl: opts.baseUrl
|
|
1090
1478
|
});
|
|
1091
|
-
|
|
1479
|
+
if (cmdOpts.wait === false) {
|
|
1480
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
const quiet = opts.quiet || opts.output === "json";
|
|
1484
|
+
if (!quiet) {
|
|
1485
|
+
info(`Sample job accepted: ${data.sample_job_id}`);
|
|
1486
|
+
}
|
|
1487
|
+
const job = await waitForSampleJob(data.sample_job_id, {
|
|
1488
|
+
apiKey: opts.apiKey,
|
|
1489
|
+
baseUrl: opts.baseUrl,
|
|
1490
|
+
quiet
|
|
1491
|
+
});
|
|
1492
|
+
if (job.status === "completed") {
|
|
1493
|
+
console.log(JSON.stringify(job.result ?? job, null, 2));
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
const message = job.error?.message || `Sample job ended with status: ${job.status}`;
|
|
1497
|
+
throw new Error(job.error?.code ? `${message} (${job.error.code})` : message);
|
|
1092
1498
|
} catch (err) {
|
|
1093
1499
|
error(formatApiError(err));
|
|
1094
1500
|
process.exit(1);
|
|
@@ -1182,6 +1588,29 @@ function registerGenerateCommands(program2) {
|
|
|
1182
1588
|
}
|
|
1183
1589
|
});
|
|
1184
1590
|
}
|
|
1591
|
+
async function waitForSampleJob(sampleJobId, opts) {
|
|
1592
|
+
const deadline = Date.now() + SAMPLE_JOB_TIMEOUT_MS;
|
|
1593
|
+
let lastStatus = "";
|
|
1594
|
+
while (Date.now() < deadline) {
|
|
1595
|
+
const job = await apiRequest({
|
|
1596
|
+
path: `/org/content-generation/sample-jobs/${sampleJobId}`,
|
|
1597
|
+
apiKey: opts.apiKey,
|
|
1598
|
+
baseUrl: opts.baseUrl
|
|
1599
|
+
});
|
|
1600
|
+
if (!opts.quiet && job.status !== lastStatus) {
|
|
1601
|
+
info(`Sample job status: ${job.status}`);
|
|
1602
|
+
lastStatus = job.status;
|
|
1603
|
+
}
|
|
1604
|
+
if (job.status === "completed" || job.status === "failed" || job.status === "expired") {
|
|
1605
|
+
return job;
|
|
1606
|
+
}
|
|
1607
|
+
await sleep(SAMPLE_JOB_POLL_INTERVAL_MS);
|
|
1608
|
+
}
|
|
1609
|
+
throw new Error(`Timed out waiting for sample job ${sampleJobId}. Poll /org/content-generation/sample-jobs/${sampleJobId} for status.`);
|
|
1610
|
+
}
|
|
1611
|
+
function sleep(ms) {
|
|
1612
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
1613
|
+
}
|
|
1185
1614
|
|
|
1186
1615
|
// src/commands/engine.ts
|
|
1187
1616
|
function registerEngineCommands(program2) {
|
|
@@ -1781,12 +2210,12 @@ function registerKBCommands(program2) {
|
|
|
1781
2210
|
process.exit(1);
|
|
1782
2211
|
}
|
|
1783
2212
|
});
|
|
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) => {
|
|
2213
|
+
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
2214
|
const opts = program2.opts();
|
|
1786
2215
|
try {
|
|
1787
2216
|
const data = await apiRequest({
|
|
1788
2217
|
path: "/org/kb/my-files",
|
|
1789
|
-
params: { limit: cmdOpts.limit, offset: cmdOpts.offset },
|
|
2218
|
+
params: { limit: cmdOpts.limit, offset: cmdOpts.offset, type: cmdOpts.type },
|
|
1790
2219
|
apiKey: opts.apiKey,
|
|
1791
2220
|
baseUrl: opts.baseUrl
|
|
1792
2221
|
});
|
|
@@ -1796,12 +2225,12 @@ function registerKBCommands(program2) {
|
|
|
1796
2225
|
process.exit(1);
|
|
1797
2226
|
}
|
|
1798
2227
|
});
|
|
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) => {
|
|
2228
|
+
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
2229
|
const opts = program2.opts();
|
|
1801
2230
|
try {
|
|
1802
2231
|
const data = await apiRequest({
|
|
1803
2232
|
path: "/org/kb/find",
|
|
1804
|
-
params: { q: cmdOpts.query, limit: cmdOpts.limit, offset: cmdOpts.offset },
|
|
2233
|
+
params: { q: cmdOpts.query, limit: cmdOpts.limit, offset: cmdOpts.offset, type: cmdOpts.type },
|
|
1805
2234
|
apiKey: opts.apiKey,
|
|
1806
2235
|
baseUrl: opts.baseUrl
|
|
1807
2236
|
});
|
|
@@ -1831,12 +2260,12 @@ function registerKBCommands(program2) {
|
|
|
1831
2260
|
process.exit(1);
|
|
1832
2261
|
}
|
|
1833
2262
|
});
|
|
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) => {
|
|
2263
|
+
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
2264
|
const opts = program2.opts();
|
|
1836
2265
|
try {
|
|
1837
2266
|
const data = await apiRequest({
|
|
1838
2267
|
path: `/org/kb/nodes/${id}/children`,
|
|
1839
|
-
params: { limit: cmdOpts.limit, offset: cmdOpts.offset },
|
|
2268
|
+
params: { limit: cmdOpts.limit, offset: cmdOpts.offset, type: cmdOpts.type },
|
|
1840
2269
|
apiKey: opts.apiKey,
|
|
1841
2270
|
baseUrl: opts.baseUrl
|
|
1842
2271
|
});
|
|
@@ -1986,30 +2415,34 @@ function registerKBCommands(program2) {
|
|
|
1986
2415
|
});
|
|
1987
2416
|
const items = response.results ?? [];
|
|
1988
2417
|
let uploaded = 0;
|
|
2418
|
+
const failed = [];
|
|
1989
2419
|
for (const item of items) {
|
|
1990
2420
|
if (item.status === "upload_pending" && item.upload_url) {
|
|
1991
2421
|
const match = fileData.find((f) => f.meta.filename === item.filename);
|
|
1992
2422
|
if (!match) {
|
|
1993
|
-
|
|
2423
|
+
failed.push({ filename: item.filename, reason: "Could not match to a local file." });
|
|
1994
2424
|
continue;
|
|
1995
2425
|
}
|
|
1996
2426
|
try {
|
|
1997
2427
|
await uploadToS32(item.upload_url, match.buffer, match.meta.content_type);
|
|
1998
2428
|
uploaded++;
|
|
1999
|
-
success(`Uploaded ${item.filename} (content_id: ${item.content_id})`);
|
|
2000
2429
|
} catch (uploadErr) {
|
|
2001
|
-
|
|
2430
|
+
failed.push({
|
|
2431
|
+
filename: item.filename,
|
|
2432
|
+
reason: `Upload failed: ${uploadErr instanceof Error ? uploadErr.message : String(uploadErr)}`
|
|
2433
|
+
});
|
|
2002
2434
|
}
|
|
2003
2435
|
} else {
|
|
2004
|
-
|
|
2436
|
+
failed.push({
|
|
2437
|
+
filename: item.filename,
|
|
2438
|
+
reason: uploadStatusToReason(item.status, item.error)
|
|
2439
|
+
});
|
|
2005
2440
|
}
|
|
2006
2441
|
}
|
|
2007
|
-
|
|
2008
|
-
success(`${uploaded} file(s) uploaded. Background processing will parse, chunk, and embed them.`);
|
|
2009
|
-
}
|
|
2442
|
+
printUploadSummary(uploaded, failed, items);
|
|
2010
2443
|
if (opts.output === "json") console.log(JSON.stringify(response, null, 2));
|
|
2011
2444
|
} catch (err) {
|
|
2012
|
-
|
|
2445
|
+
handleUploadError(err);
|
|
2013
2446
|
process.exit(1);
|
|
2014
2447
|
}
|
|
2015
2448
|
});
|
|
@@ -2055,12 +2488,12 @@ function registerPermissionsCommands(program2) {
|
|
|
2055
2488
|
|
|
2056
2489
|
// src/commands/update.ts
|
|
2057
2490
|
import semver2 from "semver";
|
|
2058
|
-
import
|
|
2491
|
+
import pc10 from "picocolors";
|
|
2059
2492
|
import { execSync } from "child_process";
|
|
2060
2493
|
var NPM_PACKAGE2 = "@senso-ai/cli";
|
|
2061
2494
|
function registerUpdateCommand(program2) {
|
|
2062
2495
|
program2.command("update").description("Update CLI to the latest version").action(async () => {
|
|
2063
|
-
info(`Current version: ${
|
|
2496
|
+
info(`Current version: ${pc10.bold(version)}`);
|
|
2064
2497
|
info("Checking npm for updates...");
|
|
2065
2498
|
const latest = await getLatestVersion();
|
|
2066
2499
|
if (!latest) {
|
|
@@ -2071,7 +2504,7 @@ function registerUpdateCommand(program2) {
|
|
|
2071
2504
|
success(`Already on the latest version (${version}).`);
|
|
2072
2505
|
return;
|
|
2073
2506
|
}
|
|
2074
|
-
info(`New version available: ${
|
|
2507
|
+
info(`New version available: ${pc10.bold(latest)}`);
|
|
2075
2508
|
info("Updating...");
|
|
2076
2509
|
try {
|
|
2077
2510
|
execSync(`npm install -g ${NPM_PACKAGE2}@latest`, {
|
|
@@ -2081,7 +2514,7 @@ function registerUpdateCommand(program2) {
|
|
|
2081
2514
|
} catch {
|
|
2082
2515
|
error("Update failed. Please reinstall manually:");
|
|
2083
2516
|
console.log(
|
|
2084
|
-
` ${
|
|
2517
|
+
` ${pc10.cyan(`npm install -g ${NPM_PACKAGE2}`)}`
|
|
2085
2518
|
);
|
|
2086
2519
|
process.exit(1);
|
|
2087
2520
|
}
|