@lotics/cli 0.151.0 → 0.152.1
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/dist/src/cli.js +152 -31
- package/dist/src/client.d.ts +27 -4
- package/dist/src/client.js +16 -0
- package/docs/cli_reference.md +5 -3
- package/docs/knowledge_docs.md +59 -9
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -44434,6 +44434,22 @@ var LoticsClient = class {
|
|
|
44434
44434
|
async getKnowledgeDoc(knowledge_doc_id) {
|
|
44435
44435
|
return this.request("GET", `/v1/knowledge_docs/${encodeURIComponent(knowledge_doc_id)}`);
|
|
44436
44436
|
}
|
|
44437
|
+
/**
|
|
44438
|
+
* Every doc's metadata — REST rather than the `list_knowledge` tool, because
|
|
44439
|
+
* this is the surface a PERSON manages the corpus from and the tool is bound
|
|
44440
|
+
* to what the assistant may browse. That distinction is the whole point of
|
|
44441
|
+
* `include_hidden`: a hidden doc is out of the assistant's corpus by design,
|
|
44442
|
+
* and someone still has to be able to find it in order to put it back.
|
|
44443
|
+
*/
|
|
44444
|
+
async listKnowledgeDocs(opts) {
|
|
44445
|
+
const qs = opts?.includeHidden ? "?include_hidden=true" : "";
|
|
44446
|
+
return this.request("GET", `/v1/knowledge_docs${qs}`);
|
|
44447
|
+
}
|
|
44448
|
+
/** Apply one metadata change to a set of docs. Tags are an add/remove DIFF —
|
|
44449
|
+
* see the endpoint's own note on why a replacement is the wrong shape here. */
|
|
44450
|
+
async bulkUpdateKnowledgeDocs(body) {
|
|
44451
|
+
return this.request("PATCH", "/v1/knowledge_docs", body);
|
|
44452
|
+
}
|
|
44437
44453
|
// --- Apps ---
|
|
44438
44454
|
async getApp(app_id) {
|
|
44439
44455
|
return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}`);
|
|
@@ -45993,15 +46009,24 @@ var COMMANDS = [
|
|
|
45993
46009
|
{
|
|
45994
46010
|
verbs: ["knowledge"],
|
|
45995
46011
|
help: [
|
|
45996
|
-
" lotics knowledge list
|
|
45997
|
-
"
|
|
46012
|
+
" lotics knowledge list [--include-hidden]",
|
|
46013
|
+
" List knowledge docs (id, name, tags, description)",
|
|
46014
|
+
" lotics knowledge create --name <n> [--description <d>] [--tags <a,b>] --from <file.md>",
|
|
45998
46015
|
" Create a doc from a file (or --content <str>);",
|
|
45999
46016
|
" prints the new id",
|
|
46000
46017
|
" lotics knowledge get <id> [-o <file.md>]",
|
|
46001
46018
|
" Fetch a doc's content (to a file, or stdout;",
|
|
46002
46019
|
" --json for the full doc)",
|
|
46003
|
-
" lotics knowledge update <id> [--from <file.md> | --content <str>] [--name <n>] [--description <d>]",
|
|
46004
|
-
" Update a doc \u2014 sends only the fields you pass",
|
|
46020
|
+
" lotics knowledge update <id> [--from <file.md> | --content <str>] [--name <n>] [--description <d>] [--tags <a,b>]",
|
|
46021
|
+
" Update a doc \u2014 sends only the fields you pass;",
|
|
46022
|
+
" --tags REPLACES the doc's labels",
|
|
46023
|
+
" lotics knowledge tag <id...> [--add <a,b>] [--remove <c,d>]",
|
|
46024
|
+
" Re-label a set of docs; adds/removes apply to",
|
|
46025
|
+
" each doc's own labels",
|
|
46026
|
+
" lotics knowledge hide <id...> Take docs out of circulation \u2014 gone from the",
|
|
46027
|
+
" Library list and from what the assistant browses",
|
|
46028
|
+
" and searches, still readable by id",
|
|
46029
|
+
" lotics knowledge unhide <id...> Put them back",
|
|
46005
46030
|
" lotics knowledge rm <id> Archive a doc"
|
|
46006
46031
|
]
|
|
46007
46032
|
},
|
|
@@ -74491,6 +74516,9 @@ ${totalErrors} error${totalErrors === 1 ? "" : "s"} across ${results.length - pa
|
|
|
74491
74516
|
}
|
|
74492
74517
|
|
|
74493
74518
|
// src/args.ts
|
|
74519
|
+
function splitList(value2) {
|
|
74520
|
+
return (value2 ?? "").split(",").map((part) => part.trim()).filter((part) => part !== "");
|
|
74521
|
+
}
|
|
74494
74522
|
function parseArgs(argv) {
|
|
74495
74523
|
const flags = {
|
|
74496
74524
|
json: false,
|
|
@@ -74509,6 +74537,10 @@ function parseArgs(argv) {
|
|
|
74509
74537
|
description: void 0,
|
|
74510
74538
|
from: void 0,
|
|
74511
74539
|
content: void 0,
|
|
74540
|
+
tags: void 0,
|
|
74541
|
+
add: void 0,
|
|
74542
|
+
remove: void 0,
|
|
74543
|
+
includeHidden: false,
|
|
74512
74544
|
timezone: void 0,
|
|
74513
74545
|
message: void 0,
|
|
74514
74546
|
session: void 0,
|
|
@@ -74579,6 +74611,21 @@ function parseArgs(argv) {
|
|
|
74579
74611
|
case "--content":
|
|
74580
74612
|
flags.content = argv[++i2];
|
|
74581
74613
|
break;
|
|
74614
|
+
// Comma-separated AND repeatable, because a label can contain a space but
|
|
74615
|
+
// never a comma, and an agent scripting this will reach for whichever form
|
|
74616
|
+
// it happens to know. Both accumulate rather than the last winning.
|
|
74617
|
+
case "--tags":
|
|
74618
|
+
flags.tags = [...flags.tags ?? [], ...splitList(argv[++i2])];
|
|
74619
|
+
break;
|
|
74620
|
+
case "--add":
|
|
74621
|
+
flags.add = [...flags.add ?? [], ...splitList(argv[++i2])];
|
|
74622
|
+
break;
|
|
74623
|
+
case "--remove":
|
|
74624
|
+
flags.remove = [...flags.remove ?? [], ...splitList(argv[++i2])];
|
|
74625
|
+
break;
|
|
74626
|
+
case "--include-hidden":
|
|
74627
|
+
flags.includeHidden = true;
|
|
74628
|
+
break;
|
|
74582
74629
|
case "--timezone":
|
|
74583
74630
|
flags.timezone = argv[++i2];
|
|
74584
74631
|
break;
|
|
@@ -102516,6 +102563,13 @@ async function runDocxCommand(subcommand, toolArgs, restArgs) {
|
|
|
102516
102563
|
|
|
102517
102564
|
// src/knowledge.ts
|
|
102518
102565
|
import fs12 from "node:fs";
|
|
102566
|
+
|
|
102567
|
+
// ../shared/src/knowledge_tags.ts
|
|
102568
|
+
function isHiddenDoc(doc) {
|
|
102569
|
+
return doc.hidden_at != null;
|
|
102570
|
+
}
|
|
102571
|
+
|
|
102572
|
+
// src/knowledge.ts
|
|
102519
102573
|
function readBodyFile(filePath) {
|
|
102520
102574
|
if (!fs12.existsSync(filePath)) fail2(`File not found: ${filePath}`);
|
|
102521
102575
|
return fs12.readFileSync(filePath, "utf-8");
|
|
@@ -102534,37 +102588,46 @@ function unwrap(res) {
|
|
|
102534
102588
|
return res.result;
|
|
102535
102589
|
}
|
|
102536
102590
|
async function knowledgeList(client, flags) {
|
|
102537
|
-
const
|
|
102538
|
-
const payload = result;
|
|
102539
|
-
const results = Array.isArray(payload?.results) ? payload.results : [];
|
|
102591
|
+
const docs = await client.listKnowledgeDocs({ includeHidden: flags.includeHidden });
|
|
102540
102592
|
if (flags.json) {
|
|
102541
|
-
console.log(JSON.stringify(
|
|
102593
|
+
console.log(JSON.stringify(docs, null, 2));
|
|
102542
102594
|
return;
|
|
102543
102595
|
}
|
|
102544
|
-
if (
|
|
102545
|
-
console.error(
|
|
102596
|
+
if (docs.length === 0) {
|
|
102597
|
+
console.error(
|
|
102598
|
+
flags.includeHidden ? "No knowledge docs in this workspace." : "No knowledge docs in this workspace. Add --include-hidden if some may be out of circulation."
|
|
102599
|
+
);
|
|
102546
102600
|
return;
|
|
102547
102601
|
}
|
|
102548
|
-
const rows =
|
|
102549
|
-
|
|
102550
|
-
|
|
102551
|
-
|
|
102552
|
-
|
|
102553
|
-
|
|
102554
|
-
|
|
102555
|
-
|
|
102556
|
-
|
|
102602
|
+
const rows = docs.map((doc) => ({
|
|
102603
|
+
id: doc.id,
|
|
102604
|
+
name: doc.name,
|
|
102605
|
+
tags: doc.tags.join(", "),
|
|
102606
|
+
// Collapse whitespace so a multi-line description never breaks the row.
|
|
102607
|
+
description: doc.description.replace(/\s+/g, " ").trim(),
|
|
102608
|
+
// The shared predicate, not a comparison written here: this CLI is pinned
|
|
102609
|
+
// per version and runs against whatever backend a workspace is on, including
|
|
102610
|
+
// one that predates the field, and reading its absence as hidden would mark
|
|
102611
|
+
// every row.
|
|
102612
|
+
hidden: isHiddenDoc(doc)
|
|
102613
|
+
}));
|
|
102557
102614
|
const wId = Math.max("ID".length, ...rows.map((r) => r.id.length));
|
|
102558
102615
|
const wName = Math.max("NAME".length, ...rows.map((r) => r.name.length));
|
|
102616
|
+
const wTags = Math.max("TAGS".length, ...rows.map((r) => r.tags.length));
|
|
102559
102617
|
const pad = (s, n) => s.padEnd(n);
|
|
102560
|
-
console.log(`${pad("ID", wId)} ${pad("NAME", wName)} DESCRIPTION`);
|
|
102618
|
+
console.log(`${pad("ID", wId)} ${pad("NAME", wName)} ${pad("TAGS", wTags)} DESCRIPTION`);
|
|
102561
102619
|
for (const r of rows) {
|
|
102562
|
-
|
|
102620
|
+
const description = r.hidden ? `(hidden) ${r.description}`.trimEnd() : r.description;
|
|
102621
|
+
console.log(`${pad(r.id, wId)} ${pad(r.name, wName)} ${pad(r.tags, wTags)} ${description}`.trimEnd());
|
|
102563
102622
|
}
|
|
102564
102623
|
}
|
|
102565
102624
|
async function knowledgeCreate(client, flags) {
|
|
102566
102625
|
const name2 = flags.name;
|
|
102567
|
-
if (!name2)
|
|
102626
|
+
if (!name2) {
|
|
102627
|
+
fail2(
|
|
102628
|
+
"Usage: lotics knowledge create --name <name> [--description <d>] [--tags <a,b>] (--from <file.md> | --content <str>)"
|
|
102629
|
+
);
|
|
102630
|
+
}
|
|
102568
102631
|
const content = resolveBody(flags, true);
|
|
102569
102632
|
const result = unwrap(
|
|
102570
102633
|
await client.execute("create_knowledge", {
|
|
@@ -102572,7 +102635,11 @@ async function knowledgeCreate(client, flags) {
|
|
|
102572
102635
|
// The tool requires a description (empty string is accepted); the CLI
|
|
102573
102636
|
// treats it as optional and defaults to "".
|
|
102574
102637
|
description: flags.description ?? "",
|
|
102575
|
-
content
|
|
102638
|
+
content,
|
|
102639
|
+
// Filed at the moment it is made. Creating and then labelling is two round
|
|
102640
|
+
// trips and leaves the doc unfiled in between, which is when a corpus
|
|
102641
|
+
// acquires the untagged rows nobody goes back for.
|
|
102642
|
+
...flags.tags !== void 0 && { tags: flags.tags }
|
|
102576
102643
|
})
|
|
102577
102644
|
);
|
|
102578
102645
|
if (flags.json) {
|
|
@@ -102598,15 +102665,20 @@ async function knowledgeGet(client, id, flags) {
|
|
|
102598
102665
|
console.log(doc.content);
|
|
102599
102666
|
}
|
|
102600
102667
|
async function knowledgeUpdate(client, id, flags) {
|
|
102601
|
-
if (!id)
|
|
102668
|
+
if (!id) {
|
|
102669
|
+
fail2(
|
|
102670
|
+
"Usage: lotics knowledge update <id> [--from <file.md> | --content <str>] [--name <n>] [--description <d>] [--tags <a,b>]"
|
|
102671
|
+
);
|
|
102672
|
+
}
|
|
102602
102673
|
const content = resolveBody(flags, false);
|
|
102603
|
-
if (content === void 0 && flags.name === void 0 && flags.description === void 0) {
|
|
102604
|
-
fail2("Nothing to update. Pass at least one of --from/--content, --name, or --
|
|
102674
|
+
if (content === void 0 && flags.name === void 0 && flags.description === void 0 && flags.tags === void 0) {
|
|
102675
|
+
fail2("Nothing to update. Pass at least one of --from/--content, --name, --description, or --tags.");
|
|
102605
102676
|
}
|
|
102606
102677
|
const args = { knowledge_doc_id: id };
|
|
102607
102678
|
if (content !== void 0) args.content = content;
|
|
102608
102679
|
if (flags.name !== void 0) args.name = flags.name;
|
|
102609
102680
|
if (flags.description !== void 0) args.description = flags.description;
|
|
102681
|
+
if (flags.tags !== void 0) args.tags = flags.tags;
|
|
102610
102682
|
const result = unwrap(await client.execute("update_knowledge", args));
|
|
102611
102683
|
if (flags.json) {
|
|
102612
102684
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -102623,21 +102695,64 @@ async function knowledgeRm(client, id, flags) {
|
|
|
102623
102695
|
}
|
|
102624
102696
|
console.error(`Deleted knowledge doc: ${id}`);
|
|
102625
102697
|
}
|
|
102698
|
+
function requireIds(ids, usage) {
|
|
102699
|
+
const parsed = ids.filter((id) => id !== void 0).flatMap((id) => id.split(",")).map((id) => id.trim()).filter((id) => id !== "");
|
|
102700
|
+
if (parsed.length === 0) fail2(usage);
|
|
102701
|
+
return parsed;
|
|
102702
|
+
}
|
|
102703
|
+
async function knowledgeTag(client, ids, flags) {
|
|
102704
|
+
const knowledge_doc_ids = requireIds(ids, "Usage: lotics knowledge tag <id...> [--add a,b] [--remove c,d]");
|
|
102705
|
+
if (flags.add === void 0 && flags.remove === void 0) {
|
|
102706
|
+
fail2("Pass --add <tags> and/or --remove <tags>.");
|
|
102707
|
+
}
|
|
102708
|
+
const updated = await client.bulkUpdateKnowledgeDocs({
|
|
102709
|
+
knowledge_doc_ids,
|
|
102710
|
+
...flags.add !== void 0 && { add_tags: flags.add },
|
|
102711
|
+
...flags.remove !== void 0 && { remove_tags: flags.remove }
|
|
102712
|
+
});
|
|
102713
|
+
if (flags.json) {
|
|
102714
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
102715
|
+
return;
|
|
102716
|
+
}
|
|
102717
|
+
for (const doc of updated) {
|
|
102718
|
+
console.error(`${doc.id} ${doc.tags.length > 0 ? doc.tags.join(", ") : "(no tags)"}`);
|
|
102719
|
+
}
|
|
102720
|
+
}
|
|
102721
|
+
async function knowledgeSetHidden(client, ids, hidden, flags) {
|
|
102722
|
+
const verb = hidden ? "hide" : "unhide";
|
|
102723
|
+
const knowledge_doc_ids = requireIds(ids, `Usage: lotics knowledge ${verb} <id...>`);
|
|
102724
|
+
const updated = await client.bulkUpdateKnowledgeDocs({ knowledge_doc_ids, hidden });
|
|
102725
|
+
if (flags.json) {
|
|
102726
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
102727
|
+
return;
|
|
102728
|
+
}
|
|
102729
|
+
console.error(
|
|
102730
|
+
hidden ? `Hid ${updated.length} doc(s). They are out of the assistant's corpus; lotics knowledge list --include-hidden still finds them.` : `Restored ${updated.length} doc(s) to the corpus.`
|
|
102731
|
+
);
|
|
102732
|
+
}
|
|
102626
102733
|
function printKnowledgeHelp() {
|
|
102627
102734
|
console.error(`Lotics knowledge commands \u2014 manage the AI's rulebook docs.
|
|
102628
102735
|
|
|
102629
|
-
lotics knowledge list [--json]
|
|
102630
|
-
Catalog every doc you can use (id, name, description).
|
|
102631
|
-
lotics knowledge create --name <n> [--description <d>] (--from <file.md> | --content <str>) [--json]
|
|
102736
|
+
lotics knowledge list [--include-hidden] [--json]
|
|
102737
|
+
Catalog every doc you can use (id, name, tags, description).
|
|
102738
|
+
lotics knowledge create --name <n> [--description <d>] [--tags <a,b>] (--from <file.md> | --content <str>) [--json]
|
|
102632
102739
|
Create a doc; the body comes from a file or an inline string. Prints the new id.
|
|
102633
102740
|
lotics knowledge get <id> [-o <file.md>] [--json]
|
|
102634
102741
|
Fetch a doc's content \u2014 to <file.md>, or stdout. --json prints the full doc.
|
|
102635
102742
|
lotics knowledge update <id> [--from <file.md> | --content <str>] [--name <n>] [--description <d>] [--json]
|
|
102636
102743
|
Update a doc; sends only the fields you pass.
|
|
102744
|
+
lotics knowledge tag <id...> [--add a,b] [--remove c,d] [--json]
|
|
102745
|
+
Re-label a set of docs. Adds and removes are a DIFF applied to each doc's
|
|
102746
|
+
own labels \u2014 use update --tags to state one doc's complete set.
|
|
102747
|
+
lotics knowledge hide <id...> [--json]
|
|
102748
|
+
Take docs out of circulation: gone from the Library's list and from what
|
|
102749
|
+
the assistant browses and searches, still readable by id, nothing deleted.
|
|
102750
|
+
lotics knowledge unhide <id...> [--json]
|
|
102751
|
+
Put them back.
|
|
102637
102752
|
lotics knowledge rm <id> [--json]
|
|
102638
102753
|
Archive a doc.`);
|
|
102639
102754
|
}
|
|
102640
|
-
async function runKnowledgeCommand(client, subcommand, toolArgs, flags) {
|
|
102755
|
+
async function runKnowledgeCommand(client, subcommand, toolArgs, flags, restArgs = []) {
|
|
102641
102756
|
switch (subcommand) {
|
|
102642
102757
|
case "list":
|
|
102643
102758
|
return knowledgeList(client, flags);
|
|
@@ -102647,6 +102762,12 @@ async function runKnowledgeCommand(client, subcommand, toolArgs, flags) {
|
|
|
102647
102762
|
return knowledgeGet(client, toolArgs, flags);
|
|
102648
102763
|
case "update":
|
|
102649
102764
|
return knowledgeUpdate(client, toolArgs, flags);
|
|
102765
|
+
case "tag":
|
|
102766
|
+
return knowledgeTag(client, [toolArgs, ...restArgs], flags);
|
|
102767
|
+
case "hide":
|
|
102768
|
+
return knowledgeSetHidden(client, [toolArgs, ...restArgs], true, flags);
|
|
102769
|
+
case "unhide":
|
|
102770
|
+
return knowledgeSetHidden(client, [toolArgs, ...restArgs], false, flags);
|
|
102650
102771
|
case "rm":
|
|
102651
102772
|
return knowledgeRm(client, toolArgs, flags);
|
|
102652
102773
|
default:
|
|
@@ -103677,7 +103798,7 @@ Available workspaces:`);
|
|
|
103677
103798
|
applyAppManifestWorkspace(ctx, command, subcommand, appPathArg, flags);
|
|
103678
103799
|
await resolveWorkspace(client, ctx);
|
|
103679
103800
|
if (command === "knowledge") {
|
|
103680
|
-
await runKnowledgeCommand(client, subcommand, toolArgs, flags);
|
|
103801
|
+
await runKnowledgeCommand(client, subcommand, toolArgs, flags, restArgs);
|
|
103681
103802
|
return;
|
|
103682
103803
|
}
|
|
103683
103804
|
if (command === "app") {
|
package/dist/src/client.d.ts
CHANGED
|
@@ -86,19 +86,24 @@ export interface ToolInfo {
|
|
|
86
86
|
* as `expected_content_sha`; the `update_knowledge` TOOL CASes internally, so a
|
|
87
87
|
* CLI caller never needs to pass it.
|
|
88
88
|
*/
|
|
89
|
-
|
|
89
|
+
/** A doc's metadata, as every listing serves it — no body. */
|
|
90
|
+
export interface KnowledgeDocSummary {
|
|
90
91
|
id: string;
|
|
91
92
|
workspace_id: string;
|
|
92
93
|
name: string;
|
|
93
94
|
description: string;
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
/** How the doc is classified. Order-insensitive; empty means untagged. */
|
|
96
|
+
tags: string[];
|
|
97
|
+
/** When the doc was taken out of circulation, or null while it is in use. */
|
|
98
|
+
hidden_at: string | null;
|
|
97
99
|
content_sha: string | null;
|
|
98
100
|
files: unknown[];
|
|
99
101
|
created_at: string;
|
|
100
102
|
updated_at: string;
|
|
101
103
|
}
|
|
104
|
+
export interface KnowledgeDocDetail extends KnowledgeDocSummary {
|
|
105
|
+
content: string;
|
|
106
|
+
}
|
|
102
107
|
export interface FileUploadResult {
|
|
103
108
|
files: Array<{
|
|
104
109
|
id: string;
|
|
@@ -228,6 +233,24 @@ export declare class LoticsClient {
|
|
|
228
233
|
* the file-model and legacy parked-column rows. Mirrors GET /v1/knowledge_docs/{id}.
|
|
229
234
|
*/
|
|
230
235
|
getKnowledgeDoc(knowledge_doc_id: string): Promise<KnowledgeDocDetail>;
|
|
236
|
+
/**
|
|
237
|
+
* Every doc's metadata — REST rather than the `list_knowledge` tool, because
|
|
238
|
+
* this is the surface a PERSON manages the corpus from and the tool is bound
|
|
239
|
+
* to what the assistant may browse. That distinction is the whole point of
|
|
240
|
+
* `include_hidden`: a hidden doc is out of the assistant's corpus by design,
|
|
241
|
+
* and someone still has to be able to find it in order to put it back.
|
|
242
|
+
*/
|
|
243
|
+
listKnowledgeDocs(opts?: {
|
|
244
|
+
includeHidden?: boolean;
|
|
245
|
+
}): Promise<KnowledgeDocSummary[]>;
|
|
246
|
+
/** Apply one metadata change to a set of docs. Tags are an add/remove DIFF —
|
|
247
|
+
* see the endpoint's own note on why a replacement is the wrong shape here. */
|
|
248
|
+
bulkUpdateKnowledgeDocs(body: {
|
|
249
|
+
knowledge_doc_ids: string[];
|
|
250
|
+
add_tags?: string[];
|
|
251
|
+
remove_tags?: string[];
|
|
252
|
+
hidden?: boolean;
|
|
253
|
+
}): Promise<KnowledgeDocSummary[]>;
|
|
231
254
|
getApp(app_id: string): Promise<{
|
|
232
255
|
id: string;
|
|
233
256
|
name: string;
|
package/dist/src/client.js
CHANGED
|
@@ -199,6 +199,22 @@ export class LoticsClient {
|
|
|
199
199
|
async getKnowledgeDoc(knowledge_doc_id) {
|
|
200
200
|
return this.request("GET", `/v1/knowledge_docs/${encodeURIComponent(knowledge_doc_id)}`);
|
|
201
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Every doc's metadata — REST rather than the `list_knowledge` tool, because
|
|
204
|
+
* this is the surface a PERSON manages the corpus from and the tool is bound
|
|
205
|
+
* to what the assistant may browse. That distinction is the whole point of
|
|
206
|
+
* `include_hidden`: a hidden doc is out of the assistant's corpus by design,
|
|
207
|
+
* and someone still has to be able to find it in order to put it back.
|
|
208
|
+
*/
|
|
209
|
+
async listKnowledgeDocs(opts) {
|
|
210
|
+
const qs = opts?.includeHidden ? "?include_hidden=true" : "";
|
|
211
|
+
return this.request("GET", `/v1/knowledge_docs${qs}`);
|
|
212
|
+
}
|
|
213
|
+
/** Apply one metadata change to a set of docs. Tags are an add/remove DIFF —
|
|
214
|
+
* see the endpoint's own note on why a replacement is the wrong shape here. */
|
|
215
|
+
async bulkUpdateKnowledgeDocs(body) {
|
|
216
|
+
return this.request("PATCH", "/v1/knowledge_docs", body);
|
|
217
|
+
}
|
|
202
218
|
// --- Apps ---
|
|
203
219
|
async getApp(app_id) {
|
|
204
220
|
return this.request("GET", `/v1/apps/${encodeURIComponent(app_id)}`);
|
package/docs/cli_reference.md
CHANGED
|
@@ -24,10 +24,12 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
|
|
|
24
24
|
| `lotics run <tool> --json '<json>'` | Execute a tool (full JSON output) |
|
|
25
25
|
| `lotics upload <file\|dir...>` · `--stdin` · `--base64` · `--url <url>` | Upload files/directories via multipart POST to /v1/files. A directory expands to its immediate files; `--as <name>` renames a single upload. **Three alternative byte sources, for a caller that never had the bytes on disk** — an attachment decoded in memory, a generated document, a signed download link — each mutually exclusive with the others and with a path argument: `--stdin` takes raw bytes on stdin, `--base64` takes base64 on stdin (the shape attachments arrive in), `--url <url>` fetches the URL first. `--stdin`/`--base64` REQUIRE `--as`, because stdin carries no filename and the mime type is derived from it; `--url` falls back to `Content-Disposition` then the URL's last path segment. `--base64` decodes STRICTLY — `Buffer.from(s, "base64")` silently skips invalid characters and truncates on bad padding, so a corrupted pipe would otherwise store a short file that only fails when a human opens it. The `--url` fetch happens in the CLI, not the server: the URL comes from the operator running the command, so routing it through the backend would add an SSRF surface to buy what `curl` already does. |
|
|
26
26
|
| `lotics file download <file_id> [-o <dir>]` | (alias `lotics download`) Download a stored file: `GET /v1/files/{id}/signed_url` → fetch the presigned URL, saving under the stored filename (from the response's `Content-Disposition`) into `-o` (a **DIRECTORY** — note this is distinct from `lotics file preview`'s `-o`, which is a FILE path), else cwd. `lotics file download record <record_id> <field_key>` pulls every file on a record's file field. |
|
|
27
|
-
| `lotics knowledge list` |
|
|
28
|
-
| `lotics knowledge create --name <n> [--description <d>] (--from <file.md> \| --content <str>)` | Read the body client-side (a file XOR an inline string — exactly one required), then call `create_knowledge` with `{ name, description, content }` (description defaults to `""`). Prints the new id to stdout. Large files ride the POST body fine. |
|
|
27
|
+
| `lotics knowledge list [--include-hidden]` | `GET /v1/knowledge_docs` — a table of id, name, tags, description (`--json` for the docs). **REST, not the `list_knowledge` tool**: the tool answers what the ASSISTANT may browse, and a hidden doc is out of that corpus by definition, so a tool-backed listing could never show one and the person who hid it would have no way back to it. Hidden docs are left out unless `--include-hidden` asks; those rows are marked `(hidden)`. |
|
|
28
|
+
| `lotics knowledge create --name <n> [--description <d>] [--tags <a,b>] (--from <file.md> \| --content <str>)` | Read the body client-side (a file XOR an inline string — exactly one required), then call `create_knowledge` with `{ name, description, content, tags? }` (description defaults to `""`). `--tags` files the doc as it is made, which is the only moment a corpus reliably gets labelled. Prints the new id to stdout. Large files ride the POST body fine. |
|
|
29
29
|
| `lotics knowledge get <id> [-o <file.md>]` | `GET /v1/knowledge_docs/{id}` (`getKnowledgeDoc`) → the doc with its **hydrated `content`** (the one content-read path for a non-sandbox client; works for file-model AND legacy parked-column rows). `-o` writes the body via `writeFileAtomic`; else the body goes to stdout. `--json` prints the full doc instead. |
|
|
30
|
-
| `lotics knowledge update <id> [--from <file.md> \| --content <str>] [--name <n>] [--description <d>]` | Call `update_knowledge` with **only** the provided fields (a body from --from/--content becomes `content`; the tool diffs + CASes the content change internally, so the CLI passes no `expected_content_file_id`). At least one field required; --from and --content are mutually exclusive. |
|
|
30
|
+
| `lotics knowledge update <id> [--from <file.md> \| --content <str>] [--name <n>] [--description <d>] [--tags <a,b>]` | Call `update_knowledge` with **only** the provided fields (a body from --from/--content becomes `content`; the tool diffs + CASes the content change internally, so the CLI passes no `expected_content_file_id`). `--tags` REPLACES the doc's label set — the single-doc form, where the caller is looking at one doc and can state what it should carry. At least one field required; --from and --content are mutually exclusive. |
|
|
31
|
+
| `lotics knowledge tag <id...> [--add <a,b>] [--remove <c,d>]` | `PATCH /v1/knowledge_docs` with `{ knowledge_doc_ids, add_tags?, remove_tags? }` — one transaction over the whole set. A **DIFF applied to each doc's own labels**, never a replacement: the docs named on one command line carry different labels, so one array across them would strip whatever the others were filed under. Removal matches case-insensitively; adding a label a doc already carries writes nothing. Ids may be separate arguments or comma-separated. At least one of --add/--remove required. |
|
|
32
|
+
| `lotics knowledge hide <id...>` / `lotics knowledge unhide <id...>` | `PATCH /v1/knowledge_docs` with `{ knowledge_doc_ids, hidden }`. Hiding takes docs out of every **listing** — the Library's list, `list_knowledge`, and the corpus `grep_knowledge` searches — while leaving IAM untouched and keeping them readable **by id** (`read_knowledge` with an id, a code run staging one, an app agent's declared set). So it can never silently break an app that depends on a doc, and unhiding costs nothing. Refuses the no-argument form rather than reading it as "everything". |
|
|
31
33
|
| `lotics knowledge rm <id>` | Archive the doc via `delete_knowledge` (`{ knowledge_doc_id }`). The REST execute path does not gate `needsApproval`, so this runs unattended. |
|
|
32
34
|
| `lotics app create <name> [path]` | Scaffold a Vite+React+TS custom-code app project; POST /v1/apps; npm install; vite build; upload as v1 |
|
|
33
35
|
| `lotics app pull <app_id> [path]` | Download source archive from R2 (presigned), extract, npm install, stamp package.json's `lotics` field. With no `[path]`: refresh the cwd IN PLACE when it's already this app's own project (its manifest `app_id` matches — the documented `cd <app> && lotics app pull` flow), else clone into an `<name>/` subdir. **A pull never overwrites a file that differs from what it is about to write** — it writes only what is ABSENT or already identical, keeps the rest, and reports which files it kept plus the commands that close the gap. The same rule covers `src/workflows/<alias>.ts` and `src/agents/<alias>.md`, so an unpushed body or prompt survives too. Those two are written from the LIVE App row (`apps.workflows` / `apps.agents`), which owns them, and the archive's own copy of them is deliberately SKIPPED on extract: a deploy tars the whole source directory, so the tarball holds a deploy-time snapshot that is stale for anything authored since. The comparison is against the app's own content, not git, so it holds for a project that was never a repo. `--force` takes the app's copy and DISCARDS local edits; there is no other way to lose them. **When a pull ACROSS versions keeps files, the manifest is left on the OLDER of the two versions** — the tree is then part one and part the other, and claiming the newer would make `deploy`'s `prev_version_id` check pass and ship a half-and-half bundle. Older, not "the one it had": `--from-version` pulls a deliberately old revision, so the version it had is the NEWER side, and holding that would match what the server serves and let the old source ship. Either way a deploy from that tree is refused until you reconcile the listed files by hand and pull again, or take the app's copy with `--force`. The report says which version each side is on, because a kept file is your unshipped work when the project was already current and merely the OLD version when it was behind — and nothing in a byte comparison can tell those apart. **`--from-version <apv_…>`** pulls an OLDER revision instead of the current one (`lotics app versions` lists the ids) — point it at a NEW path to read a previous revision without disturbing the project you are in. The manifest records the version actually written, never the live pointer, so a deploy from that checkout is refused by the version guard rather than shipping old source over newer. — `workflows` and `agents` are sourced from the live App row (NOT the archived manifest), so `set_app_workflow` / `set_app_agent` authoring survives the pull. Regenerates `.lotics/app_{workflows,queries,agents}.d.ts` so `useWorkflow` / `useQuery` / `useAgentRun` stay typed, AND — AFTER `npm install`, so `node_modules/@lotics/ui` actually exists to read — `.lotics/react_native.d.ts` (the kit's web-only ViewStyle/TextStyle augmentation) and `.lotics/tsconfig.link.json`'s peer pins; a pulled project's own `tsc` used to fail the moment it used a component relying on the augmentation (Dialog, Picker, TimePicker, …) until `app codegen` was run by hand, because nothing had regenerated either one after install populated node_modules. AND the runtime `.lotics/app_fields.ts` (the same linked-vs-bespoke branch `app codegen` runs, off the app row already fetched — see that row for the two forms). That one is not optional: `app deploy` tars source with `--exclude=.lotics`, so no archive can carry it, and a pulled project whose `src/` imports `F`/`OPT` would fail to build with `Could not resolve "../../.lotics/app_fields"` until `app codegen` was run by hand. The write NAMES the form and the reason, because an in-place pull can FLIP a project between them (`opctl app publish` links an origin, `package eject` unlinks it) and that changes what the module does at load. Skipped under `--view-as` (the schema is read as that member and silently drops tables they cannot see — a narrowed `F` map compiles and then throws at runtime, worse than the missing module). A binding/schema fetch failure is non-fatal and names the right recovery for what is on disk: an existing file is kept, an ABSENT one warns about the build error and points at `app codegen`. Pull GENERATES but never RECONCILES `.lotics/` — deleting a companion whose alias the manifest no longer declares is `app codegen`'s alone, since pull's authority is the server's alias set and a declared-but-not-yet-`set` alias is supported. Also writes one `src/workflows/<alias>.ts` per bound workflow (faithful body from `get_app_workflow`) and one `src/agents/<alias>.md` per bound agent (its instructions, straight off the live row) — so the prose an author actually edits lives in a file, and pull always overwrites it from live, leaving no second copy to drift. A legacy workflow alias with no rendered source, or an agent with no instructions, warns and is skipped. The stamped `lotics.agents` map carries the TYPED half only (`inputs`/`outputs`/`tool_names`/`model_tier`/…) — an agent's prose lives solely in its `.md`, so there is never a second local copy to desync; a stale `instructions` left by an older CLI is inert and disappears on the next pull |
|
package/docs/knowledge_docs.md
CHANGED
|
@@ -24,23 +24,30 @@ the doc, the doc is noise.
|
|
|
24
24
|
```bash
|
|
25
25
|
lotics knowledge create --name "Shipping tariffs" \
|
|
26
26
|
--description "HS-coded rates; searchable by lane and code" \
|
|
27
|
+
--tags "HS,nhập khẩu" \ # file it as you make it — see Classifying, below
|
|
27
28
|
--from ./tariffs.md # the body is a Markdown file; or --content '<inline>'
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
This reads the body from your filesystem and creates the doc through `create_knowledge`
|
|
31
32
|
(prints the new id). A raw `lotics run create_knowledge @doc.json` works too — read a large
|
|
32
|
-
`content` from a file or stdin.
|
|
33
|
+
`content` from a file or stdin. `create_knowledge` takes five fields, three of them required:
|
|
33
34
|
|
|
34
|
-
- `name` — what it is.
|
|
35
|
+
- `name` — what it is. Just the name: **grouping belongs in `tags`, not in a prefix baked into
|
|
36
|
+
every title.**
|
|
35
37
|
- `description` — what the doc is **and what to grep it for**: its vocabulary and the synonyms
|
|
36
38
|
an ambiguous query would use. Surfaced in the tree before any body is read, and truncated at
|
|
37
39
|
200 characters. See *Write for grep* below.
|
|
38
40
|
- `content` — Markdown.
|
|
41
|
+
- `tags` *(optional)* — the labels someone would filter by, e.g. `["Hồ sơ NOXH", "Khoản 13"]`.
|
|
42
|
+
Give **every** one that is true of the doc, not just the main one — a corpus is narrowed by
|
|
43
|
+
intersecting tags, so a doc carrying one label can only ever be found down one path. On
|
|
44
|
+
`update_knowledge` the array **replaces** what is stored, so resend the labels it should keep.
|
|
45
|
+
- `files` *(optional)* — file ids of the source `.docx`/`.pdf`/`.xlsx` the doc was built from,
|
|
46
|
+
stored alongside it so a reader can open the original. The body stays the searchable text.
|
|
39
47
|
|
|
40
48
|
A new doc is created **owned by you, active in your own agent context, and private** — no one
|
|
41
|
-
else can see it yet.
|
|
42
|
-
|
|
43
|
-
tool.)
|
|
49
|
+
else can see it yet. Changing a doc's default-active state is done through the web app / REST
|
|
50
|
+
surface, not this tool.
|
|
44
51
|
|
|
45
52
|
## Access vs. activation — both are required
|
|
46
53
|
|
|
@@ -143,13 +150,53 @@ lotics knowledge update kdc_... --from ./tariffs.md # replace the body (--name
|
|
|
143
150
|
```
|
|
144
151
|
|
|
145
152
|
Send only the fields you're changing. `--from` / `--content` replaces the body; `--name` /
|
|
146
|
-
`--description` change metadata. `update_knowledge` resolves concurrency **internally** — it
|
|
153
|
+
`--description` / `--tags` change metadata. `update_knowledge` resolves concurrency **internally** — it
|
|
147
154
|
re-reads the current content pointer and version-chains the new body — so there is no version
|
|
148
155
|
token to pass from the CLI. (The chat agent may instead send an `edits` array — anchored
|
|
149
156
|
replace / insert / append — for a surgical change; see `lotics tools update_knowledge`.) Refine
|
|
150
157
|
structure as you learn what users actually ask: add the synonym that failed to match, split a
|
|
151
158
|
line that was too coarse, restate a term the heading was carrying.
|
|
152
159
|
|
|
160
|
+
## Classifying a corpus — tags
|
|
161
|
+
|
|
162
|
+
A doc carries a SET of labels, not a folder. Order does not matter, a doc can wear several, and
|
|
163
|
+
the vocabulary is whatever the docs themselves use — a tag stops existing the moment nothing
|
|
164
|
+
carries it.
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
lotics knowledge update kdc_... --tags "HS,nhập khẩu" # ONE doc: state its complete set
|
|
168
|
+
lotics knowledge tag kdc_a kdc_b --add HS --remove draft # MANY docs: a diff applied to each
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The split is deliberate. Looking at one doc you know what it should carry, so `--tags` replaces.
|
|
172
|
+
Across a set you do not — the docs carry different labels — so `tag` adds and removes against
|
|
173
|
+
each doc's own set, and never states one classification for all of them. Removal ignores case;
|
|
174
|
+
adding a label a doc already carries changes nothing.
|
|
175
|
+
|
|
176
|
+
An agent narrows by tag rather than reading the whole corpus:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
lotics tools list_knowledge # takes an optional tag
|
|
180
|
+
lotics tools grep_knowledge # same
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Taking a doc out of circulation — `hide`
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
lotics knowledge hide kdc_... # superseded, draft, source material
|
|
187
|
+
lotics knowledge list --include-hidden # find it again
|
|
188
|
+
lotics knowledge unhide kdc_... # put it back
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
A hidden doc is out of every **listing**: it is gone from the Library's list, from
|
|
192
|
+
`list_knowledge`, and from the corpus `grep_knowledge` searches, so nothing finds it by browsing.
|
|
193
|
+
It is not deleted, not unshared, and still **reads when addressed by id** — `read_knowledge` with
|
|
194
|
+
its id, a code run staging it, an app agent that declares it. That is the point: hiding is a
|
|
195
|
+
statement about discovery, so it cannot silently break an app that depends on a doc by name.
|
|
196
|
+
|
|
197
|
+
Reach for it instead of `rm` whenever the material still matters: last year's tariff schedule, a
|
|
198
|
+
handbook a newer one replaced, the raw source a curated doc was written from.
|
|
199
|
+
|
|
153
200
|
## Package-managed knowledge
|
|
154
201
|
|
|
155
202
|
A knowledge doc can also be delivered as part of a **content package** — a versioned corpus of
|
|
@@ -161,12 +208,15 @@ same either way; the package layer just manages distribution and version pinning
|
|
|
161
208
|
## Reaching the tools
|
|
162
209
|
|
|
163
210
|
```bash
|
|
164
|
-
lotics knowledge list # catalog: id, name, description
|
|
211
|
+
lotics knowledge list # catalog: id, name, tags, description
|
|
165
212
|
lotics knowledge get kdc_... -o doc.md # read a body to a file (omit -o for stdout)
|
|
166
213
|
lotics tools update_knowledge # full input schema for any knowledge tool
|
|
167
214
|
```
|
|
168
215
|
|
|
169
216
|
The Knowledge category covers `list_knowledge`, `create_knowledge`, `update_knowledge`, and
|
|
170
|
-
`delete_knowledge` — fronted by the `lotics knowledge list | create | get | update |
|
|
171
|
-
commands
|
|
217
|
+
`delete_knowledge` — fronted by the `lotics knowledge list | create | get | update | tag | hide |
|
|
218
|
+
unhide | rm` commands, and reachable over MCP as well, so a corpus can be authored from whichever
|
|
219
|
+
surface you already work in. `list`, `tag`, `hide` and `unhide` go over REST rather than a tool, because
|
|
220
|
+
they are a person's view of the corpus: the tools answer what the ASSISTANT may browse, and a
|
|
221
|
+
hidden doc is out of that set by definition. Sharing a doc to other members is `share_resource` / `unshare_resource` (category
|
|
172
222
|
**Admin**).
|