@dayofweek/dcli 1.5.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 +70 -1
- package/dist/bin/dcli.js +350 -4
- package/dist/bundle/dcli.cjs +5991 -0
- package/dist/client.d.ts +103 -2
- package/dist/client.js +139 -4
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -111,6 +111,55 @@ immediately, so treat them as something you do when a person has asked for that
|
|
|
111
111
|
specific document — not as the normal path. The server rejects them for
|
|
112
112
|
non-admin tokens.
|
|
113
113
|
|
|
114
|
+
## Datasets
|
|
115
|
+
|
|
116
|
+
The platform offers additional read-only datasets beyond entities and
|
|
117
|
+
knowledge. The catalog is server-owned and discovered at runtime — what
|
|
118
|
+
`data list` returns is exactly what your credential may read.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
dcli data list --json
|
|
122
|
+
dcli data get <dataset> --limit 100 --json
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Responses are `{ dataset, total, truncated, rows }`. The CLI has no built-in
|
|
126
|
+
dataset names; new datasets appear in the listing without a CLI update.
|
|
127
|
+
|
|
128
|
+
## Feedback backlog
|
|
129
|
+
|
|
130
|
+
The customer feedback backlog that humans and coding agents work together.
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
dcli feedback next --json # what should I work on next
|
|
134
|
+
dcli feedback list --status backlog --json
|
|
135
|
+
dcli feedback show <itemId> --json
|
|
136
|
+
dcli feedback claim <itemId> --json # signal that you picked it up
|
|
137
|
+
dcli feedback comment <itemId> --body "Fixed in #482"
|
|
138
|
+
dcli feedback status <itemId> --status shipped
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Access follows the token's scopes: `read:feedback` for the reads,
|
|
142
|
+
`write:feedback` to comment, `admin:feedback` for claim/status/priority.
|
|
143
|
+
Day of Week staff hold all three implicitly.
|
|
144
|
+
|
|
145
|
+
## Customer emails (staff)
|
|
146
|
+
|
|
147
|
+
Mail sent to a customer's own inbox address becomes a thread you can answer.
|
|
148
|
+
The commands appear once `dcli auth status` has cached your admin role.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
dcli emails list --status needs_reply --json
|
|
152
|
+
dcli emails show <threadKey> --json
|
|
153
|
+
dcli emails reply <threadKey> --message "..." --approved
|
|
154
|
+
dcli emails compose --entity <entityId> --to person@example.com \
|
|
155
|
+
--subject "..." --message "..." --approved
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`reply` and `compose` refuse to run without `--approved`. Sending mail as a
|
|
159
|
+
customer is irreversible and outward-facing, so it cannot happen as a side
|
|
160
|
+
effect of reading the inbox — a person approves the exact text first, and
|
|
161
|
+
`--approved` records that they did.
|
|
162
|
+
|
|
114
163
|
## Legacy platform commands
|
|
115
164
|
|
|
116
165
|
Existing read/proposal workflows remain compatible:
|
|
@@ -135,11 +184,31 @@ Development commands:
|
|
|
135
184
|
```bash
|
|
136
185
|
npm install
|
|
137
186
|
npm test
|
|
138
|
-
npm run build
|
|
187
|
+
npm run build # tsc + the dependency-free bundle
|
|
188
|
+
npm run build:bundle # just dist/bundle/dcli.cjs
|
|
139
189
|
npm run standalone:build
|
|
140
190
|
npm run standalone:smoke
|
|
141
191
|
```
|
|
142
192
|
|
|
193
|
+
### What gets published
|
|
194
|
+
|
|
195
|
+
`npm run build` produces two runnable forms, and both ship:
|
|
196
|
+
|
|
197
|
+
- **`dist/bin/dcli.js`** — the normal entry point, the one `bin` points at. It
|
|
198
|
+
imports `commander` and `open` from `node_modules`, which is exactly right
|
|
199
|
+
when npm installed the package.
|
|
200
|
+
- **`dist/bundle/dcli.cjs`** — the same CLI with its dependencies compiled in,
|
|
201
|
+
runnable straight from an unpacked tarball.
|
|
202
|
+
|
|
203
|
+
The bundle exists for consumers that unpack the tarball themselves instead of
|
|
204
|
+
installing it, the Day of Week desktop app being the one that matters: it fetches
|
|
205
|
+
the published package and runs it with Electron's Node, so a customer with no
|
|
206
|
+
`node` and no `npm` still gets a working `dcli`. Without the bundle that install
|
|
207
|
+
starts and immediately fails on a missing `commander`.
|
|
208
|
+
|
|
209
|
+
Keep both. Dropping `dist/bundle/` from `files` silently breaks the desktop app's
|
|
210
|
+
dcli updates.
|
|
211
|
+
|
|
143
212
|
## License
|
|
144
213
|
|
|
145
214
|
MIT
|
package/dist/bin/dcli.js
CHANGED
|
@@ -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);
|
|
@@ -826,6 +839,114 @@ skill
|
|
|
826
839
|
if (!installed)
|
|
827
840
|
process.exitCode = 2;
|
|
828
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
|
+
});
|
|
829
950
|
// ── Admin Commands ───────────────────────────────────────────────────────────
|
|
830
951
|
//
|
|
831
952
|
// These are admin-only. They're registered as hidden subcommands when the
|
|
@@ -877,6 +998,224 @@ function registerAdminCommands() {
|
|
|
877
998
|
});
|
|
878
999
|
output(result);
|
|
879
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
|
+
});
|
|
880
1219
|
}
|
|
881
1220
|
registerAdminCommands();
|
|
882
1221
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -888,6 +1227,13 @@ async function readStdin() {
|
|
|
888
1227
|
}
|
|
889
1228
|
return chunks.join("\n");
|
|
890
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
|
+
}
|
|
891
1237
|
function inferMimeType(path) {
|
|
892
1238
|
const extension = path.toLowerCase().split(".").at(-1);
|
|
893
1239
|
const types = {
|