@dayofweek/dcli 1.9.1 → 1.11.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
@@ -188,6 +188,38 @@ customer is irreversible and outward-facing, so it cannot happen as a side
188
188
  effect of reading the inbox — a person approves the exact text first, and
189
189
  `--approved` records that they did.
190
190
 
191
+ ## Cross-org reads (staff)
192
+
193
+ Admin tokens can read across every organization. Both listings are
194
+ cursor-paged: filters apply within each scanned page, so a page can be short
195
+ or empty while more rows remain — `--all` walks the pages for you.
196
+
197
+ ```bash
198
+ dcli admin entities --category supply --all --json # every producer, every org
199
+ dcli admin entities --role producer --with-roles --json # first page, roles attached
200
+ dcli admin entities --search huseby --json
201
+ dcli admin entities --limit 500 --cursor <nextCursor> --json
202
+
203
+ dcli admin market-intel stats --json # the market intelligence register, counted
204
+ dcli admin market-intel list --type farm --region Agder --all --json
205
+ dcli admin market-intel list --bbox 57.95,6.35,59.70,9.40 --json
206
+ dcli admin market-intel get <id> --json
207
+ ```
208
+
209
+ Entity rows carry `typeName` and `typeCategory` resolved from the entity's
210
+ type — count on those, not on the legacy `entityType` string. The market
211
+ intelligence register is a separate table from the entity hierarchy (every row
212
+ has coordinates; `linkedEntity` names the hierarchy entity it points at, if
213
+ any), so "how many producers do we know of" reads both.
214
+
215
+ An area's actors can be tied to the platform entity they are:
216
+
217
+ ```bash
218
+ dcli brain actors list --area <areaId> --match --json # unlinked actors get name-matched candidates (admins)
219
+ dcli brain actors link --area <areaId> --actor <actorId> --entity <entityId>
220
+ dcli brain actors link --area <areaId> --actor <actorId> --unlink
221
+ ```
222
+
191
223
  ## Legacy platform commands
192
224
 
193
225
  Existing read/proposal workflows remain compatible:
package/dist/bin/dcli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from "commander";
3
- import { ApiError, DayOfWeekClient, toArrayBuffer } from "../client.js";
3
+ import { ApiError, DayOfWeekClient, toArrayBuffer, collectAdminPages } 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";
@@ -326,8 +326,29 @@ brainActors
326
326
  .command("list")
327
327
  .description("List an area's actors (active, sorted by name)")
328
328
  .requiredOption("--area <areaId>", "Area id (from `brain list`)")
329
+ .option("--match", "Suggest matching platform entities for unlinked actors (admins)")
329
330
  .action(async (opts) => {
330
- output(await getClient().listBrainActors(opts.area));
331
+ output(await getClient().listBrainActors(opts.area, { match: opts.match }));
332
+ });
333
+ brainActors
334
+ .command("link")
335
+ .description("Link an actor to the platform entity it is (or --unlink)")
336
+ .requiredOption("--area <areaId>", "Area id (from `brain list`)")
337
+ .requiredOption("--actor <actorId>", "Actor id (from `brain actors list`)")
338
+ .option("--entity <entityId>", "Platform entity id (from `admin entities` or `read entities`)")
339
+ .option("--unlink", "Remove the current link")
340
+ .action(async (opts) => {
341
+ if (!opts.entity && !opts.unlink) {
342
+ throw new Error("Provide --entity <entityId> to link, or --unlink to remove the link");
343
+ }
344
+ if (opts.entity && opts.unlink) {
345
+ throw new Error("--entity and --unlink are mutually exclusive");
346
+ }
347
+ output(await getClient().linkBrainActor({
348
+ areaId: opts.area,
349
+ actorId: opts.actor,
350
+ entityId: opts.unlink ? null : opts.entity,
351
+ }));
331
352
  });
332
353
  brainActors
333
354
  .command("add")
@@ -430,6 +451,71 @@ brainSource
430
451
  }
431
452
  output(await getClient().scopeBrainSource(sourceId, opts.actor ?? null));
432
453
  });
454
+ const brainTags = brain
455
+ .command("tags")
456
+ .description("Work with an area's tag vocabulary and resource couplings");
457
+ brainTags
458
+ .command("list")
459
+ .description("List an area's tags (predefined hierarchical set + user-defined)")
460
+ .requiredOption("--area <areaId>", "Area id (from `brain list`)")
461
+ .action(async (opts) => {
462
+ output(await getClient().listBrainTags(opts.area));
463
+ });
464
+ brainTags
465
+ .command("add")
466
+ .description("Add a user-defined tag (idempotent on the derived key)")
467
+ .requiredOption("--area <areaId>", "Area id (from `brain list`)")
468
+ .requiredOption("--label <label>", "Tag label")
469
+ .option("--parent <key>", "Parent tag key (makes this a child tag)")
470
+ .option("--description <text>", "What the tag means")
471
+ .action(async (opts) => {
472
+ output(await getClient().createBrainTag({
473
+ areaId: opts.area,
474
+ label: opts.label,
475
+ parentKey: opts.parent,
476
+ description: opts.description,
477
+ }));
478
+ });
479
+ brainTags
480
+ .command("couplings")
481
+ .description("List tag/actor couplings — filter by actor, tag, source or note")
482
+ .requiredOption("--area <areaId>", "Area id (from `brain list`)")
483
+ .option("--tag <key>", "Resources coupled to this tag")
484
+ .option("--actor <actorId>", "Resources coupled to this actor")
485
+ .option("--source <sourceId>", "One source's tags and actors")
486
+ .option("--note <noteId>", "One note/page's tags and actors")
487
+ .action(async (opts) => {
488
+ output(await getClient().listBrainCouplings({
489
+ areaId: opts.area,
490
+ tag: opts.tag,
491
+ actor: opts.actor,
492
+ source: opts.source,
493
+ note: opts.note,
494
+ }));
495
+ });
496
+ brainTags
497
+ .command("couple")
498
+ .description("Couple a source/note to a tag or an actor (--remove uncouples)")
499
+ .option("--source <sourceId>", "Source to couple")
500
+ .option("--note <noteId>", "Note/page to couple")
501
+ .option("--tag <key>", "Tag key (from `brain tags list`)")
502
+ .option("--actor <actorId>", "Actor id (from `brain actors list`)")
503
+ .option("--remove", "Remove the coupling instead of adding it")
504
+ .action(async (opts) => {
505
+ if (Boolean(opts.source) === Boolean(opts.note)) {
506
+ throw new Error("Pass exactly one of --source <sourceId> or --note <noteId>");
507
+ }
508
+ if (Boolean(opts.tag) === Boolean(opts.actor)) {
509
+ throw new Error("Pass exactly one of --tag <key> or --actor <actorId>");
510
+ }
511
+ output(await getClient().coupleBrainResource({
512
+ sourceId: opts.source,
513
+ noteId: opts.note,
514
+ tagKey: opts.tag,
515
+ actorId: opts.actor,
516
+ remove: opts.remove,
517
+ }));
518
+ });
433
519
  auth
434
520
  .command("devices")
435
521
  .description("List your agent tokens")
@@ -1284,22 +1370,86 @@ function registerAdminCommands() {
1284
1370
  .description("Admin-only cross-org operations (DoW staff)");
1285
1371
  admin
1286
1372
  .command("entities")
1287
- .description("List entities across all orgs with admin filters")
1288
- .option("--type <entityType>", "Filter by entity type")
1373
+ .description("List entities across all orgs with admin filters (cursor-paged)")
1374
+ .option("--type <entityType>", "Filter by type name (farm, producer, restaurant, …)")
1375
+ .option("--category <category>", "Filter by type category (supply, demand, partner, financial, …)")
1376
+ .option("--role <role>", "Only entities holding an active role: customer, partner, investor, producer, market_intel")
1377
+ .option("--with-roles", "Attach each entity's active roles")
1289
1378
  .option("--missing-location", "Only entities lacking metadata.places[].lat/lng")
1290
1379
  .option("--search <query>", "Substring match on name")
1291
1380
  .option("--org <slug>", "Restrict to a single org slug or ID")
1292
- .option("--limit <count>", "Max results (default 200)", parseInt)
1381
+ .option("--limit <count>", "Rows scanned per page (default 200, max 2000)", parseInt)
1382
+ .option("--cursor <cursor>", "Continue from a previous page's nextCursor")
1383
+ .option("--all", "Walk every page and return all matches")
1293
1384
  .action(async (opts) => {
1294
1385
  const client = getClient();
1295
- const result = await client.adminListEntities({
1386
+ const fetchPage = (cursor) => client.adminListEntities({
1296
1387
  org: opts.org,
1297
1388
  type: opts.type,
1389
+ category: opts.category,
1390
+ role: opts.role,
1391
+ withRoles: opts.withRoles,
1298
1392
  missingLocation: opts.missingLocation,
1299
1393
  search: opts.search,
1300
1394
  limit: opts.limit,
1395
+ cursor,
1301
1396
  });
1302
- output(result);
1397
+ output(opts.all ? await collectAdminPages(fetchPage, opts.cursor) : await fetchPage(opts.cursor));
1398
+ });
1399
+ // The market intelligence register is a separate table from the entity
1400
+ // hierarchy: businesses recorded for research and prospecting, each with
1401
+ // coordinates. A platform-wide "how many do we know of" reads both.
1402
+ const marketIntel = admin
1403
+ .command("market-intel")
1404
+ .description("The market intelligence register — businesses recorded for research and prospecting");
1405
+ marketIntel
1406
+ .command("list")
1407
+ .description("List register rows (cursor-paged, newest first)")
1408
+ .option("--type <entityType>", "farm, restaurant, farm_shop, retail_store, distributor, processor, market, cooperative, supplier, customer, competitor, other")
1409
+ .option("--relationship <type>", "prospect, active_customer, supplier, partner, competitor, market_research, other")
1410
+ .option("--priority <priority>", "critical, high, medium, low, watching")
1411
+ .option("--stage <stage>", "Opportunity stage (research, qualified_lead, negotiation, …)")
1412
+ .option("--region <region>", "Exact match on region (case-insensitive)")
1413
+ .option("--search <query>", "Substring match on name")
1414
+ .option("--bbox <latMin,lngMin,latMax,lngMax>", "Only rows inside a lat/lng box")
1415
+ .option("--active", "Only active rows")
1416
+ .option("--inactive", "Only inactive rows")
1417
+ .option("--org <slug>", "Restrict to the owning org")
1418
+ .option("--limit <count>", "Rows scanned per page (default 200, max 2000)", parseInt)
1419
+ .option("--cursor <cursor>", "Continue from a previous page's nextCursor")
1420
+ .option("--all", "Walk every page and return all matches")
1421
+ .action(async (opts) => {
1422
+ if (opts.active && opts.inactive) {
1423
+ throw new Error("--active and --inactive are mutually exclusive");
1424
+ }
1425
+ const client = getClient();
1426
+ const fetchPage = (cursor) => client.adminListMarketIntel({
1427
+ org: opts.org,
1428
+ type: opts.type,
1429
+ relationship: opts.relationship,
1430
+ priority: opts.priority,
1431
+ stage: opts.stage,
1432
+ region: opts.region,
1433
+ search: opts.search,
1434
+ bbox: opts.bbox,
1435
+ active: opts.active ? true : opts.inactive ? false : undefined,
1436
+ limit: opts.limit,
1437
+ cursor,
1438
+ });
1439
+ output(opts.all ? await collectAdminPages(fetchPage, opts.cursor) : await fetchPage(opts.cursor));
1440
+ });
1441
+ marketIntel
1442
+ .command("get <id>")
1443
+ .description("One register record with its latest interactions")
1444
+ .action(async (id) => {
1445
+ output(await getClient().adminGetMarketIntel(id));
1446
+ });
1447
+ marketIntel
1448
+ .command("stats")
1449
+ .description("Counts by type, relationship, priority, stage, region and country")
1450
+ .option("--org <slug>", "Restrict to the owning org")
1451
+ .action(async (opts) => {
1452
+ output(await getClient().adminMarketIntelStats(opts.org));
1303
1453
  });
1304
1454
  admin
1305
1455
  .command("proposals")
@@ -4209,6 +4209,21 @@ var ApiError = class extends Error {
4209
4209
  this.name = "ApiError";
4210
4210
  }
4211
4211
  };
4212
+ async function collectAdminPages(fetchPage, startCursor) {
4213
+ const results = [];
4214
+ let cursor = startCursor;
4215
+ let scanned = 0;
4216
+ let pages = 0;
4217
+ for (; ; ) {
4218
+ const page = await fetchPage(cursor);
4219
+ results.push(...page.results);
4220
+ scanned += page.scanned ?? page.results.length;
4221
+ pages += 1;
4222
+ if (page.isDone || !page.nextCursor) break;
4223
+ cursor = page.nextCursor;
4224
+ }
4225
+ return { results, total: results.length, scanned, isDone: true, nextCursor: null, truncated: false, pages };
4226
+ }
4212
4227
  var DayOfWeekClient = class {
4213
4228
  baseUrl;
4214
4229
  token;
@@ -4427,12 +4442,43 @@ var DayOfWeekClient = class {
4427
4442
  const params = new URLSearchParams();
4428
4443
  if (opts?.org) params.set("org", opts.org);
4429
4444
  if (opts?.type) params.set("type", opts.type);
4445
+ if (opts?.category) params.set("category", opts.category);
4446
+ if (opts?.role) params.set("role", opts.role);
4447
+ if (opts?.withRoles) params.set("with-roles", "1");
4430
4448
  if (opts?.missingLocation) params.set("missing-location", "1");
4431
4449
  if (opts?.search) params.set("search", opts.search);
4432
4450
  if (opts?.limit) params.set("limit", String(opts.limit));
4451
+ if (opts?.cursor) params.set("cursor", opts.cursor);
4433
4452
  const qs = params.toString();
4434
4453
  return this.get(`/admin/entities${qs ? `?${qs}` : ""}`);
4435
4454
  }
4455
+ // The market intelligence register: businesses recorded for research and
4456
+ // prospecting, each with coordinates — a separate table from the entity
4457
+ // hierarchy, so a platform-wide count has to read both.
4458
+ async adminListMarketIntel(opts) {
4459
+ const params = new URLSearchParams();
4460
+ if (opts?.org) params.set("org", opts.org);
4461
+ if (opts?.type) params.set("type", opts.type);
4462
+ if (opts?.relationship) params.set("relationship", opts.relationship);
4463
+ if (opts?.priority) params.set("priority", opts.priority);
4464
+ if (opts?.stage) params.set("stage", opts.stage);
4465
+ if (opts?.region) params.set("region", opts.region);
4466
+ if (opts?.search) params.set("search", opts.search);
4467
+ if (opts?.bbox) params.set("bbox", opts.bbox);
4468
+ if (opts?.active !== void 0) params.set("active", opts.active ? "1" : "0");
4469
+ if (opts?.limit) params.set("limit", String(opts.limit));
4470
+ if (opts?.cursor) params.set("cursor", opts.cursor);
4471
+ const qs = params.toString();
4472
+ return this.get(`/admin/market-intel${qs ? `?${qs}` : ""}`);
4473
+ }
4474
+ /** One register record in full, with its latest interactions. */
4475
+ async adminGetMarketIntel(id) {
4476
+ return this.get(`/admin/market-intel/${encodeURIComponent(id)}`);
4477
+ }
4478
+ /** Counts over the whole register — by type, relationship, priority, stage, region, country. */
4479
+ async adminMarketIntelStats(org) {
4480
+ return this.get(`/admin/market-intel/stats${org ? `?org=${encodeURIComponent(org)}` : ""}`);
4481
+ }
4436
4482
  async adminListProposals(opts) {
4437
4483
  const params = new URLSearchParams();
4438
4484
  if (opts?.status) params.set("status", opts.status);
@@ -4472,9 +4518,19 @@ var DayOfWeekClient = class {
4472
4518
  async listBrainSources(areaId) {
4473
4519
  return this.get(`/brain/sources?area=${encodeURIComponent(areaId)}`);
4474
4520
  }
4475
- /** An area's actors (people and organizations, the Actors tab). Active only. */
4476
- async listBrainActors(areaId) {
4477
- return this.get(`/brain/actors?area=${encodeURIComponent(areaId)}`);
4521
+ /**
4522
+ * An area's actors (people and organizations, the Actors tab). Active only.
4523
+ * With `match`, admins also get `candidates` on unlinked actors — the
4524
+ * platform entities whose name matches — to review before linking.
4525
+ */
4526
+ async listBrainActors(areaId, opts) {
4527
+ const params = new URLSearchParams({ area: areaId });
4528
+ if (opts?.match) params.set("match", "1");
4529
+ return this.get(`/brain/actors?${params.toString()}`);
4530
+ }
4531
+ /** Link an actor to the platform entity it is; `entityId: null` unlinks. */
4532
+ async linkBrainActor(input) {
4533
+ return this.post("/brain/actors/link", input);
4478
4534
  }
4479
4535
  /**
4480
4536
  * Create an actor in an area. Idempotent on (area, name): an existing active
@@ -4518,6 +4574,27 @@ var DayOfWeekClient = class {
4518
4574
  async scopeBrainSource(sourceId, actorId) {
4519
4575
  return this.patch(`/brain/sources/${encodeURIComponent(sourceId)}/actor`, { actorId });
4520
4576
  }
4577
+ /** The area's tag vocabulary: predefined hierarchical set + user-defined. */
4578
+ async listBrainTags(areaId) {
4579
+ return this.get(`/brain/tags?area=${encodeURIComponent(areaId)}`);
4580
+ }
4581
+ /** Add a user-defined tag to an area's vocabulary. Idempotent on the key. */
4582
+ async createBrainTag(input) {
4583
+ return this.post("/brain/tags", input);
4584
+ }
4585
+ /** Tag/actor couplings, filterable by actor, tag, source or note. */
4586
+ async listBrainCouplings(input) {
4587
+ const params = new URLSearchParams({ area: input.areaId });
4588
+ if (input.tag) params.set("tag", input.tag);
4589
+ if (input.actor) params.set("actor", input.actor);
4590
+ if (input.source) params.set("source", input.source);
4591
+ if (input.note) params.set("note", input.note);
4592
+ return this.get(`/brain/tags/couplings?${params.toString()}`);
4593
+ }
4594
+ /** Couple (or uncouple, with remove) a source/note to a tag or an actor. */
4595
+ async coupleBrainResource(input) {
4596
+ return this.post("/brain/tags/couplings", input);
4597
+ }
4521
4598
  /** Entities with an active customer role and no investor/partner/producer role. */
4522
4599
  async adminCustomersOnly() {
4523
4600
  return this.get("/admin/customers-only");
@@ -5144,7 +5221,7 @@ var import_promises4 = require("node:readline/promises");
5144
5221
  // package.json
5145
5222
  var package_default = {
5146
5223
  name: "@dayofweek/dcli",
5147
- version: "1.9.1",
5224
+ version: "1.11.0",
5148
5225
  description: "CLI for the Day of Week AgTech platform \u2014 read data and submit proposals for review",
5149
5226
  license: "MIT",
5150
5227
  type: "module",
@@ -5475,8 +5552,21 @@ brainSource.command("download <uri>").description("Download exact original bytes
5475
5552
  }));
5476
5553
  });
5477
5554
  var brainActors = brain.command("actors").description("Work with an area's actors (people and organizations)");
5478
- brainActors.command("list").description("List an area's actors (active, sorted by name)").requiredOption("--area <areaId>", "Area id (from `brain list`)").action(async (opts) => {
5479
- output(await getClient().listBrainActors(opts.area));
5555
+ brainActors.command("list").description("List an area's actors (active, sorted by name)").requiredOption("--area <areaId>", "Area id (from `brain list`)").option("--match", "Suggest matching platform entities for unlinked actors (admins)").action(async (opts) => {
5556
+ output(await getClient().listBrainActors(opts.area, { match: opts.match }));
5557
+ });
5558
+ brainActors.command("link").description("Link an actor to the platform entity it is (or --unlink)").requiredOption("--area <areaId>", "Area id (from `brain list`)").requiredOption("--actor <actorId>", "Actor id (from `brain actors list`)").option("--entity <entityId>", "Platform entity id (from `admin entities` or `read entities`)").option("--unlink", "Remove the current link").action(async (opts) => {
5559
+ if (!opts.entity && !opts.unlink) {
5560
+ throw new Error("Provide --entity <entityId> to link, or --unlink to remove the link");
5561
+ }
5562
+ if (opts.entity && opts.unlink) {
5563
+ throw new Error("--entity and --unlink are mutually exclusive");
5564
+ }
5565
+ output(await getClient().linkBrainActor({
5566
+ areaId: opts.area,
5567
+ actorId: opts.actor,
5568
+ entityId: opts.unlink ? null : opts.entity
5569
+ }));
5480
5570
  });
5481
5571
  brainActors.command("add").description("Add an actor to an area (idempotent on name)").requiredOption("--area <areaId>", "Area id (from `brain list`)").requiredOption("--name <name>", "Actor name (person or organization)").option("--kind <kind>", "person | organization", "organization").option("--role <text>", "Role or relationship in the project").option("--description <text>", "Longer free-text description").action(async (opts) => {
5482
5572
  if (opts.kind !== "person" && opts.kind !== "organization") {
@@ -5532,6 +5622,42 @@ brainSource.command("scope <sourceId>").description("Scope a source to an actor
5532
5622
  }
5533
5623
  output(await getClient().scopeBrainSource(sourceId, opts.actor ?? null));
5534
5624
  });
5625
+ var brainTags = brain.command("tags").description("Work with an area's tag vocabulary and resource couplings");
5626
+ brainTags.command("list").description("List an area's tags (predefined hierarchical set + user-defined)").requiredOption("--area <areaId>", "Area id (from `brain list`)").action(async (opts) => {
5627
+ output(await getClient().listBrainTags(opts.area));
5628
+ });
5629
+ brainTags.command("add").description("Add a user-defined tag (idempotent on the derived key)").requiredOption("--area <areaId>", "Area id (from `brain list`)").requiredOption("--label <label>", "Tag label").option("--parent <key>", "Parent tag key (makes this a child tag)").option("--description <text>", "What the tag means").action(async (opts) => {
5630
+ output(await getClient().createBrainTag({
5631
+ areaId: opts.area,
5632
+ label: opts.label,
5633
+ parentKey: opts.parent,
5634
+ description: opts.description
5635
+ }));
5636
+ });
5637
+ brainTags.command("couplings").description("List tag/actor couplings \u2014 filter by actor, tag, source or note").requiredOption("--area <areaId>", "Area id (from `brain list`)").option("--tag <key>", "Resources coupled to this tag").option("--actor <actorId>", "Resources coupled to this actor").option("--source <sourceId>", "One source's tags and actors").option("--note <noteId>", "One note/page's tags and actors").action(async (opts) => {
5638
+ output(await getClient().listBrainCouplings({
5639
+ areaId: opts.area,
5640
+ tag: opts.tag,
5641
+ actor: opts.actor,
5642
+ source: opts.source,
5643
+ note: opts.note
5644
+ }));
5645
+ });
5646
+ brainTags.command("couple").description("Couple a source/note to a tag or an actor (--remove uncouples)").option("--source <sourceId>", "Source to couple").option("--note <noteId>", "Note/page to couple").option("--tag <key>", "Tag key (from `brain tags list`)").option("--actor <actorId>", "Actor id (from `brain actors list`)").option("--remove", "Remove the coupling instead of adding it").action(async (opts) => {
5647
+ if (Boolean(opts.source) === Boolean(opts.note)) {
5648
+ throw new Error("Pass exactly one of --source <sourceId> or --note <noteId>");
5649
+ }
5650
+ if (Boolean(opts.tag) === Boolean(opts.actor)) {
5651
+ throw new Error("Pass exactly one of --tag <key> or --actor <actorId>");
5652
+ }
5653
+ output(await getClient().coupleBrainResource({
5654
+ sourceId: opts.source,
5655
+ noteId: opts.note,
5656
+ tagKey: opts.tag,
5657
+ actorId: opts.actor,
5658
+ remove: opts.remove
5659
+ }));
5660
+ });
5535
5661
  auth.command("devices").description("List your agent tokens").action(async () => {
5536
5662
  const client = getClient();
5537
5663
  const devices = await client.listDevices();
@@ -6097,16 +6223,47 @@ function registerAdminCommands() {
6097
6223
  const cfg = loadConfig();
6098
6224
  if (!cfg.isAdmin) return;
6099
6225
  const admin = program2.command("admin", { hidden: true }).description("Admin-only cross-org operations (DoW staff)");
6100
- admin.command("entities").description("List entities across all orgs with admin filters").option("--type <entityType>", "Filter by entity type").option("--missing-location", "Only entities lacking metadata.places[].lat/lng").option("--search <query>", "Substring match on name").option("--org <slug>", "Restrict to a single org slug or ID").option("--limit <count>", "Max results (default 200)", parseInt).action(async (opts) => {
6226
+ admin.command("entities").description("List entities across all orgs with admin filters (cursor-paged)").option("--type <entityType>", "Filter by type name (farm, producer, restaurant, \u2026)").option("--category <category>", "Filter by type category (supply, demand, partner, financial, \u2026)").option("--role <role>", "Only entities holding an active role: customer, partner, investor, producer, market_intel").option("--with-roles", "Attach each entity's active roles").option("--missing-location", "Only entities lacking metadata.places[].lat/lng").option("--search <query>", "Substring match on name").option("--org <slug>", "Restrict to a single org slug or ID").option("--limit <count>", "Rows scanned per page (default 200, max 2000)", parseInt).option("--cursor <cursor>", "Continue from a previous page's nextCursor").option("--all", "Walk every page and return all matches").action(async (opts) => {
6101
6227
  const client = getClient();
6102
- const result = await client.adminListEntities({
6228
+ const fetchPage = (cursor) => client.adminListEntities({
6103
6229
  org: opts.org,
6104
6230
  type: opts.type,
6231
+ category: opts.category,
6232
+ role: opts.role,
6233
+ withRoles: opts.withRoles,
6105
6234
  missingLocation: opts.missingLocation,
6106
6235
  search: opts.search,
6107
- limit: opts.limit
6236
+ limit: opts.limit,
6237
+ cursor
6108
6238
  });
6109
- output(result);
6239
+ output(opts.all ? await collectAdminPages(fetchPage, opts.cursor) : await fetchPage(opts.cursor));
6240
+ });
6241
+ const marketIntel = admin.command("market-intel").description("The market intelligence register \u2014 businesses recorded for research and prospecting");
6242
+ marketIntel.command("list").description("List register rows (cursor-paged, newest first)").option("--type <entityType>", "farm, restaurant, farm_shop, retail_store, distributor, processor, market, cooperative, supplier, customer, competitor, other").option("--relationship <type>", "prospect, active_customer, supplier, partner, competitor, market_research, other").option("--priority <priority>", "critical, high, medium, low, watching").option("--stage <stage>", "Opportunity stage (research, qualified_lead, negotiation, \u2026)").option("--region <region>", "Exact match on region (case-insensitive)").option("--search <query>", "Substring match on name").option("--bbox <latMin,lngMin,latMax,lngMax>", "Only rows inside a lat/lng box").option("--active", "Only active rows").option("--inactive", "Only inactive rows").option("--org <slug>", "Restrict to the owning org").option("--limit <count>", "Rows scanned per page (default 200, max 2000)", parseInt).option("--cursor <cursor>", "Continue from a previous page's nextCursor").option("--all", "Walk every page and return all matches").action(async (opts) => {
6243
+ if (opts.active && opts.inactive) {
6244
+ throw new Error("--active and --inactive are mutually exclusive");
6245
+ }
6246
+ const client = getClient();
6247
+ const fetchPage = (cursor) => client.adminListMarketIntel({
6248
+ org: opts.org,
6249
+ type: opts.type,
6250
+ relationship: opts.relationship,
6251
+ priority: opts.priority,
6252
+ stage: opts.stage,
6253
+ region: opts.region,
6254
+ search: opts.search,
6255
+ bbox: opts.bbox,
6256
+ active: opts.active ? true : opts.inactive ? false : void 0,
6257
+ limit: opts.limit,
6258
+ cursor
6259
+ });
6260
+ output(opts.all ? await collectAdminPages(fetchPage, opts.cursor) : await fetchPage(opts.cursor));
6261
+ });
6262
+ marketIntel.command("get <id>").description("One register record with its latest interactions").action(async (id) => {
6263
+ output(await getClient().adminGetMarketIntel(id));
6264
+ });
6265
+ marketIntel.command("stats").description("Counts by type, relationship, priority, stage, region and country").option("--org <slug>", "Restrict to the owning org").action(async (opts) => {
6266
+ output(await getClient().adminMarketIntelStats(opts.org));
6110
6267
  });
6111
6268
  admin.command("proposals").description("List agent proposals across all orgs").option("--status <status>", "Filter: pending, approved, rejected, failed").option("--source-agent <name>", "Filter by sourceAgent identifier").option("--limit <count>", "Max results (default 100)", parseInt).action(async (opts) => {
6112
6269
  const client = getClient();
package/dist/client.d.ts CHANGED
@@ -13,6 +13,27 @@ export declare class ApiError extends Error {
13
13
  requestId?: string | undefined;
14
14
  constructor(status: number, message: string, code?: string | undefined, requestId?: string | undefined);
15
15
  }
16
+ /**
17
+ * One page of a cursor-paged admin read. Filters apply within the scanned
18
+ * page, so `results` can be shorter than the page size — even empty — while
19
+ * `isDone` is still false. `nextCursor` is the only way forward.
20
+ */
21
+ export type AdminPage<T = any> = {
22
+ results: T[];
23
+ total: number;
24
+ scanned: number;
25
+ isDone: boolean;
26
+ nextCursor: string | null;
27
+ truncated: boolean;
28
+ };
29
+ /**
30
+ * Walk a cursor-paged admin read to the end and return every match as one
31
+ * page. This is the loop behind `--all`; without it every caller re-invents
32
+ * it, or reaches for a giant `--limit` and still misses rows.
33
+ */
34
+ export declare function collectAdminPages<T>(fetchPage: (cursor?: string) => Promise<AdminPage<T>>, startCursor?: string): Promise<AdminPage<T> & {
35
+ pages: number;
36
+ }>;
16
37
  export declare class DayOfWeekClient {
17
38
  private baseUrl;
18
39
  private token;
@@ -146,14 +167,31 @@ export declare class DayOfWeekClient {
146
167
  adminListEntities(opts?: {
147
168
  org?: string;
148
169
  type?: string;
170
+ category?: string;
171
+ role?: string;
172
+ withRoles?: boolean;
149
173
  missingLocation?: boolean;
150
174
  search?: string;
151
175
  limit?: number;
152
- }): Promise<{
153
- results: any[];
154
- truncated: boolean;
155
- total: number;
156
- }>;
176
+ cursor?: string;
177
+ }): Promise<AdminPage>;
178
+ adminListMarketIntel(opts?: {
179
+ org?: string;
180
+ type?: string;
181
+ relationship?: string;
182
+ priority?: string;
183
+ stage?: string;
184
+ region?: string;
185
+ search?: string;
186
+ bbox?: string;
187
+ active?: boolean;
188
+ limit?: number;
189
+ cursor?: string;
190
+ }): Promise<AdminPage>;
191
+ /** One register record in full, with its latest interactions. */
192
+ adminGetMarketIntel(id: string): Promise<any>;
193
+ /** Counts over the whole register — by type, relationship, priority, stage, region, country. */
194
+ adminMarketIntelStats(org?: string): Promise<any>;
157
195
  adminListProposals(opts?: {
158
196
  status?: string;
159
197
  sourceAgent?: string;
@@ -199,8 +237,20 @@ export declare class DayOfWeekClient {
199
237
  }): Promise<any>;
200
238
  /** Area sources, metadata only, newest first — the freshness evidence. */
201
239
  listBrainSources(areaId: string): Promise<any>;
202
- /** An area's actors (people and organizations, the Actors tab). Active only. */
203
- listBrainActors(areaId: string): Promise<any>;
240
+ /**
241
+ * An area's actors (people and organizations, the Actors tab). Active only.
242
+ * With `match`, admins also get `candidates` on unlinked actors — the
243
+ * platform entities whose name matches — to review before linking.
244
+ */
245
+ listBrainActors(areaId: string, opts?: {
246
+ match?: boolean;
247
+ }): Promise<any>;
248
+ /** Link an actor to the platform entity it is; `entityId: null` unlinks. */
249
+ linkBrainActor(input: {
250
+ areaId: string;
251
+ actorId: string;
252
+ entityId: string | null;
253
+ }): Promise<any>;
204
254
  /**
205
255
  * Create an actor in an area. Idempotent on (area, name): an existing active
206
256
  * actor comes back with `created: false` instead of a duplicate, so imports
@@ -251,6 +301,31 @@ export declare class DayOfWeekClient {
251
301
  }): Promise<any>;
252
302
  /** Scope a source to an actor (actorId null returns it to area level). */
253
303
  scopeBrainSource(sourceId: string, actorId: string | null): Promise<any>;
304
+ /** The area's tag vocabulary: predefined hierarchical set + user-defined. */
305
+ listBrainTags(areaId: string): Promise<any>;
306
+ /** Add a user-defined tag to an area's vocabulary. Idempotent on the key. */
307
+ createBrainTag(input: {
308
+ areaId: string;
309
+ label: string;
310
+ parentKey?: string;
311
+ description?: string;
312
+ }): Promise<any>;
313
+ /** Tag/actor couplings, filterable by actor, tag, source or note. */
314
+ listBrainCouplings(input: {
315
+ areaId: string;
316
+ tag?: string;
317
+ actor?: string;
318
+ source?: string;
319
+ note?: string;
320
+ }): Promise<any>;
321
+ /** Couple (or uncouple, with remove) a source/note to a tag or an actor. */
322
+ coupleBrainResource(input: {
323
+ sourceId?: string;
324
+ noteId?: string;
325
+ tagKey?: string;
326
+ actorId?: string;
327
+ remove?: boolean;
328
+ }): Promise<any>;
254
329
  /** Entities with an active customer role and no investor/partner/producer role. */
255
330
  adminCustomersOnly(): Promise<any>;
256
331
  /** Tips mailed to press@mail.dayofweek.com, read by the press-scan skill. */
package/dist/client.js CHANGED
@@ -29,6 +29,27 @@ export class ApiError extends Error {
29
29
  this.name = "ApiError";
30
30
  }
31
31
  }
32
+ /**
33
+ * Walk a cursor-paged admin read to the end and return every match as one
34
+ * page. This is the loop behind `--all`; without it every caller re-invents
35
+ * it, or reaches for a giant `--limit` and still misses rows.
36
+ */
37
+ export async function collectAdminPages(fetchPage, startCursor) {
38
+ const results = [];
39
+ let cursor = startCursor;
40
+ let scanned = 0;
41
+ let pages = 0;
42
+ for (;;) {
43
+ const page = await fetchPage(cursor);
44
+ results.push(...page.results);
45
+ scanned += page.scanned ?? page.results.length;
46
+ pages += 1;
47
+ if (page.isDone || !page.nextCursor)
48
+ break;
49
+ cursor = page.nextCursor;
50
+ }
51
+ return { results, total: results.length, scanned, isDone: true, nextCursor: null, truncated: false, pages };
52
+ }
32
53
  export class DayOfWeekClient {
33
54
  baseUrl;
34
55
  token;
@@ -280,15 +301,61 @@ export class DayOfWeekClient {
280
301
  params.set("org", opts.org);
281
302
  if (opts?.type)
282
303
  params.set("type", opts.type);
304
+ if (opts?.category)
305
+ params.set("category", opts.category);
306
+ if (opts?.role)
307
+ params.set("role", opts.role);
308
+ if (opts?.withRoles)
309
+ params.set("with-roles", "1");
283
310
  if (opts?.missingLocation)
284
311
  params.set("missing-location", "1");
285
312
  if (opts?.search)
286
313
  params.set("search", opts.search);
287
314
  if (opts?.limit)
288
315
  params.set("limit", String(opts.limit));
316
+ if (opts?.cursor)
317
+ params.set("cursor", opts.cursor);
289
318
  const qs = params.toString();
290
319
  return this.get(`/admin/entities${qs ? `?${qs}` : ""}`);
291
320
  }
321
+ // The market intelligence register: businesses recorded for research and
322
+ // prospecting, each with coordinates — a separate table from the entity
323
+ // hierarchy, so a platform-wide count has to read both.
324
+ async adminListMarketIntel(opts) {
325
+ const params = new URLSearchParams();
326
+ if (opts?.org)
327
+ params.set("org", opts.org);
328
+ if (opts?.type)
329
+ params.set("type", opts.type);
330
+ if (opts?.relationship)
331
+ params.set("relationship", opts.relationship);
332
+ if (opts?.priority)
333
+ params.set("priority", opts.priority);
334
+ if (opts?.stage)
335
+ params.set("stage", opts.stage);
336
+ if (opts?.region)
337
+ params.set("region", opts.region);
338
+ if (opts?.search)
339
+ params.set("search", opts.search);
340
+ if (opts?.bbox)
341
+ params.set("bbox", opts.bbox);
342
+ if (opts?.active !== undefined)
343
+ params.set("active", opts.active ? "1" : "0");
344
+ if (opts?.limit)
345
+ params.set("limit", String(opts.limit));
346
+ if (opts?.cursor)
347
+ params.set("cursor", opts.cursor);
348
+ const qs = params.toString();
349
+ return this.get(`/admin/market-intel${qs ? `?${qs}` : ""}`);
350
+ }
351
+ /** One register record in full, with its latest interactions. */
352
+ async adminGetMarketIntel(id) {
353
+ return this.get(`/admin/market-intel/${encodeURIComponent(id)}`);
354
+ }
355
+ /** Counts over the whole register — by type, relationship, priority, stage, region, country. */
356
+ async adminMarketIntelStats(org) {
357
+ return this.get(`/admin/market-intel/stats${org ? `?org=${encodeURIComponent(org)}` : ""}`);
358
+ }
292
359
  async adminListProposals(opts) {
293
360
  const params = new URLSearchParams();
294
361
  if (opts?.status)
@@ -332,9 +399,20 @@ export class DayOfWeekClient {
332
399
  async listBrainSources(areaId) {
333
400
  return this.get(`/brain/sources?area=${encodeURIComponent(areaId)}`);
334
401
  }
335
- /** An area's actors (people and organizations, the Actors tab). Active only. */
336
- async listBrainActors(areaId) {
337
- return this.get(`/brain/actors?area=${encodeURIComponent(areaId)}`);
402
+ /**
403
+ * An area's actors (people and organizations, the Actors tab). Active only.
404
+ * With `match`, admins also get `candidates` on unlinked actors — the
405
+ * platform entities whose name matches — to review before linking.
406
+ */
407
+ async listBrainActors(areaId, opts) {
408
+ const params = new URLSearchParams({ area: areaId });
409
+ if (opts?.match)
410
+ params.set("match", "1");
411
+ return this.get(`/brain/actors?${params.toString()}`);
412
+ }
413
+ /** Link an actor to the platform entity it is; `entityId: null` unlinks. */
414
+ async linkBrainActor(input) {
415
+ return this.post("/brain/actors/link", input);
338
416
  }
339
417
  /**
340
418
  * Create an actor in an area. Idempotent on (area, name): an existing active
@@ -378,6 +456,31 @@ export class DayOfWeekClient {
378
456
  async scopeBrainSource(sourceId, actorId) {
379
457
  return this.patch(`/brain/sources/${encodeURIComponent(sourceId)}/actor`, { actorId });
380
458
  }
459
+ /** The area's tag vocabulary: predefined hierarchical set + user-defined. */
460
+ async listBrainTags(areaId) {
461
+ return this.get(`/brain/tags?area=${encodeURIComponent(areaId)}`);
462
+ }
463
+ /** Add a user-defined tag to an area's vocabulary. Idempotent on the key. */
464
+ async createBrainTag(input) {
465
+ return this.post("/brain/tags", input);
466
+ }
467
+ /** Tag/actor couplings, filterable by actor, tag, source or note. */
468
+ async listBrainCouplings(input) {
469
+ const params = new URLSearchParams({ area: input.areaId });
470
+ if (input.tag)
471
+ params.set("tag", input.tag);
472
+ if (input.actor)
473
+ params.set("actor", input.actor);
474
+ if (input.source)
475
+ params.set("source", input.source);
476
+ if (input.note)
477
+ params.set("note", input.note);
478
+ return this.get(`/brain/tags/couplings?${params.toString()}`);
479
+ }
480
+ /** Couple (or uncouple, with remove) a source/note to a tag or an actor. */
481
+ async coupleBrainResource(input) {
482
+ return this.post("/brain/tags/couplings", input);
483
+ }
381
484
  /** Entities with an active customer role and no investor/partner/producer role. */
382
485
  async adminCustomersOnly() {
383
486
  return this.get("/admin/customers-only");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dayofweek/dcli",
3
- "version": "1.9.1",
4
- "description": "CLI for the Day of Week AgTech platform \u2014 read data and submit proposals for review",
3
+ "version": "1.11.0",
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",
7
7
  "bin": {