@lotics/cli 0.150.1 → 0.151.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/dist/src/cli.js CHANGED
@@ -45132,14 +45132,27 @@ var LoticsClient = class {
45132
45132
  );
45133
45133
  }
45134
45134
  async uploadFiles(filePaths, options) {
45135
- const formData = new FormData();
45135
+ const items = [];
45136
45136
  for (let i2 = 0; i2 < filePaths.length; i2++) {
45137
45137
  const absolutePath = path2.resolve(filePaths[i2]);
45138
- const buffer = await fs2.promises.readFile(absolutePath);
45139
- const filename = options?.filenames?.[i2] ?? path2.basename(absolutePath);
45140
- const mimeType = getMimeType(filename);
45141
- const blob = new Blob([buffer], { type: mimeType });
45142
- formData.append("file", blob, filename);
45138
+ items.push({
45139
+ bytes: await fs2.promises.readFile(absolutePath),
45140
+ filename: options?.filenames?.[i2] ?? path2.basename(absolutePath)
45141
+ });
45142
+ }
45143
+ return this.uploadFileBytes(items);
45144
+ }
45145
+ /**
45146
+ * Store files from bytes the caller already holds — the path for a caller that
45147
+ * never had them on disk (an email attachment decoded in memory, a generated
45148
+ * document, a fetched URL). `uploadFiles` is this with a read in front, so
45149
+ * both routes hit one endpoint and one mime-derivation rule.
45150
+ */
45151
+ async uploadFileBytes(items) {
45152
+ const formData = new FormData();
45153
+ for (const item of items) {
45154
+ const mimeType = item.mimeType ?? getMimeType(item.filename);
45155
+ formData.append("file", new Blob([item.bytes], { type: mimeType }), item.filename);
45143
45156
  }
45144
45157
  const url2 = `${this.baseUrl}/v1/files`;
45145
45158
  const response = await fetch(url2, {
@@ -45152,6 +45165,92 @@ var LoticsClient = class {
45152
45165
  }
45153
45166
  };
45154
45167
 
45168
+ // src/upload_source.ts
45169
+ var MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
45170
+ var URL_FETCH_TIMEOUT_MS = 3e4;
45171
+ async function readStdinBytes(limit) {
45172
+ const chunks = [];
45173
+ let total = 0;
45174
+ for await (const chunk of process.stdin) {
45175
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
45176
+ total += buf.byteLength;
45177
+ if (total > limit) return { tooLarge: true };
45178
+ chunks.push(buf);
45179
+ }
45180
+ return Buffer.concat(chunks);
45181
+ }
45182
+ function decodeBase64Strict(input) {
45183
+ const text = Buffer.from(input).toString("utf8").trim();
45184
+ const canonical = text.replace(/\s+/g, "").replace(/=+$/, "");
45185
+ if (canonical.length > 0 && !/^[A-Za-z0-9+/]+$/.test(canonical)) return null;
45186
+ const decoded = Buffer.from(text, "base64");
45187
+ if (decoded.toString("base64").replace(/=+$/, "") !== canonical) return null;
45188
+ return decoded;
45189
+ }
45190
+ function filenameFromResponse(headers, url2) {
45191
+ const disposition = headers.get("content-disposition");
45192
+ const encoded = disposition?.match(/filename\*=\s*(?:UTF-8'')?"?([^";]+)"?/i);
45193
+ const plain = disposition?.match(/filename=\s*"?([^";]+)"?/i);
45194
+ const raw = encoded?.[1] ?? plain?.[1];
45195
+ if (raw) return decodeURIComponent(raw.trim());
45196
+ const last3 = new URL(url2).pathname.split("/").filter(Boolean).pop();
45197
+ return last3 && last3.includes(".") ? decodeURIComponent(last3) : void 0;
45198
+ }
45199
+ async function resolveUploadSource(sourceFlag, flags) {
45200
+ let bytes;
45201
+ let filename = flags.as;
45202
+ if (flags.url !== void 0) {
45203
+ const controller = new AbortController();
45204
+ const timer2 = setTimeout(() => controller.abort(), URL_FETCH_TIMEOUT_MS);
45205
+ let response;
45206
+ try {
45207
+ response = await fetch(flags.url, { signal: controller.signal });
45208
+ } catch (e) {
45209
+ const reason = controller.signal.aborted ? `no response within ${URL_FETCH_TIMEOUT_MS}ms` : e instanceof Error ? e.message : String(e);
45210
+ return { error: `Fetch failed for ${flags.url}: ${reason}` };
45211
+ } finally {
45212
+ clearTimeout(timer2);
45213
+ }
45214
+ if (!response.ok) {
45215
+ return { error: `Fetch failed: ${response.status} ${response.statusText} for ${flags.url}` };
45216
+ }
45217
+ const declared = Number(response.headers.get("content-length"));
45218
+ if (Number.isFinite(declared) && declared > MAX_UPLOAD_BYTES) {
45219
+ return { error: `That URL returns ${declared} bytes; the upload limit is ${MAX_UPLOAD_BYTES}.` };
45220
+ }
45221
+ bytes = new Uint8Array(await response.arrayBuffer());
45222
+ filename ??= filenameFromResponse(response.headers, flags.url);
45223
+ } else {
45224
+ const read = await readStdinBytes(MAX_UPLOAD_BYTES);
45225
+ if ("tooLarge" in read) {
45226
+ return { error: `stdin exceeded the ${MAX_UPLOAD_BYTES}-byte upload limit.` };
45227
+ }
45228
+ if (read.byteLength === 0) {
45229
+ return { error: `${sourceFlag} was given but stdin was empty \u2014 pipe the bytes in.` };
45230
+ }
45231
+ if (flags.base64) {
45232
+ const decoded = decodeBase64Strict(read);
45233
+ if (decoded === null) {
45234
+ return {
45235
+ error: "--base64 input is not valid base64: it contains characters outside the standard alphabet (base64url is not accepted) or is truncated."
45236
+ };
45237
+ }
45238
+ bytes = decoded;
45239
+ } else {
45240
+ bytes = read;
45241
+ }
45242
+ }
45243
+ if (bytes.byteLength > MAX_UPLOAD_BYTES) {
45244
+ return { error: `That is ${bytes.byteLength} bytes; the upload limit is ${MAX_UPLOAD_BYTES}.` };
45245
+ }
45246
+ if (filename === void 0) {
45247
+ return {
45248
+ error: "--as <name> is required: no filename could be derived from the source, and the mime type comes from it."
45249
+ };
45250
+ }
45251
+ return { bytes, filename };
45252
+ }
45253
+
45155
45254
  // src/telemetry.ts
45156
45255
  import crypto from "node:crypto";
45157
45256
  import fs3 from "node:fs";
@@ -45928,6 +46027,9 @@ var COMMANDS = [
45928
46027
  aliases: ["upload", "download", "preview"],
45929
46028
  help: [
45930
46029
  " lotics file upload <file|dir...> Upload files (alias: lotics upload)",
46030
+ " lotics file upload --stdin --as <name>",
46031
+ " Upload bytes you already hold, piped on stdin",
46032
+ " (--base64 for base64; --url <url> to fetch first)",
45931
46033
  " lotics file download <file_id> Download a file by ID (alias: lotics download)",
45932
46034
  " lotics file download record <record_id> <field_key>",
45933
46035
  " Download all files on a record file field",
@@ -71401,6 +71503,24 @@ var subjectSchema = zod_default.discriminatedUnion("type", [
71401
71503
  })
71402
71504
  ]);
71403
71505
  var resourceTypeSchema = zod_default.enum(["table", "table_view", "app", "automation", "skill", "document_template", "instruction", "knowledge_doc"]);
71506
+ var accessPrincipalSchema = zod_default.discriminatedUnion("type", [
71507
+ zod_default.object({
71508
+ type: zod_default.literal("member").describe("Principal type: an individual member"),
71509
+ id: zod_default.string().describe("Member ID")
71510
+ }),
71511
+ zod_default.object({
71512
+ type: zod_default.literal("member_group").describe("Principal type: a member group"),
71513
+ id: zod_default.string().describe("Group ID")
71514
+ }),
71515
+ zod_default.object({
71516
+ type: zod_default.literal("organization").describe("Principal type: everyone in the organization"),
71517
+ id: zod_default.string().describe("Organization ID")
71518
+ })
71519
+ ]);
71520
+ var resourceAccessSchema = zod_default.object({
71521
+ resource_id: zod_default.string().describe("ID of the resource these principals can reach"),
71522
+ principals: zod_default.array(accessPrincipalSchema).describe("Members, groups and the organization that have been granted access")
71523
+ });
71404
71524
  var resourceRoleBindingSchema = zod_default.object({
71405
71525
  id: zod_default.string().describe("Unique identifier for the role binding"),
71406
71526
  principal: principalSchema.describe(
@@ -71553,12 +71673,14 @@ var documentTemplateSchema = zod_default.object({
71553
71673
  id: zod_default.string().describe("Unique identifier for the document template"),
71554
71674
  workspace_id: zod_default.string().describe("ID of the workspace this template belongs to"),
71555
71675
  name: zod_default.string().describe("Display name of the template"),
71676
+ description: zod_default.string().describe("What the template produces, shown under its name in the library"),
71556
71677
  type: documentTemplateTypeSchema,
71557
71678
  template: zod_default.string().describe("Template content with variable placeholders (e.g., {{customer_name}})"),
71558
71679
  format: zod_default.string().describe("Supported document formats.").optional(),
71559
71680
  orientation: zod_default.enum(["portrait", "landscape"]).describe("Page orientation for PDF generation").optional(),
71560
71681
  variables: zod_default.record(zod_default.string(), zod_default.unknown()).describe("Data to pass to the template."),
71561
71682
  current_version: zod_default.number().describe("Current version number of the template"),
71683
+ owner_member_id: zod_default.string().nullable().optional().describe("Member who created the template, from the IAM registry. Absent on write responses."),
71562
71684
  created_at: zod_default.string().describe("Timestamp when template was created"),
71563
71685
  template_url: zod_default.string().nullable().describe("Signed URL to download the template file (pdf-form/excel only)")
71564
71686
  });
@@ -71866,11 +71988,29 @@ var contractKnowledgeEntrySchema = zod_default.object({
71866
71988
  "Display name \u2014 the runtime routing key an agent resolves the doc by; unique across the contract's knowledge"
71867
71989
  ),
71868
71990
  description: zod_default.string().describe("Doc description (may be empty)"),
71869
- // Optional so contracts published before folders still parse; absent means
71870
- // root. A curated corpus is exactly the case where organization is authored
71871
- // deliberately, so install and upgrade have to carry it — otherwise every
71872
- // installed corpus lands flat and the customer re-files it by hand.
71873
- path: zod_default.string().optional().describe("Folder the doc is filed under; omitted means root"),
71991
+ // Optional so contracts published before tagging still parse; absent means
71992
+ // untagged. A curated corpus is exactly the case where classification is
71993
+ // authored deliberately, so install and upgrade have to carry it — otherwise
71994
+ // every installed corpus lands unlabelled and the customer re-tags it by hand.
71995
+ //
71996
+ // ABSENT IS NOT EMPTY, and that distinction is load-bearing on the wire: a
71997
+ // contract that says nothing about tags must leave the customer's own labels
71998
+ // alone, or a routine content upgrade silently strips classification the
71999
+ // workspace added. Every reader below branches on `undefined`, never on length.
72000
+ tags: zod_default.array(zod_default.string()).optional().describe("Labels for the doc; omitted leaves existing labels alone"),
72001
+ /**
72002
+ * LEGACY, read-only, removed in a later release.
72003
+ *
72004
+ * Content packages published while knowledge docs had folders carry a `path`,
72005
+ * and those contracts are stored verbatim in the workspaces that installed
72006
+ * them. Dropping the field would make an installed contract fail to parse and
72007
+ * would discard what the publisher declared, so it is still accepted and
72008
+ * mapped to `tags` on install/upgrade (a path's segments were always labels;
72009
+ * the hierarchy merely insisted on an order for them). Nothing EMITS it any
72010
+ * more — `content_bundle` writes `tags` — so it disappears from a package the
72011
+ * first time it is re-published.
72012
+ */
72013
+ path: zod_default.string().optional().describe("Deprecated: pre-tagging folder. Mapped to tags on install."),
71874
72014
  content_ref: zod_default.string().min(1).describe("Path of the content file within the bundle \u2014 must be `knowledge/<alias>.md`"),
71875
72015
  content_sha256: zod_default.string().describe("sha256 (64-char lowercase hex) of the content bytes \u2014 the modified-detection reference")
71876
72016
  });
@@ -72832,6 +72972,18 @@ function appDirName(name2) {
72832
72972
  const cleaned = name2.replace(/[/\\]+/g, "-").replace(/[<>:"|?*]/g, "").replace(/\s+/g, " ").trim().replace(/^[.\s-]+|[.\s-]+$/g, "");
72833
72973
  return cleaned || "app";
72834
72974
  }
72975
+ async function materializeScaffold(targetPath, files) {
72976
+ for (const file2 of files) {
72977
+ const fullPath = path9.join(targetPath, file2.path);
72978
+ fs8.mkdirSync(path9.dirname(fullPath), { recursive: true });
72979
+ fs8.writeFileSync(fullPath, file2.content);
72980
+ }
72981
+ console.error(`Scaffolded ${files.length} files into ${targetPath}`);
72982
+ writeAppDts(targetPath, {});
72983
+ console.error("Installing npm dependencies...");
72984
+ await runNpm(["install"], targetPath);
72985
+ writeAppDtsKitLinks(targetPath);
72986
+ }
72835
72987
  async function appCreate(client, args) {
72836
72988
  const targetPath = path9.resolve(args.targetPath ?? appDirName(args.name));
72837
72989
  if (fs8.existsSync(targetPath)) {
@@ -72854,16 +73006,7 @@ async function appCreate(client, args) {
72854
73006
  ui_version: `^${uiLatest}`,
72855
73007
  sdk_version: `^${sdkLatest}`
72856
73008
  });
72857
- for (const file2 of files) {
72858
- const fullPath = path9.join(targetPath, file2.path);
72859
- fs8.mkdirSync(path9.dirname(fullPath), { recursive: true });
72860
- fs8.writeFileSync(fullPath, file2.content);
72861
- }
72862
- console.error(`Scaffolded ${files.length} files into ${targetPath}`);
72863
- writeAppDts(targetPath, {});
72864
- console.error("Installing npm dependencies...");
72865
- await runNpm(["install"], targetPath);
72866
- writeAppDtsKitLinks(targetPath);
73009
+ await materializeScaffold(targetPath, files);
72867
73010
  console.error("Building initial version...");
72868
73011
  await appDeploy(client, {
72869
73012
  projectDir: targetPath,
@@ -74352,6 +74495,9 @@ function parseArgs(argv) {
74352
74495
  const flags = {
74353
74496
  json: false,
74354
74497
  force: false,
74498
+ stdin: false,
74499
+ base64: false,
74500
+ url: void 0,
74355
74501
  fromVersion: void 0,
74356
74502
  timeout: void 0,
74357
74503
  output: void 0,
@@ -74389,6 +74535,15 @@ function parseArgs(argv) {
74389
74535
  case "--force":
74390
74536
  flags.force = true;
74391
74537
  break;
74538
+ case "--stdin":
74539
+ flags.stdin = true;
74540
+ break;
74541
+ case "--base64":
74542
+ flags.base64 = true;
74543
+ break;
74544
+ case "--url":
74545
+ flags.url = argv[++i2];
74546
+ break;
74392
74547
  case "--from-version":
74393
74548
  flags.fromVersion = argv[++i2];
74394
74549
  break;
@@ -102750,7 +102905,10 @@ FLAGS
102750
102905
  --json Full JSON output (default is human-readable text)
102751
102906
  --timeout <ms> Timeout for tool execution (default: 60000)
102752
102907
  -o <path> Output dir for downloads
102753
- --as <name> Override upload filename
102908
+ --as <name> Override upload filename (required with --stdin/--base64)
102909
+ --stdin Upload raw bytes piped on stdin instead of a path
102910
+ --base64 Upload base64 piped on stdin (decoded strictly)
102911
+ --url <url> Fetch a URL and upload what it returns
102754
102912
  --api-key <key> One-off API key (overrides saved config + env)
102755
102913
  --workspace <id> One-off workspace override (alias: -w)
102756
102914
  --view-as <id> Admin "View as": run every request as this member, so
@@ -103353,8 +103511,12 @@ async function main() {
103353
103511
  console.error('Run "lotics tools" to see available tools.');
103354
103512
  process.exit(1);
103355
103513
  }
103356
- if (command === "upload" && !subcommand) {
103357
- console.error("Usage: lotics upload <file|dir...> [--as <name>]");
103514
+ if (command === "upload" && !subcommand && !flags.stdin && !flags.base64 && !flags.url) {
103515
+ console.error("Usage:");
103516
+ console.error(" lotics upload <file|dir...> [--as <name>]");
103517
+ console.error(" lotics upload --stdin --as <name> Raw bytes on stdin");
103518
+ console.error(" lotics upload --base64 --as <name> Base64 on stdin");
103519
+ console.error(" lotics upload --url <url> [--as <name>]");
103358
103520
  console.error("Uploads files and returns their file_ids. Directories expand to their immediate files.");
103359
103521
  process.exit(1);
103360
103522
  }
@@ -103758,19 +103920,39 @@ ${JSON.stringify(info.input_schema, null, 2)}`);
103758
103920
  return;
103759
103921
  }
103760
103922
  if (command === "upload") {
103761
- const rawPaths = [subcommand, ...toolArgs ? [toolArgs] : [], ...restArgs];
103762
- const filePaths = resolveUploadPaths(rawPaths);
103763
- if (filePaths.length === 0) {
103764
- console.error("No files found in the specified paths.");
103923
+ const sources = [flags.stdin && "--stdin", flags.base64 && "--base64", flags.url && "--url"].filter(Boolean);
103924
+ if (sources.length > 1) {
103925
+ console.error(`Pick one source: ${sources.join(", ")} were all given.`);
103765
103926
  process.exit(1);
103766
103927
  }
103767
- if (flags.as && filePaths.length > 1) {
103768
- console.error("Cannot use --as with multiple files.");
103769
- process.exit(1);
103928
+ let upload;
103929
+ if (sources.length === 1) {
103930
+ const positional = [subcommand, toolArgs, ...restArgs].filter((v) => v !== void 0);
103931
+ if (positional.length > 0) {
103932
+ console.error(`${sources[0]} reads the bytes itself \u2014 drop the path argument (${positional.join(", ")}).`);
103933
+ process.exit(1);
103934
+ }
103935
+ const resolved = await resolveUploadSource(String(sources[0]), flags);
103936
+ if ("error" in resolved) {
103937
+ console.error(resolved.error);
103938
+ process.exit(1);
103939
+ }
103940
+ upload = await client.uploadFileBytes([resolved]);
103941
+ } else {
103942
+ const rawPaths = [subcommand, ...toolArgs ? [toolArgs] : [], ...restArgs];
103943
+ const filePaths = resolveUploadPaths(rawPaths);
103944
+ if (filePaths.length === 0) {
103945
+ console.error("No files found in the specified paths.");
103946
+ process.exit(1);
103947
+ }
103948
+ if (flags.as && filePaths.length > 1) {
103949
+ console.error("Cannot use --as with multiple files.");
103950
+ process.exit(1);
103951
+ }
103952
+ upload = await client.uploadFiles(filePaths, {
103953
+ filenames: flags.as ? [flags.as] : void 0
103954
+ });
103770
103955
  }
103771
- const upload = await client.uploadFiles(filePaths, {
103772
- filenames: flags.as ? [flags.as] : void 0
103773
- });
103774
103956
  if (upload.files.length === 0 && upload.errors.length > 0) {
103775
103957
  console.error(`Upload failed: ${upload.errors.map((e) => `${e.filename}: ${e.error}`).join(", ")}`);
103776
103958
  process.exit(1);
@@ -1112,4 +1112,15 @@ export declare class LoticsClient {
1112
1112
  uploadFiles(filePaths: string[], options?: {
1113
1113
  filenames?: string[];
1114
1114
  }): Promise<FileUploadResult>;
1115
+ /**
1116
+ * Store files from bytes the caller already holds — the path for a caller that
1117
+ * never had them on disk (an email attachment decoded in memory, a generated
1118
+ * document, a fetched URL). `uploadFiles` is this with a read in front, so
1119
+ * both routes hit one endpoint and one mime-derivation rule.
1120
+ */
1121
+ uploadFileBytes(items: {
1122
+ bytes: Uint8Array<ArrayBuffer>;
1123
+ filename: string;
1124
+ mimeType?: string;
1125
+ }[]): Promise<FileUploadResult>;
1115
1126
  }
@@ -846,14 +846,40 @@ export class LoticsClient {
846
846
  }));
847
847
  }
848
848
  async uploadFiles(filePaths, options) {
849
- const formData = new FormData();
849
+ // Sequential, not `Promise.all`: `lotics upload <dir>` expands a directory to
850
+ // every file in it, and reading them all at once opens one handle per file —
851
+ // EMFILE on a large directory. The upload itself is a single request either
852
+ // way, so concurrency here buys nothing.
853
+ const items = [];
850
854
  for (let i = 0; i < filePaths.length; i++) {
851
855
  const absolutePath = path.resolve(filePaths[i]);
852
- const buffer = await fs.promises.readFile(absolutePath);
853
- const filename = options?.filenames?.[i] ?? path.basename(absolutePath);
854
- const mimeType = getMimeType(filename);
855
- const blob = new Blob([buffer], { type: mimeType });
856
- formData.append("file", blob, filename);
856
+ items.push({
857
+ bytes: await fs.promises.readFile(absolutePath),
858
+ filename: options?.filenames?.[i] ?? path.basename(absolutePath),
859
+ });
860
+ }
861
+ return this.uploadFileBytes(items);
862
+ }
863
+ /**
864
+ * Store files from bytes the caller already holds — the path for a caller that
865
+ * never had them on disk (an email attachment decoded in memory, a generated
866
+ * document, a fetched URL). `uploadFiles` is this with a read in front, so
867
+ * both routes hit one endpoint and one mime-derivation rule.
868
+ */
869
+ async uploadFileBytes(
870
+ // `Uint8Array<ArrayBuffer>`, not a bare `Uint8Array`: `Blob` takes a
871
+ // `BufferSource`, which excludes a `SharedArrayBuffer`-backed view. Every
872
+ // real source here (`readFile`, `Buffer.concat`, `response.arrayBuffer()`)
873
+ // is already ArrayBuffer-backed, so the narrower type states the
874
+ // requirement rather than casting it away at the call site.
875
+ items) {
876
+ const formData = new FormData();
877
+ for (const item of items) {
878
+ const mimeType = item.mimeType ?? getMimeType(item.filename);
879
+ // `Blob` copies a BufferSource by the VIEW's offset+length, so passing a
880
+ // pooled `Buffer` from `readFile` straight in is correct — no defensive
881
+ // re-copy, which would double peak memory on every upload.
882
+ formData.append("file", new Blob([item.bytes], { type: mimeType }), item.filename);
857
883
  }
858
884
  const url = `${this.baseUrl}/v1/files`;
859
885
  const response = await fetch(url, {
@@ -22,7 +22,7 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
22
22
  | `lotics tools <name>` | Full description + JSON Schema for one tool |
23
23
  | `lotics run <tool> '<json>'` | Execute a tool (text output via toModelOutput). Args may also come from a file (`lotics run <tool> @args.json`) or piped stdin (`cat args.json \| lotics run <tool>`) — both bypass the OS `ARG_MAX` limit for large payloads (a knowledge-doc `content`, a bulk update). A leading `@` on the args is unambiguously a file path (JSON args start with `{`). |
24
24
  | `lotics run <tool> --json '<json>'` | Execute a tool (full JSON output) |
25
- | `lotics upload <file\|dir...>` | Upload files/directories via multipart POST to /v1/files |
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
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
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. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/cli",
3
- "version": "0.150.1",
3
+ "version": "0.151.0",
4
4
  "description": "Lotics SDK and CLI for AI agents",
5
5
  "type": "module",
6
6
  "bin": {