@dayofweek/dcli 1.10.0 → 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")
@@ -1349,22 +1370,86 @@ function registerAdminCommands() {
1349
1370
  .description("Admin-only cross-org operations (DoW staff)");
1350
1371
  admin
1351
1372
  .command("entities")
1352
- .description("List entities across all orgs with admin filters")
1353
- .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")
1354
1378
  .option("--missing-location", "Only entities lacking metadata.places[].lat/lng")
1355
1379
  .option("--search <query>", "Substring match on name")
1356
1380
  .option("--org <slug>", "Restrict to a single org slug or ID")
1357
- .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")
1358
1384
  .action(async (opts) => {
1359
1385
  const client = getClient();
1360
- const result = await client.adminListEntities({
1386
+ const fetchPage = (cursor) => client.adminListEntities({
1361
1387
  org: opts.org,
1362
1388
  type: opts.type,
1389
+ category: opts.category,
1390
+ role: opts.role,
1391
+ withRoles: opts.withRoles,
1363
1392
  missingLocation: opts.missingLocation,
1364
1393
  search: opts.search,
1365
1394
  limit: opts.limit,
1395
+ cursor,
1366
1396
  });
1367
- 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));
1368
1453
  });
1369
1454
  admin
1370
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
@@ -5165,7 +5221,7 @@ var import_promises4 = require("node:readline/promises");
5165
5221
  // package.json
5166
5222
  var package_default = {
5167
5223
  name: "@dayofweek/dcli",
5168
- version: "1.10.0",
5224
+ version: "1.11.0",
5169
5225
  description: "CLI for the Day of Week AgTech platform \u2014 read data and submit proposals for review",
5170
5226
  license: "MIT",
5171
5227
  type: "module",
@@ -5496,8 +5552,21 @@ brainSource.command("download <uri>").description("Download exact original bytes
5496
5552
  }));
5497
5553
  });
5498
5554
  var brainActors = brain.command("actors").description("Work with an area's actors (people and organizations)");
5499
- brainActors.command("list").description("List an area's actors (active, sorted by name)").requiredOption("--area <areaId>", "Area id (from `brain list`)").action(async (opts) => {
5500
- 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
+ }));
5501
5570
  });
5502
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) => {
5503
5572
  if (opts.kind !== "person" && opts.kind !== "organization") {
@@ -6154,16 +6223,47 @@ function registerAdminCommands() {
6154
6223
  const cfg = loadConfig();
6155
6224
  if (!cfg.isAdmin) return;
6156
6225
  const admin = program2.command("admin", { hidden: true }).description("Admin-only cross-org operations (DoW staff)");
6157
- 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) => {
6158
6227
  const client = getClient();
6159
- const result = await client.adminListEntities({
6228
+ const fetchPage = (cursor) => client.adminListEntities({
6160
6229
  org: opts.org,
6161
6230
  type: opts.type,
6231
+ category: opts.category,
6232
+ role: opts.role,
6233
+ withRoles: opts.withRoles,
6162
6234
  missingLocation: opts.missingLocation,
6163
6235
  search: opts.search,
6164
- limit: opts.limit
6236
+ limit: opts.limit,
6237
+ cursor
6165
6238
  });
6166
- 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));
6167
6267
  });
6168
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) => {
6169
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
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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dayofweek/dcli",
3
- "version": "1.10.0",
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": {