@dayofweek/dcli 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -85,6 +85,32 @@ https://field.dayofweek.com/brain/<areaId>/source/<sourceId>
85
85
 
86
86
  Unknown hosts, user-info, ports, query strings, fragments, encoded path separators, malformed identifiers, and oversized input are rejected before any API request.
87
87
 
88
+ ## Entity knowledge
89
+
90
+ Knowledge documents attached to an entity — read them, add to them, mirror them
91
+ out into another knowledge base.
92
+
93
+ ```bash
94
+ # Read
95
+ dcli knowledge list --entity <entityId> --json
96
+ dcli knowledge list --entity <entityId> --full --json # whole content, not excerpts
97
+ dcli knowledge get <documentId> --json
98
+ dcli knowledge search "cold chain" --entity <entityId> --json
99
+
100
+ # Add a markdown note — submitted for human review
101
+ dcli knowledge add --entity <entityId> --title "Variety catalogue" --file note.md
102
+
103
+ # Mirror an entity's sources to disk as markdown with provenance front-matter
104
+ dcli knowledge export --entity <entityId> --out ./knowledge
105
+ ```
106
+
107
+ `add` submits a proposal by default: a person approves it before it lands. If
108
+ your token has admin rights you can pass `--direct` to skip review, and
109
+ `dcli knowledge attach --file article.pdf` to upload a binary source. Both write
110
+ immediately, so treat them as something you do when a person has asked for that
111
+ specific document — not as the normal path. The server rejects them for
112
+ non-admin tokens.
113
+
88
114
  ## Legacy platform commands
89
115
 
90
116
  Existing read/proposal workflows remain compatible:
package/dist/bin/dcli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
- import { ApiError, DayOfWeekClient } from "../client.js";
3
+ import { ApiError, DayOfWeekClient, toArrayBuffer } from "../client.js";
4
4
  import { getToken, getApiUrl, saveConfig, loadConfig, saveCredential, deleteCredential } from "../config.js";
5
5
  import { browserLogin } from "../auth/login.js";
6
6
  import { parseBrainResource } from "../uri.js";
7
- import { accessSync, constants, readFileSync, existsSync, statSync } from "node:fs";
7
+ import { accessSync, constants, mkdirSync, readFileSync, existsSync, statSync, writeFileSync } from "node:fs";
8
8
  import { join, basename, resolve, relative, sep } from "node:path";
9
9
  import { homedir } from "node:os";
10
10
  import { createInterface } from "node:readline/promises";
@@ -462,6 +462,191 @@ agent
462
462
  const result = await client.getProposal(proposalId);
463
463
  output(result);
464
464
  });
465
+ // ── Knowledge Commands ───────────────────────────────────────────────────────
466
+ const MIME_BY_EXT = {
467
+ ".pdf": "application/pdf",
468
+ ".md": "text/markdown",
469
+ ".txt": "text/plain",
470
+ ".csv": "text/csv",
471
+ ".json": "application/json",
472
+ ".html": "text/html",
473
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
474
+ ".doc": "application/msword",
475
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
476
+ ".xls": "application/vnd.ms-excel",
477
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
478
+ };
479
+ function guessMimeType(path) {
480
+ const dot = path.lastIndexOf(".");
481
+ const ext = dot === -1 ? "" : path.slice(dot).toLowerCase();
482
+ return MIME_BY_EXT[ext] ?? "application/octet-stream";
483
+ }
484
+ /** Filesystem-safe name for an exported document, keeping it recognisable. */
485
+ function exportFileName(doc) {
486
+ const base = (doc.fileName || doc.title || doc.id || "document").trim();
487
+ const stripped = base.replace(/\.(md|txt)$/i, "");
488
+ const safe = stripped.replace(/[/\\:*?"<>|]+/g, "-").replace(/\s+/g, " ").slice(0, 120);
489
+ return `${safe || doc.id || "document"}.md`;
490
+ }
491
+ const knowledge = program
492
+ .command("knowledge")
493
+ .description("Read, add and export entity knowledge documents");
494
+ knowledge
495
+ .command("list")
496
+ .description("List knowledge documents on an entity")
497
+ .requiredOption("--entity <entityId>", "Target entity id")
498
+ .option("--full", "Include each document's full content, not an excerpt")
499
+ .option("--org <org>", "Organization (admin only)")
500
+ .action(async (opts) => {
501
+ const client = getClient();
502
+ output(await client.listKnowledge({ entity: opts.entity, full: opts.full, org: opts.org }));
503
+ });
504
+ knowledge
505
+ .command("get <documentId>")
506
+ .description("Show one knowledge document with full content")
507
+ .option("--org <org>", "Organization (admin only)")
508
+ .action(async (documentId, opts) => {
509
+ const client = getClient();
510
+ output(await client.getKnowledge(documentId, opts.org));
511
+ });
512
+ knowledge
513
+ .command("search <query>")
514
+ .description("Semantic search across knowledge documents")
515
+ .option("--entity <entityId>", "Scope to one entity's org tree")
516
+ .option("--types <types>", "Comma-separated sourceType filter")
517
+ .option("--limit <count>", "Max hits", parseInt)
518
+ .option("--all-orgs", "Search every org (admin only)")
519
+ .option("--org <org>", "Organization (admin only)")
520
+ .action(async (query, opts) => {
521
+ const client = getClient();
522
+ output(await client.searchKnowledge({
523
+ query,
524
+ entity: opts.entity,
525
+ types: opts.types
526
+ ? String(opts.types).split(",").map((t) => t.trim()).filter(Boolean)
527
+ : undefined,
528
+ limit: opts.limit,
529
+ allOrgs: opts.allOrgs,
530
+ org: opts.org,
531
+ }));
532
+ });
533
+ knowledge
534
+ .command("add")
535
+ .description("Add a markdown knowledge note (proposal by default)")
536
+ .requiredOption("--entity <entityId>", "Target entity id")
537
+ .requiredOption("--title <title>", "Document title")
538
+ .option("--file <path>", "Markdown file to read (- for stdin)")
539
+ .option("--content <text>", "Inline content instead of --file")
540
+ .option("--source-type <type>", "research | website | competitive_intel | …", "research")
541
+ .option("--source-url <url>", "Where the content came from")
542
+ .option("--source-description <text>", "Short provenance note")
543
+ .option("--confidence <n>", "0-1, helps reviewers prioritize", parseFloat)
544
+ .option("--source-agent <name>", "Attribution for the submitting agent")
545
+ .option("--direct", "Write immediately instead of proposing. Admin only — ask the operator first")
546
+ .option("--org <org>", "Organization (admin only)")
547
+ .action(async (opts) => {
548
+ let content;
549
+ if (opts.content) {
550
+ content = String(opts.content);
551
+ }
552
+ else if (opts.file) {
553
+ content = opts.file === "-" ? readFileSync(0, "utf8") : readFileSync(opts.file, "utf8");
554
+ }
555
+ else {
556
+ throw new Error("Provide --file or --content");
557
+ }
558
+ if (!content.trim())
559
+ throw new Error("Content is empty");
560
+ const client = getClient();
561
+ output(await client.addKnowledge({
562
+ entityId: opts.entity,
563
+ title: opts.title,
564
+ content,
565
+ sourceType: opts.sourceType,
566
+ sourceUrl: opts.sourceUrl,
567
+ sourceDescription: opts.sourceDescription,
568
+ confidence: opts.confidence,
569
+ sourceAgent: opts.sourceAgent,
570
+ direct: Boolean(opts.direct),
571
+ org: opts.org,
572
+ }));
573
+ });
574
+ knowledge
575
+ .command("attach")
576
+ .description("Attach a file (PDF, DOCX, XLSX …) as a source. Admin only")
577
+ .requiredOption("--entity <entityId>", "Target entity id")
578
+ .requiredOption("--file <path>", "File to upload")
579
+ .option("--name <fileName>", "Stored file name (defaults to the file's own)")
580
+ .option("--mime <mimeType>", "Content type (guessed from the extension)")
581
+ .option("--source-type <type>", "research | website | contract | …", "other")
582
+ .option("--source-url <url>", "Where the file came from")
583
+ .option("--source-description <text>", "Short provenance note")
584
+ .option("--org <org>", "Organization (admin only)")
585
+ .action(async (opts) => {
586
+ if (!existsSync(opts.file))
587
+ throw new Error(`File not found: ${opts.file}`);
588
+ const data = readFileSync(opts.file);
589
+ if (data.byteLength === 0)
590
+ throw new Error("File is empty");
591
+ const client = getClient();
592
+ output(await client.attachKnowledgeFile({
593
+ entityId: opts.entity,
594
+ data: toArrayBuffer(data),
595
+ fileName: opts.name ?? basename(opts.file),
596
+ mimeType: opts.mime ?? guessMimeType(opts.file),
597
+ sourceType: opts.sourceType,
598
+ sourceUrl: opts.sourceUrl,
599
+ sourceDescription: opts.sourceDescription,
600
+ org: opts.org,
601
+ }));
602
+ });
603
+ knowledge
604
+ .command("export")
605
+ .description("Write an entity's knowledge documents to disk as markdown")
606
+ .requiredOption("--entity <entityId>", "Source entity id")
607
+ .requiredOption("--out <dir>", "Directory to write into (created if missing)")
608
+ .option("--org <org>", "Organization (admin only)")
609
+ .action(async (opts) => {
610
+ const client = getClient();
611
+ const docs = await client.listKnowledge({ entity: opts.entity, full: true, org: opts.org });
612
+ mkdirSync(opts.out, { recursive: true });
613
+ const written = [];
614
+ const skipped = [];
615
+ for (const doc of docs) {
616
+ const body = typeof doc.content === "string" ? doc.content : "";
617
+ if (!body) {
618
+ // A binary source whose text extraction hasn't finished has nothing to
619
+ // mirror yet. Report it rather than writing an empty file.
620
+ skipped.push({
621
+ id: doc.id,
622
+ title: doc.title,
623
+ reason: doc.processingStatus && doc.processingStatus !== "completed"
624
+ ? `processingStatus=${doc.processingStatus}`
625
+ : "no extracted content",
626
+ });
627
+ continue;
628
+ }
629
+ const frontMatter = [
630
+ "---",
631
+ `document_id: ${doc.id}`,
632
+ `title: ${JSON.stringify(doc.title ?? "")}`,
633
+ `source_type: ${doc.sourceType ?? "other"}`,
634
+ ...(doc.sourceUrl ? [`source_url: ${doc.sourceUrl}`] : []),
635
+ ...(doc.sourceDescription
636
+ ? [`source_description: ${JSON.stringify(doc.sourceDescription)}`]
637
+ : []),
638
+ `entity_id: ${opts.entity}`,
639
+ ...(doc.createdAt ? [`created_at: ${new Date(doc.createdAt).toISOString()}`] : []),
640
+ "exported_from: dayofweek-platform",
641
+ "---",
642
+ "",
643
+ ].join("\n");
644
+ const target = join(opts.out, exportFileName(doc));
645
+ writeFileSync(target, `${frontMatter}${body}\n`, "utf8");
646
+ written.push({ id: doc.id, file: target, bytes: Buffer.byteLength(body, "utf8") });
647
+ }
648
+ output({ entity: opts.entity, out: opts.out, total: docs.length, written, skipped });
649
+ });
465
650
  // ── Skill Commands ───────────────────────────────────────────────────────────
466
651
  const skill = program.command("skill").description("Manage the Day of Week agent skill");
467
652
  function resolveTargetDirs(target, bundleName, customDir) {
package/dist/client.d.ts CHANGED
@@ -2,6 +2,11 @@
2
2
  * HTTP client for the Day of Week platform REST API.
3
3
  * All calls go through the proxy at field.dayofweek.com/app/api/dcli.
4
4
  */
5
+ /**
6
+ * Node's Buffer is a Uint8Array view over a possibly-larger, possibly-shared
7
+ * backing store, which is not assignable to BodyInit. Copy out the exact bytes.
8
+ */
9
+ export declare function toArrayBuffer(view: Uint8Array): ArrayBuffer;
5
10
  export declare class ApiError extends Error {
6
11
  status: number;
7
12
  code?: string | undefined;
@@ -154,6 +159,64 @@ export declare class DayOfWeekClient {
154
159
  truncated: boolean;
155
160
  total: number;
156
161
  }>;
162
+ /**
163
+ * List the knowledge documents attached to an entity. `full` returns each
164
+ * document's whole content instead of an excerpt, which is what you want when
165
+ * mirroring an entity's sources into an external knowledge base.
166
+ */
167
+ listKnowledge(opts: {
168
+ entity: string;
169
+ full?: boolean;
170
+ org?: string;
171
+ }): Promise<any[]>;
172
+ getKnowledge(documentId: string, org?: string): Promise<any>;
173
+ /** Semantic search across knowledge documents. */
174
+ searchKnowledge(opts: {
175
+ query: string;
176
+ entity?: string;
177
+ types?: string[];
178
+ limit?: number;
179
+ allOrgs?: boolean;
180
+ org?: string;
181
+ }): Promise<any>;
182
+ /**
183
+ * Add a markdown knowledge note.
184
+ *
185
+ * Without `direct` this submits a proposal for human review — the default,
186
+ * and the only option non-admin tokens have. With `direct: true` an admin
187
+ * token writes the document immediately, skipping review. Ask the operator
188
+ * before doing that; see references/admin.md in the served skill.
189
+ */
190
+ addKnowledge(input: {
191
+ entityId: string;
192
+ title: string;
193
+ content: string;
194
+ sourceType?: string;
195
+ sourceUrl?: string;
196
+ sourceDescription?: string;
197
+ confidence?: number;
198
+ sourceAgent?: string;
199
+ direct?: boolean;
200
+ org?: string;
201
+ }): Promise<any>;
202
+ /**
203
+ * Attach a binary source (PDF, DOCX, XLSX, …) to an entity. Admin only.
204
+ *
205
+ * Three hops: ask for an upload URL, send the bytes straight to Convex
206
+ * storage, then register the resulting storageId. The bytes never pass
207
+ * through function arguments, so file size isn't bounded by an arg limit.
208
+ */
209
+ attachKnowledgeFile(input: {
210
+ entityId: string;
211
+ /** Raw file bytes. Buffer callers: pass `toArrayBuffer(buf)` below. */
212
+ data: ArrayBuffer;
213
+ fileName: string;
214
+ mimeType: string;
215
+ sourceType?: string;
216
+ sourceUrl?: string;
217
+ sourceDescription?: string;
218
+ org?: string;
219
+ }): Promise<any>;
157
220
  getSkillBundle(name?: string): Promise<{
158
221
  name: string;
159
222
  version: string;
@@ -172,6 +235,7 @@ export declare class DayOfWeekClient {
172
235
  getSchema(): Promise<any>;
173
236
  private get;
174
237
  private post;
238
+ private put;
175
239
  private patch;
176
240
  private delete;
177
241
  private request;
package/dist/client.js CHANGED
@@ -8,6 +8,15 @@ import { basename, dirname, join } from "node:path";
8
8
  import { Readable, Transform } from "node:stream";
9
9
  import { pipeline } from "node:stream/promises";
10
10
  const DEFAULT_BASE_URL = "https://field.dayofweek.com/app/api/dcli";
11
+ /**
12
+ * Node's Buffer is a Uint8Array view over a possibly-larger, possibly-shared
13
+ * backing store, which is not assignable to BodyInit. Copy out the exact bytes.
14
+ */
15
+ export function toArrayBuffer(view) {
16
+ const out = new ArrayBuffer(view.byteLength);
17
+ new Uint8Array(out).set(view);
18
+ return out;
19
+ }
11
20
  export class ApiError extends Error {
12
21
  status;
13
22
  code;
@@ -279,6 +288,88 @@ export class DayOfWeekClient {
279
288
  const qs = params.toString();
280
289
  return this.get(`/admin/proposals${qs ? `?${qs}` : ""}`);
281
290
  }
291
+ // ── Knowledge ─────────────────────────────────────────────────────────────
292
+ /**
293
+ * List the knowledge documents attached to an entity. `full` returns each
294
+ * document's whole content instead of an excerpt, which is what you want when
295
+ * mirroring an entity's sources into an external knowledge base.
296
+ */
297
+ async listKnowledge(opts) {
298
+ const params = new URLSearchParams({ entity: opts.entity });
299
+ if (opts.full)
300
+ params.set("full", "1");
301
+ if (opts.org)
302
+ params.set("org", opts.org);
303
+ return this.get(`/knowledge?${params.toString()}`);
304
+ }
305
+ async getKnowledge(documentId, org) {
306
+ const params = new URLSearchParams({ document: documentId });
307
+ if (org)
308
+ params.set("org", org);
309
+ return this.get(`/knowledge?${params.toString()}`);
310
+ }
311
+ /** Semantic search across knowledge documents. */
312
+ async searchKnowledge(opts) {
313
+ const params = new URLSearchParams({ q: opts.query });
314
+ if (opts.entity)
315
+ params.set("entity", opts.entity);
316
+ if (opts.types?.length)
317
+ params.set("types", opts.types.join(","));
318
+ if (opts.limit)
319
+ params.set("limit", String(opts.limit));
320
+ if (opts.allOrgs)
321
+ params.set("allOrgs", "1");
322
+ if (opts.org)
323
+ params.set("org", opts.org);
324
+ return this.get(`/knowledge?${params.toString()}`);
325
+ }
326
+ /**
327
+ * Add a markdown knowledge note.
328
+ *
329
+ * Without `direct` this submits a proposal for human review — the default,
330
+ * and the only option non-admin tokens have. With `direct: true` an admin
331
+ * token writes the document immediately, skipping review. Ask the operator
332
+ * before doing that; see references/admin.md in the served skill.
333
+ */
334
+ async addKnowledge(input) {
335
+ return this.post("/knowledge", input);
336
+ }
337
+ /**
338
+ * Attach a binary source (PDF, DOCX, XLSX, …) to an entity. Admin only.
339
+ *
340
+ * Three hops: ask for an upload URL, send the bytes straight to Convex
341
+ * storage, then register the resulting storageId. The bytes never pass
342
+ * through function arguments, so file size isn't bounded by an arg limit.
343
+ */
344
+ async attachKnowledgeFile(input) {
345
+ const { uploadUrl } = await this.post("/knowledge/file", {
346
+ entityId: input.entityId,
347
+ org: input.org,
348
+ });
349
+ const byteSize = input.data.byteLength;
350
+ const uploadRes = await fetch(uploadUrl, {
351
+ method: "POST",
352
+ headers: { "Content-Type": input.mimeType },
353
+ body: input.data,
354
+ });
355
+ if (!uploadRes.ok) {
356
+ throw new ApiError(uploadRes.status, `Upload to storage failed: ${uploadRes.status} ${uploadRes.statusText}`);
357
+ }
358
+ const uploaded = (await uploadRes.json());
359
+ if (!uploaded.storageId)
360
+ throw new Error("Storage upload returned no storageId");
361
+ return this.put("/knowledge/file", {
362
+ entityId: input.entityId,
363
+ org: input.org,
364
+ storageId: uploaded.storageId,
365
+ fileName: input.fileName,
366
+ mimeType: input.mimeType,
367
+ byteSize,
368
+ sourceType: input.sourceType,
369
+ sourceUrl: input.sourceUrl,
370
+ sourceDescription: input.sourceDescription,
371
+ });
372
+ }
282
373
  // ── Skill ─────────────────────────────────────────────────────────────────
283
374
  async getSkillBundle(name) {
284
375
  return this.get(`/skill${name ? `?name=${encodeURIComponent(name)}` : ""}`);
@@ -297,6 +388,9 @@ export class DayOfWeekClient {
297
388
  async post(path, body) {
298
389
  return this.request(path, { method: "POST", body: JSON.stringify(body) });
299
390
  }
391
+ async put(path, body) {
392
+ return this.request(path, { method: "PUT", body: JSON.stringify(body) });
393
+ }
300
394
  async patch(path, body) {
301
395
  return this.request(path, { method: "PATCH", body: JSON.stringify(body) });
302
396
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dayofweek/dcli",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "CLI for the Day of Week AgTech platform — read data and submit proposals for review",
5
5
  "license": "MIT",
6
6
  "type": "module",