@senso-ai/cli 0.14.0 → 0.15.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 +290 -16
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ argument and flag, generated from the CLI itself.
|
|
|
118
118
|
| ------------------------------------------------------------------------ | ------------------------------------------------------------------- |
|
|
119
119
|
| `login` `logout` `whoami` | Authentication and the current organization |
|
|
120
120
|
| `search` | Ask the knowledge base, with an AI answer, raw chunks, or streaming |
|
|
121
|
-
| `kb` `ingest` `content`
|
|
121
|
+
| `kb` `ingest` `content` `website-import` | The knowledge base: upload, browse, organize, verify |
|
|
122
122
|
| `ctas` | Call-to-action cards on published pages |
|
|
123
123
|
| `generate` `engine` `generated-content` `destinations` `publish-records` | Generate content and publish it |
|
|
124
124
|
| `analytics` | GEO metrics for your own organization |
|
package/dist/cli.js
CHANGED
|
@@ -201,6 +201,9 @@ var ApiError = class extends Error {
|
|
|
201
201
|
this.body = body;
|
|
202
202
|
this.name = "ApiError";
|
|
203
203
|
}
|
|
204
|
+
status;
|
|
205
|
+
statusText;
|
|
206
|
+
body;
|
|
204
207
|
};
|
|
205
208
|
function extractErrorMessage(body, fallback) {
|
|
206
209
|
if (typeof body !== "object" || !body) return fallback;
|
|
@@ -934,10 +937,10 @@ function registerOrgCommands(program) {
|
|
|
934
937
|
})
|
|
935
938
|
);
|
|
936
939
|
org.command("update").description(
|
|
937
|
-
"Update organization details.
|
|
940
|
+
"Update organization details. Only the fields you pass are changed; omitting a field leaves it alone. But 'websites' and 'locations' REPLACE their whole list when passed \u2014 sending one website deletes the rest. To add to either list, run 'org get' first and send back every entry you want to keep."
|
|
938
941
|
).requiredOption(
|
|
939
942
|
"--data <json>",
|
|
940
|
-
|
|
943
|
+
`JSON: { "name": "Acme", "slug": "acme", "logo_url": "https://acme.com/logo.png", "websites": [{"url": "https://acme.com"}], "locations": [{"country_code": "US", "region_name": "California"}] }. Every field is optional. "websites" and "locations" REPLACE the existing list rather than adding to it \u2014 include every entry you want to keep, or pass [] to clear the list. A website entry takes only "url"; sending the "org_website_id" from 'org get' is rejected. Send "logo_url": "" to clear the logo.`
|
|
941
944
|
).action(
|
|
942
945
|
runAction(program, async (ctx, cmdOpts) => {
|
|
943
946
|
const body = parseJsonFlag(cmdOpts.data);
|
|
@@ -1769,7 +1772,7 @@ function registerIngestCommands(program) {
|
|
|
1769
1772
|
"Ingest files into the knowledge base. Upload documents (PDF, TXT, DOCX, etc.) to be parsed, chunked, and embedded for semantic search."
|
|
1770
1773
|
);
|
|
1771
1774
|
ingest.command("upload <files...>").description(
|
|
1772
|
-
"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
|
|
1775
|
+
"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 kb get <kb-node-id>' until content.processing_status is 'complete' before searching the uploaded content."
|
|
1773
1776
|
).option("--folder-id <id>", "Destination folder ID (skip interactive prompt)").action(
|
|
1774
1777
|
runAction(program, async (ctx, files, cmdOpts) => {
|
|
1775
1778
|
if (files.length > 10) {
|
|
@@ -1868,9 +1871,13 @@ function registerIngestCommands(program) {
|
|
|
1868
1871
|
rows: items.map((i) => ({
|
|
1869
1872
|
filename: i.filename,
|
|
1870
1873
|
status: i.status,
|
|
1874
|
+
// The id `senso kb get` takes. `content_id` is not interchangeable
|
|
1875
|
+
// with it: polling a KB upload through `senso content get` hits an
|
|
1876
|
+
// endpoint that serves non-KB content only and answers 400.
|
|
1877
|
+
kb_node_id: i.kb_node_id,
|
|
1871
1878
|
content_id: i.content_id
|
|
1872
1879
|
})),
|
|
1873
|
-
columns: ["filename", "status", "content_id"]
|
|
1880
|
+
columns: ["filename", "status", "kb_node_id", "content_id"]
|
|
1874
1881
|
},
|
|
1875
1882
|
plain: []
|
|
1876
1883
|
});
|
|
@@ -1904,6 +1911,159 @@ function registerIngestCommands(program) {
|
|
|
1904
1911
|
);
|
|
1905
1912
|
}
|
|
1906
1913
|
|
|
1914
|
+
// src/commands/website-import.ts
|
|
1915
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
1916
|
+
var IMPORT_TIMEOUT_MS = 18e4;
|
|
1917
|
+
var RUN_COLUMNS = [
|
|
1918
|
+
"run_id",
|
|
1919
|
+
"status",
|
|
1920
|
+
"source_url",
|
|
1921
|
+
"pages_fetched",
|
|
1922
|
+
"pages_ingested",
|
|
1923
|
+
"brand_kit_generated"
|
|
1924
|
+
];
|
|
1925
|
+
function getStatus(ctx) {
|
|
1926
|
+
return apiRequest({
|
|
1927
|
+
path: "/org/website-import/status",
|
|
1928
|
+
apiKey: ctx.apiKey,
|
|
1929
|
+
baseUrl: ctx.baseUrl
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
function sleep(ms) {
|
|
1933
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
1934
|
+
}
|
|
1935
|
+
async function waitForImport(runId, ctx) {
|
|
1936
|
+
const deadline = Date.now() + IMPORT_TIMEOUT_MS;
|
|
1937
|
+
let lastStatus = "";
|
|
1938
|
+
while (Date.now() < deadline) {
|
|
1939
|
+
const { current, latest_completed: latest } = await getStatus(ctx);
|
|
1940
|
+
if (current && !ctx.quiet && current.status !== lastStatus) {
|
|
1941
|
+
info(`Website import status: ${current.status}`);
|
|
1942
|
+
lastStatus = current.status;
|
|
1943
|
+
}
|
|
1944
|
+
if (!current) {
|
|
1945
|
+
if (latest?.run_id === runId) return latest;
|
|
1946
|
+
throw new CliError(
|
|
1947
|
+
`Website import ${runId} finished but its result could not be read.`,
|
|
1948
|
+
EXIT.ERROR,
|
|
1949
|
+
{
|
|
1950
|
+
hint: "Run `senso website-import status` to see the most recent import."
|
|
1951
|
+
}
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
await sleep(POLL_INTERVAL_MS);
|
|
1955
|
+
}
|
|
1956
|
+
throw new CliError(`Timed out waiting for website import ${runId}.`, EXIT.NETWORK, {
|
|
1957
|
+
code: "timeout",
|
|
1958
|
+
hint: "Run `senso website-import status` to check on it."
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
function describeOutcome(run, quiet) {
|
|
1962
|
+
if (quiet) return;
|
|
1963
|
+
if (run.status === "failed") {
|
|
1964
|
+
error(`Website import failed: ${run.error_message ?? "no reason given"}`);
|
|
1965
|
+
if (run.error_code) error(`Reason code: ${run.error_code}`);
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
success(
|
|
1969
|
+
`Imported ${String(run.pages_ingested)} of ${String(run.pages_fetched)} page(s) from ${run.source_url}.`
|
|
1970
|
+
);
|
|
1971
|
+
if (run.brand_kit_generated) {
|
|
1972
|
+
success("A brand kit was generated from the site.");
|
|
1973
|
+
} else if (run.brand_kit_skip_reason === "already_populated") {
|
|
1974
|
+
info("Brand kit left alone \u2014 this organization already has one.");
|
|
1975
|
+
}
|
|
1976
|
+
if (run.pages_ingested < run.pages_fetched) {
|
|
1977
|
+
info("Pages already in the knowledge base unchanged are fetched but not re-ingested.");
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
function buildStatusRows(data) {
|
|
1981
|
+
const rows = [];
|
|
1982
|
+
if (data.current) rows.push({ slot: "current", ...data.current });
|
|
1983
|
+
if (data.latest_completed) rows.push({ slot: "latest_completed", ...data.latest_completed });
|
|
1984
|
+
return rows;
|
|
1985
|
+
}
|
|
1986
|
+
function registerWebsiteImportCommands(program) {
|
|
1987
|
+
const websiteImport = program.command("website-import").description(
|
|
1988
|
+
"Import your organization's website into the knowledge base. Fetches the home page plus up to 10 linked pages, ingests each as a document under a folder named 'Website', and drafts a brand kit if the organization does not have one yet."
|
|
1989
|
+
);
|
|
1990
|
+
websiteImport.command("start").description(
|
|
1991
|
+
"Start a website import and wait for it to finish. The site imported is the one on file for your organization \u2014 see 'senso org get' \u2014 not a value you pass, so this takes no arguments. Exits 1 if the import finishes in a failed state."
|
|
1992
|
+
).option(
|
|
1993
|
+
"--no-wait",
|
|
1994
|
+
"Return the accepted run immediately instead of polling until the import finishes."
|
|
1995
|
+
).action(
|
|
1996
|
+
runAction(program, async (ctx, cmdOpts) => {
|
|
1997
|
+
let accepted;
|
|
1998
|
+
try {
|
|
1999
|
+
accepted = await apiRequest({
|
|
2000
|
+
method: "POST",
|
|
2001
|
+
path: "/org/website-import",
|
|
2002
|
+
apiKey: ctx.apiKey,
|
|
2003
|
+
baseUrl: ctx.baseUrl
|
|
2004
|
+
});
|
|
2005
|
+
} catch (err) {
|
|
2006
|
+
const status = err.status;
|
|
2007
|
+
if (status === 409) {
|
|
2008
|
+
throw new CliError(
|
|
2009
|
+
"A website import is already running for this organization.",
|
|
2010
|
+
EXIT.ERROR,
|
|
2011
|
+
{
|
|
2012
|
+
code: "conflict",
|
|
2013
|
+
status: 409,
|
|
2014
|
+
hint: "Run `senso website-import status` to follow the one in flight.",
|
|
2015
|
+
cause: err
|
|
2016
|
+
}
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
throw err;
|
|
2020
|
+
}
|
|
2021
|
+
if (!cmdOpts.wait) {
|
|
2022
|
+
if (!ctx.quiet) success(`Website import ${accepted.run_id} started.`);
|
|
2023
|
+
emit(ctx, accepted, { columns: RUN_COLUMNS });
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
const spin = spinner4(ctx.quiet);
|
|
2027
|
+
spin.start("Importing website...");
|
|
2028
|
+
let finished;
|
|
2029
|
+
try {
|
|
2030
|
+
finished = await waitForImport(accepted.run_id, ctx);
|
|
2031
|
+
} catch (err) {
|
|
2032
|
+
spin.stop("Website import did not finish");
|
|
2033
|
+
throw err;
|
|
2034
|
+
}
|
|
2035
|
+
spin.stop("Website import finished");
|
|
2036
|
+
describeOutcome(finished, ctx.quiet);
|
|
2037
|
+
if (finished.status === "failed") {
|
|
2038
|
+
throw new CliError(
|
|
2039
|
+
`Website import ${finished.run_id} failed${finished.error_code ? ` (${finished.error_code})` : ""}.`,
|
|
2040
|
+
EXIT.ERROR,
|
|
2041
|
+
{ hint: "Run `senso website-import status` for the full record." }
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
2044
|
+
emit(ctx, finished, { columns: RUN_COLUMNS });
|
|
2045
|
+
})
|
|
2046
|
+
);
|
|
2047
|
+
websiteImport.command("status").description(
|
|
2048
|
+
"Show the website import in flight and the most recently finished one. Either may be absent. This is a read: it exits 0 even when the last import failed."
|
|
2049
|
+
).action(
|
|
2050
|
+
runAction(program, async (ctx) => {
|
|
2051
|
+
const data = await getStatus(ctx);
|
|
2052
|
+
if (!ctx.quiet && !data.current && !data.latest_completed) {
|
|
2053
|
+
info("This organization has never imported its website.");
|
|
2054
|
+
}
|
|
2055
|
+
emit(ctx, data, {
|
|
2056
|
+
// A status envelope is two nullable runs, not a list, so the table
|
|
2057
|
+
// rendering is built here rather than left to findRows.
|
|
2058
|
+
table: {
|
|
2059
|
+
rows: buildStatusRows(data),
|
|
2060
|
+
columns: ["slot", ...RUN_COLUMNS]
|
|
2061
|
+
}
|
|
2062
|
+
});
|
|
2063
|
+
})
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
|
|
1907
2067
|
// src/commands/content.ts
|
|
1908
2068
|
import pc9 from "picocolors";
|
|
1909
2069
|
|
|
@@ -2716,14 +2876,14 @@ async function waitForSampleJob(sampleJobId, opts) {
|
|
|
2716
2876
|
if (job.status === "completed" || job.status === "failed" || job.status === "expired") {
|
|
2717
2877
|
return job;
|
|
2718
2878
|
}
|
|
2719
|
-
await
|
|
2879
|
+
await sleep2(SAMPLE_JOB_POLL_INTERVAL_MS);
|
|
2720
2880
|
}
|
|
2721
2881
|
throw new CliError(`Timed out waiting for sample job ${sampleJobId}.`, EXIT.NETWORK, {
|
|
2722
2882
|
code: "timeout",
|
|
2723
2883
|
hint: `Poll /org/content-generation/sample-jobs/${sampleJobId} for status.`
|
|
2724
2884
|
});
|
|
2725
2885
|
}
|
|
2726
|
-
function
|
|
2886
|
+
function sleep2(ms) {
|
|
2727
2887
|
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
2728
2888
|
}
|
|
2729
2889
|
|
|
@@ -2893,11 +3053,122 @@ function registerPublishRecordsCommands(program) {
|
|
|
2893
3053
|
}
|
|
2894
3054
|
|
|
2895
3055
|
// src/commands/brand-kit.ts
|
|
3056
|
+
var GUIDELINE_STRING_FIELDS = [
|
|
3057
|
+
"brand_name",
|
|
3058
|
+
"brand_domain",
|
|
3059
|
+
"brand_description",
|
|
3060
|
+
"voice_and_tone",
|
|
3061
|
+
"author_persona"
|
|
3062
|
+
];
|
|
3063
|
+
var GUIDELINE_FIELDS = [...GUIDELINE_STRING_FIELDS, "global_writing_rules"];
|
|
3064
|
+
var FIELD_LIST = GUIDELINE_FIELDS.join(", ");
|
|
3065
|
+
var SET_EXAMPLE = `--data '{"guidelines":{"brand_name":"Acme","voice_and_tone":"Warm and direct"}}'`;
|
|
3066
|
+
function describe(value) {
|
|
3067
|
+
if (value === null) return "null";
|
|
3068
|
+
if (Array.isArray(value)) return "an array";
|
|
3069
|
+
return `a ${typeof value}`;
|
|
3070
|
+
}
|
|
3071
|
+
function nearest(key, allowed) {
|
|
3072
|
+
let best;
|
|
3073
|
+
let bestDistance = 3;
|
|
3074
|
+
for (const candidate of allowed) {
|
|
3075
|
+
const distance = editDistance(key, candidate);
|
|
3076
|
+
if (distance < bestDistance) {
|
|
3077
|
+
bestDistance = distance;
|
|
3078
|
+
best = candidate;
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
return best;
|
|
3082
|
+
}
|
|
3083
|
+
function editDistance(a, b) {
|
|
3084
|
+
let row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
3085
|
+
for (let i = 1; i <= a.length; i++) {
|
|
3086
|
+
const next = [i];
|
|
3087
|
+
for (let j = 1; j <= b.length; j++) {
|
|
3088
|
+
next.push(
|
|
3089
|
+
Math.min(
|
|
3090
|
+
(row[j] ?? 0) + 1,
|
|
3091
|
+
(next[j - 1] ?? 0) + 1,
|
|
3092
|
+
(row[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
3093
|
+
)
|
|
3094
|
+
);
|
|
3095
|
+
}
|
|
3096
|
+
row = next;
|
|
3097
|
+
}
|
|
3098
|
+
return row[b.length] ?? 0;
|
|
3099
|
+
}
|
|
3100
|
+
function usageError(message, hint2) {
|
|
3101
|
+
return new CliError(message, EXIT.USAGE, { code: "usage", hint: hint2 });
|
|
3102
|
+
}
|
|
3103
|
+
function parseBrandKitData(data, mode) {
|
|
3104
|
+
const body = parseJsonFlag(data);
|
|
3105
|
+
if (!("guidelines" in body)) {
|
|
3106
|
+
throw usageError(
|
|
3107
|
+
`--data must have a "guidelines" object at the top level.`,
|
|
3108
|
+
`The fields go inside it: ${SET_EXAMPLE}. Accepted fields: ${FIELD_LIST}.`
|
|
3109
|
+
);
|
|
3110
|
+
}
|
|
3111
|
+
const stray = Object.keys(body).filter((key) => key !== "guidelines");
|
|
3112
|
+
if (stray.length > 0) {
|
|
3113
|
+
throw usageError(
|
|
3114
|
+
`--data has ${stray.length === 1 ? "a key" : "keys"} outside "guidelines": ${stray.join(", ")}.`,
|
|
3115
|
+
`Move ${stray.length === 1 ? "it" : "them"} inside "guidelines" \u2014 the API ignores anything beside it without reporting that it did. Accepted fields: ${FIELD_LIST}.`
|
|
3116
|
+
);
|
|
3117
|
+
}
|
|
3118
|
+
const guidelines = body.guidelines;
|
|
3119
|
+
if (typeof guidelines !== "object" || guidelines === null || Array.isArray(guidelines)) {
|
|
3120
|
+
throw usageError(
|
|
3121
|
+
`"guidelines" must be a JSON object, not ${describe(guidelines)}.`,
|
|
3122
|
+
`Example: ${SET_EXAMPLE}`
|
|
3123
|
+
);
|
|
3124
|
+
}
|
|
3125
|
+
const entries = Object.entries(guidelines);
|
|
3126
|
+
if (mode === "merge" && entries.length === 0) {
|
|
3127
|
+
throw usageError(
|
|
3128
|
+
`"guidelines" must name at least one field to patch.`,
|
|
3129
|
+
`Accepted fields: ${FIELD_LIST}. To clear the whole brand kit instead, run: senso brand-kit set --data '{"guidelines":{}}'`
|
|
3130
|
+
);
|
|
3131
|
+
}
|
|
3132
|
+
for (const [key, value] of entries) {
|
|
3133
|
+
if (!GUIDELINE_FIELDS.includes(key)) {
|
|
3134
|
+
const suggestion = nearest(key, GUIDELINE_FIELDS);
|
|
3135
|
+
throw usageError(
|
|
3136
|
+
`"guidelines" does not accept the field "${key}".`,
|
|
3137
|
+
suggestion ? `Did you mean "${suggestion}"? Accepted fields: ${FIELD_LIST}.` : `Accepted fields: ${FIELD_LIST}.`
|
|
3138
|
+
);
|
|
3139
|
+
}
|
|
3140
|
+
if (key === "global_writing_rules") {
|
|
3141
|
+
if (!Array.isArray(value)) {
|
|
3142
|
+
throw usageError(
|
|
3143
|
+
`"global_writing_rules" must be an array of strings, not ${describe(value)}.`,
|
|
3144
|
+
`Example: {"global_writing_rules":["Avoid superlatives unless backed by a number"]}. Pass [] to clear the rules.`
|
|
3145
|
+
);
|
|
3146
|
+
}
|
|
3147
|
+
const badIndex = value.findIndex((rule) => typeof rule !== "string");
|
|
3148
|
+
if (badIndex !== -1) {
|
|
3149
|
+
throw usageError(
|
|
3150
|
+
`"global_writing_rules[${String(badIndex)}]" must be a string, not ${describe(value[badIndex])}.`,
|
|
3151
|
+
`Every rule is a line of guidance for the AI writer, e.g. "Prefer concrete examples over abstract claims".`
|
|
3152
|
+
);
|
|
3153
|
+
}
|
|
3154
|
+
continue;
|
|
3155
|
+
}
|
|
3156
|
+
if (typeof value !== "string") {
|
|
3157
|
+
throw usageError(
|
|
3158
|
+
`"${key}" must be a string, not ${describe(value)}.`,
|
|
3159
|
+
value === null ? `No field may be null. Omit it to leave it unchanged, or use 'brand-kit set' without it to remove it.` : `Example: {"${key}":"..."}`
|
|
3160
|
+
);
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
return body;
|
|
3164
|
+
}
|
|
2896
3165
|
function registerBrandKitCommands(program) {
|
|
2897
3166
|
const bk = program.command("brand-kit").description(
|
|
2898
|
-
|
|
3167
|
+
`Manage the organization's brand kit guidelines that inform AI content generation about your brand voice, tone, and style. The guidelines object accepts a defined set of keys: ${FIELD_LIST} (global_writing_rules is an array of strings, the rest are strings). Unknown keys, wrong types and nulls are rejected before the request is sent.`
|
|
2899
3168
|
);
|
|
2900
|
-
bk.command("get").description(
|
|
3169
|
+
bk.command("get").description(
|
|
3170
|
+
"Get the current brand kit guidelines. An organization that has never saved one gets an empty guidelines object rather than an error."
|
|
3171
|
+
).action(
|
|
2901
3172
|
runAction(program, async (ctx) => {
|
|
2902
3173
|
const data = await apiRequest({
|
|
2903
3174
|
path: "/org/brand-kit",
|
|
@@ -2908,13 +3179,13 @@ function registerBrandKitCommands(program) {
|
|
|
2908
3179
|
})
|
|
2909
3180
|
);
|
|
2910
3181
|
bk.command("set").description(
|
|
2911
|
-
"Replace the entire brand kit (PUT). All existing fields are overwritten \u2014 run 'brand-kit get' first to preserve fields you are not changing. For a safe partial update, use 'brand-kit patch'."
|
|
3182
|
+
"Replace the entire brand kit (PUT). All existing fields are overwritten \u2014 run 'brand-kit get' first to preserve fields you are not changing. For a safe partial update, use 'brand-kit patch'. This is also what creates the brand kit the first time."
|
|
2912
3183
|
).requiredOption(
|
|
2913
3184
|
"--data <json>",
|
|
2914
|
-
|
|
3185
|
+
`JSON: { "guidelines": { "brand_name": "Acme", "brand_domain": "https://acme.com", "brand_description": "...", "voice_and_tone": "...", "author_persona": "...", "global_writing_rules": ["..."] } }. Every field is optional, but anything you omit is REMOVED \u2014 pass '{"guidelines":{}}' to clear the brand kit entirely.`
|
|
2915
3186
|
).action(
|
|
2916
3187
|
runAction(program, async (ctx, cmdOpts) => {
|
|
2917
|
-
const body =
|
|
3188
|
+
const body = parseBrandKitData(cmdOpts.data, "replace");
|
|
2918
3189
|
const data = await apiRequest({
|
|
2919
3190
|
method: "PUT",
|
|
2920
3191
|
path: "/org/brand-kit",
|
|
@@ -2927,13 +3198,13 @@ function registerBrandKitCommands(program) {
|
|
|
2927
3198
|
})
|
|
2928
3199
|
);
|
|
2929
3200
|
bk.command("patch").description(
|
|
2930
|
-
"Partially update the brand kit (PATCH). Only the fields you provide are changed \u2014 existing fields are preserved. Preferred over 'set' for targeted updates."
|
|
3201
|
+
"Partially update the brand kit (PATCH). Only the fields you provide are changed \u2014 existing fields are preserved. Preferred over 'set' for targeted updates. Note that global_writing_rules is replaced wholesale, not appended to, and no field can be removed this way \u2014 use 'set' for that."
|
|
2931
3202
|
).requiredOption(
|
|
2932
3203
|
"--data <json>",
|
|
2933
|
-
|
|
3204
|
+
`JSON: { "guidelines": { "voice_and_tone": "Warm and approachable" } }. At least one field is required; accepted fields are ${FIELD_LIST}.`
|
|
2934
3205
|
).action(
|
|
2935
3206
|
runAction(program, async (ctx, cmdOpts) => {
|
|
2936
|
-
const body =
|
|
3207
|
+
const body = parseBrandKitData(cmdOpts.data, "merge");
|
|
2937
3208
|
const data = await apiRequest({
|
|
2938
3209
|
method: "PATCH",
|
|
2939
3210
|
path: "/org/brand-kit",
|
|
@@ -3947,7 +4218,7 @@ function registerKBCommands(program) {
|
|
|
3947
4218
|
})
|
|
3948
4219
|
);
|
|
3949
4220
|
kb.command("upload <files...>").description(
|
|
3950
|
-
"Upload files to the knowledge base (up to 10). Files are hashed, uploaded to S3, then parsed and embedded by a background worker."
|
|
4221
|
+
"Upload files to the knowledge base (up to 10). Files are hashed, uploaded to S3, then parsed and embedded by a background worker. Poll 'senso kb get <kb-node-id>' until content.processing_status is 'complete' before searching the uploaded content."
|
|
3951
4222
|
).option("--folder-id <id>", "Parent folder node ID to place files in (omit for root)").action(
|
|
3952
4223
|
runAction(program, async (ctx, files, cmdOpts) => {
|
|
3953
4224
|
if (files.length > 10) {
|
|
@@ -4010,7 +4281,9 @@ function registerKBCommands(program) {
|
|
|
4010
4281
|
});
|
|
4011
4282
|
}
|
|
4012
4283
|
if (ctx.format !== "plain") {
|
|
4013
|
-
emit(ctx, response, {
|
|
4284
|
+
emit(ctx, response, {
|
|
4285
|
+
columns: ["filename", "status", "kb_node_id", "content_id", "error"]
|
|
4286
|
+
});
|
|
4014
4287
|
}
|
|
4015
4288
|
})
|
|
4016
4289
|
);
|
|
@@ -5702,6 +5975,7 @@ function createProgram() {
|
|
|
5702
5975
|
registerApiKeyCommands(program);
|
|
5703
5976
|
registerSearchCommands(program);
|
|
5704
5977
|
registerIngestCommands(program);
|
|
5978
|
+
registerWebsiteImportCommands(program);
|
|
5705
5979
|
registerContentCommands(program);
|
|
5706
5980
|
registerCtaCommands(program);
|
|
5707
5981
|
registerGenerateCommands(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@senso-ai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Senso CLI — Infrastructure for the Agentic Web. Manage your Senso knowledge base, content and GEO analytics from the terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -76,5 +76,8 @@
|
|
|
76
76
|
"homepage": "https://github.com/AI-Template-SDK/senso-user-cli#readme",
|
|
77
77
|
"bugs": {
|
|
78
78
|
"url": "https://github.com/AI-Template-SDK/senso-user-cli/issues"
|
|
79
|
+
},
|
|
80
|
+
"overrides": {
|
|
81
|
+
"esbuild": "^0.28.2"
|
|
79
82
|
}
|
|
80
83
|
}
|