@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 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 List knowledge docs (id, name, description)",
45997
- " lotics knowledge create --name <n> [--description <d>] --from <file.md>",
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 result = unwrap(await client.execute("list_knowledge", {}));
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(results, null, 2));
102593
+ console.log(JSON.stringify(docs, null, 2));
102542
102594
  return;
102543
102595
  }
102544
- if (results.length === 0) {
102545
- console.error("No knowledge docs in this workspace.");
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 = results.map((r) => {
102549
- const doc = r;
102550
- return {
102551
- id: typeof doc.id === "string" ? doc.id : "",
102552
- name: typeof doc.name === "string" ? doc.name : "",
102553
- // Collapse whitespace so a multi-line description never breaks the row.
102554
- description: typeof doc.description === "string" ? doc.description.replace(/\s+/g, " ").trim() : ""
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
- console.log(`${pad(r.id, wId)} ${pad(r.name, wName)} ${r.description}`.trimEnd());
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) fail2("Usage: lotics knowledge create --name <name> [--description <d>] (--from <file.md> | --content <str>)");
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) fail2("Usage: lotics knowledge update <id> [--from <file.md> | --content <str>] [--name <n>] [--description <d>]");
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 --description.");
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") {
@@ -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
- export interface KnowledgeDocDetail {
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
- /** Folder address; "" is root. */
95
- path: string;
96
- content: string;
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;
@@ -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)}`);
@@ -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` | List knowledge docs via the `list_knowledge` tool — a table of id, name, description (`--json` for the raw results array). |
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 |
@@ -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. Either way, `create_knowledge` takes three things:
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. (The tool itself takes only name/description/content; attaching files to a
42
- doc and changing its default-active state are done through the web app / REST surface, not this
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 | rm`
171
- commands. Sharing a doc to other members is `share_resource` / `unshare_resource` (category
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**).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.151.0",
3
+ "version": "0.152.1",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {