@dayofweek/dcli 1.4.0 → 1.6.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 +96 -1
- package/dist/bin/dcli.js +537 -6
- package/dist/bundle/dcli.cjs +5991 -0
- package/dist/client.d.ts +167 -2
- package/dist/client.js +233 -4
- package/package.json +5 -3
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";
|
|
@@ -240,6 +240,13 @@ brain
|
|
|
240
240
|
output(await getClient().listBrainAudit(opts.area, { cursor: opts.cursor, limit: opts.limit }));
|
|
241
241
|
});
|
|
242
242
|
const brainSource = brain.command("source").description("Work with original shared files and recordings");
|
|
243
|
+
brainSource
|
|
244
|
+
.command("list")
|
|
245
|
+
.description("List an area's sources, newest first (metadata only)")
|
|
246
|
+
.requiredOption("--area <areaId>", "Area to inventory")
|
|
247
|
+
.action(async (opts) => {
|
|
248
|
+
output(await getClient().listBrainSources(opts.area));
|
|
249
|
+
});
|
|
243
250
|
brainSource
|
|
244
251
|
.command("get <uri>")
|
|
245
252
|
.description("Read source metadata and derived text")
|
|
@@ -325,6 +332,7 @@ read
|
|
|
325
332
|
.option("--type <entityType>", "Filter by type (Farm, Producer, Restaurant, ...)")
|
|
326
333
|
.option("--parent <entityId>", "List children of an entity")
|
|
327
334
|
.option("--limit <count>", "Max results", parseInt)
|
|
335
|
+
.option("--org <org>", "Organization slug or id (admin only)")
|
|
328
336
|
.action(async (opts) => {
|
|
329
337
|
const client = getClient();
|
|
330
338
|
const result = await client.listEntities(opts);
|
|
@@ -333,9 +341,10 @@ read
|
|
|
333
341
|
read
|
|
334
342
|
.command("entity <entityId>")
|
|
335
343
|
.description("Get entity details")
|
|
336
|
-
.
|
|
344
|
+
.option("--org <org>", "Organization slug or id (admin only)")
|
|
345
|
+
.action(async (entityId, opts) => {
|
|
337
346
|
const client = getClient();
|
|
338
|
-
const result = await client.getEntity(entityId);
|
|
347
|
+
const result = await client.getEntity(entityId, opts.org);
|
|
339
348
|
output(result);
|
|
340
349
|
});
|
|
341
350
|
read
|
|
@@ -343,6 +352,7 @@ read
|
|
|
343
352
|
.description("List produce profiles")
|
|
344
353
|
.option("--entity <entityId>", "Filter by entity")
|
|
345
354
|
.option("--limit <count>", "Max results", parseInt)
|
|
355
|
+
.option("--org <org>", "Organization slug or id (admin only)")
|
|
346
356
|
.action(async (opts) => {
|
|
347
357
|
const client = getClient();
|
|
348
358
|
const result = await client.listProduce(opts);
|
|
@@ -353,6 +363,7 @@ read
|
|
|
353
363
|
.description("List contacts and memberships")
|
|
354
364
|
.option("--entity <entityId>", "Filter by entity")
|
|
355
365
|
.option("--limit <count>", "Max results", parseInt)
|
|
366
|
+
.option("--org <org>", "Organization slug or id (admin only)")
|
|
356
367
|
.action(async (opts) => {
|
|
357
368
|
const client = getClient();
|
|
358
369
|
const result = await client.listContacts(opts);
|
|
@@ -361,9 +372,10 @@ read
|
|
|
361
372
|
read
|
|
362
373
|
.command("entity-types")
|
|
363
374
|
.description("List available entity types")
|
|
364
|
-
.
|
|
375
|
+
.option("--org <org>", "Organization slug or id (admin only)")
|
|
376
|
+
.action(async (opts) => {
|
|
365
377
|
const client = getClient();
|
|
366
|
-
const result = await client.listEntityTypes();
|
|
378
|
+
const result = await client.listEntityTypes(opts.org);
|
|
367
379
|
output(result);
|
|
368
380
|
});
|
|
369
381
|
read
|
|
@@ -374,6 +386,7 @@ read
|
|
|
374
386
|
.option("--type <nodeType>", "Filter by type (category, produce, variety)")
|
|
375
387
|
.option("--include-categories", "Include non-selectable categories in results")
|
|
376
388
|
.option("--limit <count>", "Max results", parseInt)
|
|
389
|
+
.option("--org <org>", "Organization slug or id (admin only)")
|
|
377
390
|
.action(async (opts) => {
|
|
378
391
|
const client = getClient();
|
|
379
392
|
const result = await client.searchCatalog(opts);
|
|
@@ -462,6 +475,191 @@ agent
|
|
|
462
475
|
const result = await client.getProposal(proposalId);
|
|
463
476
|
output(result);
|
|
464
477
|
});
|
|
478
|
+
// ── Knowledge Commands ───────────────────────────────────────────────────────
|
|
479
|
+
const MIME_BY_EXT = {
|
|
480
|
+
".pdf": "application/pdf",
|
|
481
|
+
".md": "text/markdown",
|
|
482
|
+
".txt": "text/plain",
|
|
483
|
+
".csv": "text/csv",
|
|
484
|
+
".json": "application/json",
|
|
485
|
+
".html": "text/html",
|
|
486
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
487
|
+
".doc": "application/msword",
|
|
488
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
489
|
+
".xls": "application/vnd.ms-excel",
|
|
490
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
491
|
+
};
|
|
492
|
+
function guessMimeType(path) {
|
|
493
|
+
const dot = path.lastIndexOf(".");
|
|
494
|
+
const ext = dot === -1 ? "" : path.slice(dot).toLowerCase();
|
|
495
|
+
return MIME_BY_EXT[ext] ?? "application/octet-stream";
|
|
496
|
+
}
|
|
497
|
+
/** Filesystem-safe name for an exported document, keeping it recognisable. */
|
|
498
|
+
function exportFileName(doc) {
|
|
499
|
+
const base = (doc.fileName || doc.title || doc.id || "document").trim();
|
|
500
|
+
const stripped = base.replace(/\.(md|txt)$/i, "");
|
|
501
|
+
const safe = stripped.replace(/[/\\:*?"<>|]+/g, "-").replace(/\s+/g, " ").slice(0, 120);
|
|
502
|
+
return `${safe || doc.id || "document"}.md`;
|
|
503
|
+
}
|
|
504
|
+
const knowledge = program
|
|
505
|
+
.command("knowledge")
|
|
506
|
+
.description("Read, add and export entity knowledge documents");
|
|
507
|
+
knowledge
|
|
508
|
+
.command("list")
|
|
509
|
+
.description("List knowledge documents on an entity")
|
|
510
|
+
.requiredOption("--entity <entityId>", "Target entity id")
|
|
511
|
+
.option("--full", "Include each document's full content, not an excerpt")
|
|
512
|
+
.option("--org <org>", "Organization (admin only)")
|
|
513
|
+
.action(async (opts) => {
|
|
514
|
+
const client = getClient();
|
|
515
|
+
output(await client.listKnowledge({ entity: opts.entity, full: opts.full, org: opts.org }));
|
|
516
|
+
});
|
|
517
|
+
knowledge
|
|
518
|
+
.command("get <documentId>")
|
|
519
|
+
.description("Show one knowledge document with full content")
|
|
520
|
+
.option("--org <org>", "Organization (admin only)")
|
|
521
|
+
.action(async (documentId, opts) => {
|
|
522
|
+
const client = getClient();
|
|
523
|
+
output(await client.getKnowledge(documentId, opts.org));
|
|
524
|
+
});
|
|
525
|
+
knowledge
|
|
526
|
+
.command("search <query>")
|
|
527
|
+
.description("Semantic search across knowledge documents")
|
|
528
|
+
.option("--entity <entityId>", "Scope to one entity's org tree")
|
|
529
|
+
.option("--types <types>", "Comma-separated sourceType filter")
|
|
530
|
+
.option("--limit <count>", "Max hits", parseInt)
|
|
531
|
+
.option("--all-orgs", "Search every org (admin only)")
|
|
532
|
+
.option("--org <org>", "Organization (admin only)")
|
|
533
|
+
.action(async (query, opts) => {
|
|
534
|
+
const client = getClient();
|
|
535
|
+
output(await client.searchKnowledge({
|
|
536
|
+
query,
|
|
537
|
+
entity: opts.entity,
|
|
538
|
+
types: opts.types
|
|
539
|
+
? String(opts.types).split(",").map((t) => t.trim()).filter(Boolean)
|
|
540
|
+
: undefined,
|
|
541
|
+
limit: opts.limit,
|
|
542
|
+
allOrgs: opts.allOrgs,
|
|
543
|
+
org: opts.org,
|
|
544
|
+
}));
|
|
545
|
+
});
|
|
546
|
+
knowledge
|
|
547
|
+
.command("add")
|
|
548
|
+
.description("Add a markdown knowledge note (proposal by default)")
|
|
549
|
+
.requiredOption("--entity <entityId>", "Target entity id")
|
|
550
|
+
.requiredOption("--title <title>", "Document title")
|
|
551
|
+
.option("--file <path>", "Markdown file to read (- for stdin)")
|
|
552
|
+
.option("--content <text>", "Inline content instead of --file")
|
|
553
|
+
.option("--source-type <type>", "research | website | competitive_intel | …", "research")
|
|
554
|
+
.option("--source-url <url>", "Where the content came from")
|
|
555
|
+
.option("--source-description <text>", "Short provenance note")
|
|
556
|
+
.option("--confidence <n>", "0-1, helps reviewers prioritize", parseFloat)
|
|
557
|
+
.option("--source-agent <name>", "Attribution for the submitting agent")
|
|
558
|
+
.option("--direct", "Write immediately instead of proposing. Admin only — ask the operator first")
|
|
559
|
+
.option("--org <org>", "Organization (admin only)")
|
|
560
|
+
.action(async (opts) => {
|
|
561
|
+
let content;
|
|
562
|
+
if (opts.content) {
|
|
563
|
+
content = String(opts.content);
|
|
564
|
+
}
|
|
565
|
+
else if (opts.file) {
|
|
566
|
+
content = opts.file === "-" ? readFileSync(0, "utf8") : readFileSync(opts.file, "utf8");
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
throw new Error("Provide --file or --content");
|
|
570
|
+
}
|
|
571
|
+
if (!content.trim())
|
|
572
|
+
throw new Error("Content is empty");
|
|
573
|
+
const client = getClient();
|
|
574
|
+
output(await client.addKnowledge({
|
|
575
|
+
entityId: opts.entity,
|
|
576
|
+
title: opts.title,
|
|
577
|
+
content,
|
|
578
|
+
sourceType: opts.sourceType,
|
|
579
|
+
sourceUrl: opts.sourceUrl,
|
|
580
|
+
sourceDescription: opts.sourceDescription,
|
|
581
|
+
confidence: opts.confidence,
|
|
582
|
+
sourceAgent: opts.sourceAgent,
|
|
583
|
+
direct: Boolean(opts.direct),
|
|
584
|
+
org: opts.org,
|
|
585
|
+
}));
|
|
586
|
+
});
|
|
587
|
+
knowledge
|
|
588
|
+
.command("attach")
|
|
589
|
+
.description("Attach a file (PDF, DOCX, XLSX …) as a source. Admin only")
|
|
590
|
+
.requiredOption("--entity <entityId>", "Target entity id")
|
|
591
|
+
.requiredOption("--file <path>", "File to upload")
|
|
592
|
+
.option("--name <fileName>", "Stored file name (defaults to the file's own)")
|
|
593
|
+
.option("--mime <mimeType>", "Content type (guessed from the extension)")
|
|
594
|
+
.option("--source-type <type>", "research | website | contract | …", "other")
|
|
595
|
+
.option("--source-url <url>", "Where the file came from")
|
|
596
|
+
.option("--source-description <text>", "Short provenance note")
|
|
597
|
+
.option("--org <org>", "Organization (admin only)")
|
|
598
|
+
.action(async (opts) => {
|
|
599
|
+
if (!existsSync(opts.file))
|
|
600
|
+
throw new Error(`File not found: ${opts.file}`);
|
|
601
|
+
const data = readFileSync(opts.file);
|
|
602
|
+
if (data.byteLength === 0)
|
|
603
|
+
throw new Error("File is empty");
|
|
604
|
+
const client = getClient();
|
|
605
|
+
output(await client.attachKnowledgeFile({
|
|
606
|
+
entityId: opts.entity,
|
|
607
|
+
data: toArrayBuffer(data),
|
|
608
|
+
fileName: opts.name ?? basename(opts.file),
|
|
609
|
+
mimeType: opts.mime ?? guessMimeType(opts.file),
|
|
610
|
+
sourceType: opts.sourceType,
|
|
611
|
+
sourceUrl: opts.sourceUrl,
|
|
612
|
+
sourceDescription: opts.sourceDescription,
|
|
613
|
+
org: opts.org,
|
|
614
|
+
}));
|
|
615
|
+
});
|
|
616
|
+
knowledge
|
|
617
|
+
.command("export")
|
|
618
|
+
.description("Write an entity's knowledge documents to disk as markdown")
|
|
619
|
+
.requiredOption("--entity <entityId>", "Source entity id")
|
|
620
|
+
.requiredOption("--out <dir>", "Directory to write into (created if missing)")
|
|
621
|
+
.option("--org <org>", "Organization (admin only)")
|
|
622
|
+
.action(async (opts) => {
|
|
623
|
+
const client = getClient();
|
|
624
|
+
const docs = await client.listKnowledge({ entity: opts.entity, full: true, org: opts.org });
|
|
625
|
+
mkdirSync(opts.out, { recursive: true });
|
|
626
|
+
const written = [];
|
|
627
|
+
const skipped = [];
|
|
628
|
+
for (const doc of docs) {
|
|
629
|
+
const body = typeof doc.content === "string" ? doc.content : "";
|
|
630
|
+
if (!body) {
|
|
631
|
+
// A binary source whose text extraction hasn't finished has nothing to
|
|
632
|
+
// mirror yet. Report it rather than writing an empty file.
|
|
633
|
+
skipped.push({
|
|
634
|
+
id: doc.id,
|
|
635
|
+
title: doc.title,
|
|
636
|
+
reason: doc.processingStatus && doc.processingStatus !== "completed"
|
|
637
|
+
? `processingStatus=${doc.processingStatus}`
|
|
638
|
+
: "no extracted content",
|
|
639
|
+
});
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
const frontMatter = [
|
|
643
|
+
"---",
|
|
644
|
+
`document_id: ${doc.id}`,
|
|
645
|
+
`title: ${JSON.stringify(doc.title ?? "")}`,
|
|
646
|
+
`source_type: ${doc.sourceType ?? "other"}`,
|
|
647
|
+
...(doc.sourceUrl ? [`source_url: ${doc.sourceUrl}`] : []),
|
|
648
|
+
...(doc.sourceDescription
|
|
649
|
+
? [`source_description: ${JSON.stringify(doc.sourceDescription)}`]
|
|
650
|
+
: []),
|
|
651
|
+
`entity_id: ${opts.entity}`,
|
|
652
|
+
...(doc.createdAt ? [`created_at: ${new Date(doc.createdAt).toISOString()}`] : []),
|
|
653
|
+
"exported_from: dayofweek-platform",
|
|
654
|
+
"---",
|
|
655
|
+
"",
|
|
656
|
+
].join("\n");
|
|
657
|
+
const target = join(opts.out, exportFileName(doc));
|
|
658
|
+
writeFileSync(target, `${frontMatter}${body}\n`, "utf8");
|
|
659
|
+
written.push({ id: doc.id, file: target, bytes: Buffer.byteLength(body, "utf8") });
|
|
660
|
+
}
|
|
661
|
+
output({ entity: opts.entity, out: opts.out, total: docs.length, written, skipped });
|
|
662
|
+
});
|
|
465
663
|
// ── Skill Commands ───────────────────────────────────────────────────────────
|
|
466
664
|
const skill = program.command("skill").description("Manage the Day of Week agent skill");
|
|
467
665
|
function resolveTargetDirs(target, bundleName, customDir) {
|
|
@@ -641,6 +839,114 @@ skill
|
|
|
641
839
|
if (!installed)
|
|
642
840
|
process.exitCode = 2;
|
|
643
841
|
});
|
|
842
|
+
// ── Data Commands ────────────────────────────────────────────────────────────
|
|
843
|
+
//
|
|
844
|
+
// Generic, name-blind reads of platform datasets. The server's catalog decides
|
|
845
|
+
// what exists and what the caller may see — this CLI ships no dataset names,
|
|
846
|
+
// so new platform surfaces appear in `data list` without a CLI release.
|
|
847
|
+
const data = program.command("data").description("Read platform datasets the server offers you");
|
|
848
|
+
data
|
|
849
|
+
.command("list")
|
|
850
|
+
.description("List the datasets your credential may read")
|
|
851
|
+
.option("--org <org>", "Organization slug or id (admin only)")
|
|
852
|
+
.action(async (opts) => {
|
|
853
|
+
const client = getClient();
|
|
854
|
+
output(await client.listDatasets(opts.org));
|
|
855
|
+
});
|
|
856
|
+
data
|
|
857
|
+
.command("get <dataset>")
|
|
858
|
+
.description("Read one dataset's rows (names come from `data list`)")
|
|
859
|
+
.option("--limit <count>", "Max rows (default 50, max 200)", parseInt)
|
|
860
|
+
.option("--org <org>", "Organization slug or id (admin only)")
|
|
861
|
+
.action(async (dataset, opts) => {
|
|
862
|
+
const client = getClient();
|
|
863
|
+
output(await client.readDataset(dataset, { limit: opts.limit, org: opts.org }));
|
|
864
|
+
});
|
|
865
|
+
// ── Feedback Commands ────────────────────────────────────────────────────────
|
|
866
|
+
//
|
|
867
|
+
// The customer feedback backlog. Scope-gated rather than admin-gated:
|
|
868
|
+
// read:feedback for the reads, write:feedback to comment, admin:feedback for
|
|
869
|
+
// claim/status/priority. backOffice users implicitly hold every scope, so the
|
|
870
|
+
// group stays visible and the endpoint decides what the caller may do.
|
|
871
|
+
const feedback = program
|
|
872
|
+
.command("feedback")
|
|
873
|
+
.description("Read and work the customer feedback backlog");
|
|
874
|
+
feedback
|
|
875
|
+
.command("list")
|
|
876
|
+
.description("List backlog items")
|
|
877
|
+
.option("--status <status>", "backlog | planned | in_progress | shipped | rejected | new")
|
|
878
|
+
.option("--priority <priority>", "urgent | high | medium | low")
|
|
879
|
+
.option("--category <category>", "Filter by category, e.g. studio")
|
|
880
|
+
.option("--limit <count>", "Max results", parseInt)
|
|
881
|
+
.action(async (opts) => {
|
|
882
|
+
const client = getClient();
|
|
883
|
+
output(await client.listFeedback({
|
|
884
|
+
status: opts.status,
|
|
885
|
+
priority: opts.priority,
|
|
886
|
+
category: opts.category,
|
|
887
|
+
limit: opts.limit,
|
|
888
|
+
}));
|
|
889
|
+
});
|
|
890
|
+
feedback
|
|
891
|
+
.command("next")
|
|
892
|
+
.description("What should I work on next — the prioritizer's top picks")
|
|
893
|
+
.option("--limit <count>", "How many recommendations", parseInt)
|
|
894
|
+
.action(async (opts) => {
|
|
895
|
+
const client = getClient();
|
|
896
|
+
output(await client.feedbackRecommendations(opts.limit));
|
|
897
|
+
});
|
|
898
|
+
feedback
|
|
899
|
+
.command("show <itemId>")
|
|
900
|
+
.description("Show one backlog item in full")
|
|
901
|
+
.action(async (itemId) => {
|
|
902
|
+
const client = getClient();
|
|
903
|
+
output(await client.getFeedbackItem(itemId));
|
|
904
|
+
});
|
|
905
|
+
feedback
|
|
906
|
+
.command("claim <itemId>")
|
|
907
|
+
.description("Claim an item so humans see it is being worked on")
|
|
908
|
+
.action(async (itemId) => {
|
|
909
|
+
const client = getClient();
|
|
910
|
+
output(await client.claimFeedbackItem(itemId));
|
|
911
|
+
});
|
|
912
|
+
feedback
|
|
913
|
+
.command("comment <itemId>")
|
|
914
|
+
.description("Add a comment to a backlog item")
|
|
915
|
+
.option("--body <text>", "Comment text")
|
|
916
|
+
.option("--file <path>", "Read the comment from a file (- for stdin)")
|
|
917
|
+
.action(async (itemId, opts) => {
|
|
918
|
+
let body;
|
|
919
|
+
if (opts.body) {
|
|
920
|
+
body = String(opts.body);
|
|
921
|
+
}
|
|
922
|
+
else if (opts.file) {
|
|
923
|
+
body = opts.file === "-" ? readFileSync(0, "utf8") : readFileSync(opts.file, "utf8");
|
|
924
|
+
}
|
|
925
|
+
else {
|
|
926
|
+
throw new Error("Provide --body or --file");
|
|
927
|
+
}
|
|
928
|
+
if (!body.trim())
|
|
929
|
+
throw new Error("Comment is empty");
|
|
930
|
+
const client = getClient();
|
|
931
|
+
output(await client.commentOnFeedbackItem(itemId, body));
|
|
932
|
+
});
|
|
933
|
+
feedback
|
|
934
|
+
.command("status <itemId>")
|
|
935
|
+
.description("Set status and/or priority on a backlog item")
|
|
936
|
+
.option("--status <status>", "backlog | planned | in_progress | shipped | rejected")
|
|
937
|
+
.option("--priority <priority>", "urgent | high | medium | low")
|
|
938
|
+
.option("--rejected-reason <text>", "Why it was rejected (with --status rejected)")
|
|
939
|
+
.action(async (itemId, opts) => {
|
|
940
|
+
if (!opts.status && !opts.priority) {
|
|
941
|
+
throw new Error("Provide --status and/or --priority");
|
|
942
|
+
}
|
|
943
|
+
const client = getClient();
|
|
944
|
+
output(await client.updateFeedbackItem(itemId, {
|
|
945
|
+
status: opts.status,
|
|
946
|
+
priority: opts.priority,
|
|
947
|
+
rejectedReason: opts.rejectedReason,
|
|
948
|
+
}));
|
|
949
|
+
});
|
|
644
950
|
// ── Admin Commands ───────────────────────────────────────────────────────────
|
|
645
951
|
//
|
|
646
952
|
// These are admin-only. They're registered as hidden subcommands when the
|
|
@@ -692,6 +998,224 @@ function registerAdminCommands() {
|
|
|
692
998
|
});
|
|
693
999
|
output(result);
|
|
694
1000
|
});
|
|
1001
|
+
// ── Assortment import ──────────────────────────────────────────────────────
|
|
1002
|
+
//
|
|
1003
|
+
// These write real Creator rows rather than proposals, so the operator
|
|
1004
|
+
// approves the parsed result first. --dry-run rehearses the whole import
|
|
1005
|
+
// server-side and rolls it back; that response is what the review is built
|
|
1006
|
+
// from. Writing requires --approved for the same reason email does.
|
|
1007
|
+
const produce = program
|
|
1008
|
+
.command("produce", { hidden: true })
|
|
1009
|
+
.description("Assortment import and recipe refinement (DoW staff)");
|
|
1010
|
+
produce
|
|
1011
|
+
.command("import")
|
|
1012
|
+
.description("Import a producer's items into Creator. Requires --dry-run or --approved")
|
|
1013
|
+
.requiredOption("--file <path>", "JSON payload: { entityId, items[] } (- for stdin)")
|
|
1014
|
+
.option("--dry-run", "Rehearse server-side and roll back — build the review from this")
|
|
1015
|
+
.option("--skip-existing-names", "Skip items whose displayName already exists")
|
|
1016
|
+
.option("--approved", "The operator approved the dry-run result")
|
|
1017
|
+
.option("--org <org>", "Organization slug or id")
|
|
1018
|
+
.action(async (opts) => {
|
|
1019
|
+
if (!opts.dryRun && !opts.approved) {
|
|
1020
|
+
throw new Error("Refusing to write: run with --dry-run first, show the operator the result, " +
|
|
1021
|
+
"then repeat with --approved. This creates real Creator rows, not proposals.");
|
|
1022
|
+
}
|
|
1023
|
+
const raw = opts.file === "-" ? readFileSync(0, "utf8") : readFileSync(opts.file, "utf8");
|
|
1024
|
+
const payload = JSON.parse(raw);
|
|
1025
|
+
if (!payload?.entityId || !Array.isArray(payload.items)) {
|
|
1026
|
+
throw new Error("Payload needs { entityId, items: [...] }");
|
|
1027
|
+
}
|
|
1028
|
+
const client = getClient();
|
|
1029
|
+
output(await client.importProduce({
|
|
1030
|
+
entityId: payload.entityId,
|
|
1031
|
+
items: payload.items,
|
|
1032
|
+
dryRun: Boolean(opts.dryRun),
|
|
1033
|
+
skipExistingNames: opts.skipExistingNames ?? payload.skipExistingNames,
|
|
1034
|
+
org: opts.org ?? payload.org,
|
|
1035
|
+
}));
|
|
1036
|
+
});
|
|
1037
|
+
produce
|
|
1038
|
+
.command("add-concepts")
|
|
1039
|
+
.description("Add concepts to the shared produce catalog. Requires --approved")
|
|
1040
|
+
.requiredOption("--file <path>", "JSON: { concepts: [...] } or a bare array (- for stdin)")
|
|
1041
|
+
.option("--approved", "The operator approved these concepts")
|
|
1042
|
+
.option("--org <org>", "Organization slug or id")
|
|
1043
|
+
.action(async (opts) => {
|
|
1044
|
+
if (!opts.approved) {
|
|
1045
|
+
throw new Error("Refusing to write: the produce catalog is shared by every customer. " +
|
|
1046
|
+
"Show the operator the concepts you want to add, then repeat with --approved.");
|
|
1047
|
+
}
|
|
1048
|
+
const raw = opts.file === "-" ? readFileSync(0, "utf8") : readFileSync(opts.file, "utf8");
|
|
1049
|
+
const parsed = JSON.parse(raw);
|
|
1050
|
+
const concepts = Array.isArray(parsed) ? parsed : parsed?.concepts;
|
|
1051
|
+
if (!Array.isArray(concepts) || concepts.length === 0) {
|
|
1052
|
+
throw new Error("Payload needs a non-empty `concepts` array");
|
|
1053
|
+
}
|
|
1054
|
+
const client = getClient();
|
|
1055
|
+
output(await client.createCatalogConcepts({ concepts, org: opts.org ?? parsed?.org }));
|
|
1056
|
+
});
|
|
1057
|
+
produce
|
|
1058
|
+
.command("ingredients")
|
|
1059
|
+
.description("Recipe ingredients still standing in for something vaguer")
|
|
1060
|
+
.requiredOption("--entity <entityId>", "Producer entity")
|
|
1061
|
+
.option("--org <org>", "Organization slug or id")
|
|
1062
|
+
.action(async (opts) => {
|
|
1063
|
+
const client = getClient();
|
|
1064
|
+
output(await client.listImpreciseIngredients(opts.entity, opts.org));
|
|
1065
|
+
});
|
|
1066
|
+
produce
|
|
1067
|
+
.command("refine")
|
|
1068
|
+
.description("Point an imprecise ingredient at what it actually is")
|
|
1069
|
+
.requiredOption("--process-input <id>", "processInputId from `produce ingredients`")
|
|
1070
|
+
.option("--concept <catalogConceptId>", "What it actually is")
|
|
1071
|
+
.option("--material <materialId>", "Material node instead of a catalog concept")
|
|
1072
|
+
.option("--role <role>", "e.g. seasoning, base")
|
|
1073
|
+
.option("--qty <n>", "Quantity", parseFloat)
|
|
1074
|
+
.option("--unit <unitCode>", "Unit code")
|
|
1075
|
+
.option("--still-imprecise", "Narrowed but not resolved — keeps it on the worklist")
|
|
1076
|
+
.option("--org <org>", "Organization slug or id")
|
|
1077
|
+
.action(async (opts) => {
|
|
1078
|
+
if (!opts.concept && !opts.material) {
|
|
1079
|
+
throw new Error("Provide --concept or --material");
|
|
1080
|
+
}
|
|
1081
|
+
const client = getClient();
|
|
1082
|
+
output(await client.refineIngredient({
|
|
1083
|
+
processInputId: opts.processInput,
|
|
1084
|
+
catalogConceptId: opts.concept,
|
|
1085
|
+
materialId: opts.material,
|
|
1086
|
+
role: opts.role,
|
|
1087
|
+
qty: opts.qty,
|
|
1088
|
+
unitCode: opts.unit,
|
|
1089
|
+
stillImprecise: Boolean(opts.stillImprecise),
|
|
1090
|
+
org: opts.org,
|
|
1091
|
+
}));
|
|
1092
|
+
});
|
|
1093
|
+
admin
|
|
1094
|
+
.command("customers-only")
|
|
1095
|
+
.description("Entities with a customer role and no investor/partner/producer role")
|
|
1096
|
+
.action(async () => {
|
|
1097
|
+
const client = getClient();
|
|
1098
|
+
output(await client.adminCustomersOnly());
|
|
1099
|
+
});
|
|
1100
|
+
admin
|
|
1101
|
+
.command("press-inbox")
|
|
1102
|
+
.description("Tips mailed to press@mail.dayofweek.com (Press Radar)")
|
|
1103
|
+
.option("--since <date>", "ISO date or epoch ms (default 45 days back)")
|
|
1104
|
+
.option("--limit <count>", "Max emails (max 200)", parseInt)
|
|
1105
|
+
.action(async (opts) => {
|
|
1106
|
+
const client = getClient();
|
|
1107
|
+
output(await client.adminPressInbox({ since: opts.since, limit: opts.limit }));
|
|
1108
|
+
});
|
|
1109
|
+
// ── Customer emails ────────────────────────────────────────────────────────
|
|
1110
|
+
//
|
|
1111
|
+
// Mail sent to a customer's own inbox address (<slug>@mail.dayofweek.com)
|
|
1112
|
+
// becomes a thread the agent can answer. Reading is free; sending is not:
|
|
1113
|
+
// `reply` and `compose` refuse to run without --approved, so an agent cannot
|
|
1114
|
+
// mail a customer as a side effect of "check the inbox". The human approves
|
|
1115
|
+
// the exact text first — that rule lives in the skill; --approved is the
|
|
1116
|
+
// mechanical backstop for it.
|
|
1117
|
+
const emails = program
|
|
1118
|
+
.command("emails", { hidden: true })
|
|
1119
|
+
.description("Customer email threads — read, reply, compose (DoW staff)");
|
|
1120
|
+
const APPROVAL_REQUIRED = "Refusing to send: pass --approved once the human has approved this exact text. " +
|
|
1121
|
+
"Never send an email the human has not seen.";
|
|
1122
|
+
emails
|
|
1123
|
+
.command("list")
|
|
1124
|
+
.description("List customer email threads")
|
|
1125
|
+
.option("--status <status>", "needs_reply | manual_review | all (default all)")
|
|
1126
|
+
.option("--since <date>", "ISO date (2026-07-01) or epoch ms; default 30 days back")
|
|
1127
|
+
.option("--customer <entityId>", "One customer only")
|
|
1128
|
+
.option("--limit <count>", "Max threads (default 50, max 200)", parseInt)
|
|
1129
|
+
.option("--org <org>", "Organization slug or id")
|
|
1130
|
+
.action(async (opts) => {
|
|
1131
|
+
const client = getClient();
|
|
1132
|
+
output(await client.listEmailThreads({
|
|
1133
|
+
status: opts.status,
|
|
1134
|
+
since: opts.since,
|
|
1135
|
+
customer: opts.customer,
|
|
1136
|
+
limit: opts.limit,
|
|
1137
|
+
org: opts.org,
|
|
1138
|
+
}));
|
|
1139
|
+
});
|
|
1140
|
+
emails
|
|
1141
|
+
.command("show <threadKey>")
|
|
1142
|
+
.description("Full thread: messages, attachments, and reply hints")
|
|
1143
|
+
.action(async (threadKey) => {
|
|
1144
|
+
const client = getClient();
|
|
1145
|
+
output(await client.getEmailThread(threadKey));
|
|
1146
|
+
});
|
|
1147
|
+
emails
|
|
1148
|
+
.command("reply <threadKey>")
|
|
1149
|
+
.description("Reply in a thread. Requires --approved")
|
|
1150
|
+
.option("--message <text>", "Plain-text reply body")
|
|
1151
|
+
.option("--file <path>", "Read the body from a file (- for stdin)")
|
|
1152
|
+
.option("--subject <text>", "Override the subject (defaults to Re: …)")
|
|
1153
|
+
.option("--to <address>", "Override the recipient")
|
|
1154
|
+
.option("--cc <addresses>", "Comma-separated cc list")
|
|
1155
|
+
.option("--no-quote", "Do not quote the original underneath")
|
|
1156
|
+
.option("--approved", "The human approved this exact text")
|
|
1157
|
+
.action(async (threadKey, opts) => {
|
|
1158
|
+
if (!opts.approved)
|
|
1159
|
+
throw new Error(APPROVAL_REQUIRED);
|
|
1160
|
+
let message;
|
|
1161
|
+
if (opts.message) {
|
|
1162
|
+
message = String(opts.message);
|
|
1163
|
+
}
|
|
1164
|
+
else if (opts.file) {
|
|
1165
|
+
message = opts.file === "-" ? readFileSync(0, "utf8") : readFileSync(opts.file, "utf8");
|
|
1166
|
+
}
|
|
1167
|
+
else {
|
|
1168
|
+
throw new Error("Provide --message or --file");
|
|
1169
|
+
}
|
|
1170
|
+
if (!message.trim())
|
|
1171
|
+
throw new Error("Message is empty");
|
|
1172
|
+
const client = getClient();
|
|
1173
|
+
output(await client.replyToEmailThread(threadKey, {
|
|
1174
|
+
message,
|
|
1175
|
+
subject: opts.subject,
|
|
1176
|
+
to: opts.to,
|
|
1177
|
+
cc: splitList(opts.cc),
|
|
1178
|
+
quote: opts.quote,
|
|
1179
|
+
}));
|
|
1180
|
+
});
|
|
1181
|
+
emails
|
|
1182
|
+
.command("compose")
|
|
1183
|
+
.description("Start a new thread from a customer inbox. Requires --approved")
|
|
1184
|
+
.option("--entity <entityId>", "Customer entity whose inbox sends the mail")
|
|
1185
|
+
.option("--inbox <address>", "Inbox address instead of --entity")
|
|
1186
|
+
.requiredOption("--to <address>", "Recipient")
|
|
1187
|
+
.option("--cc <addresses>", "Comma-separated cc list")
|
|
1188
|
+
.requiredOption("--subject <text>", "Subject line")
|
|
1189
|
+
.option("--message <text>", "Plain-text body")
|
|
1190
|
+
.option("--file <path>", "Read the body from a file (- for stdin)")
|
|
1191
|
+
.option("--approved", "The human approved this exact text")
|
|
1192
|
+
.action(async (opts) => {
|
|
1193
|
+
if (!opts.approved)
|
|
1194
|
+
throw new Error(APPROVAL_REQUIRED);
|
|
1195
|
+
if (!opts.entity && !opts.inbox)
|
|
1196
|
+
throw new Error("Provide --entity or --inbox");
|
|
1197
|
+
let message;
|
|
1198
|
+
if (opts.message) {
|
|
1199
|
+
message = String(opts.message);
|
|
1200
|
+
}
|
|
1201
|
+
else if (opts.file) {
|
|
1202
|
+
message = opts.file === "-" ? readFileSync(0, "utf8") : readFileSync(opts.file, "utf8");
|
|
1203
|
+
}
|
|
1204
|
+
else {
|
|
1205
|
+
throw new Error("Provide --message or --file");
|
|
1206
|
+
}
|
|
1207
|
+
if (!message.trim())
|
|
1208
|
+
throw new Error("Message is empty");
|
|
1209
|
+
const client = getClient();
|
|
1210
|
+
output(await client.composeEmail({
|
|
1211
|
+
entityId: opts.entity,
|
|
1212
|
+
inbox: opts.inbox,
|
|
1213
|
+
to: opts.to,
|
|
1214
|
+
cc: splitList(opts.cc),
|
|
1215
|
+
subject: opts.subject,
|
|
1216
|
+
message,
|
|
1217
|
+
}));
|
|
1218
|
+
});
|
|
695
1219
|
}
|
|
696
1220
|
registerAdminCommands();
|
|
697
1221
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -703,6 +1227,13 @@ async function readStdin() {
|
|
|
703
1227
|
}
|
|
704
1228
|
return chunks.join("\n");
|
|
705
1229
|
}
|
|
1230
|
+
/** Parse a comma-separated CLI option into a trimmed list, or undefined. */
|
|
1231
|
+
function splitList(value) {
|
|
1232
|
+
if (!value)
|
|
1233
|
+
return undefined;
|
|
1234
|
+
const items = String(value).split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
1235
|
+
return items.length ? items : undefined;
|
|
1236
|
+
}
|
|
706
1237
|
function inferMimeType(path) {
|
|
707
1238
|
const extension = path.toLowerCase().split(".").at(-1);
|
|
708
1239
|
const types = {
|