borgmcp 3.17.0 → 4.0.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.
Files changed (51) hide show
  1. package/README.md +10 -0
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/dist/cli-help.d.ts.map +1 -1
  4. package/dist/cli-help.js +4 -1
  5. package/dist/cli-help.js.map +1 -1
  6. package/dist/direct-log.d.ts +2 -1
  7. package/dist/direct-log.d.ts.map +1 -1
  8. package/dist/direct-log.js +23 -9
  9. package/dist/direct-log.js.map +1 -1
  10. package/dist/docs-sections.js +3 -3
  11. package/dist/docs-sections.js.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +33 -13
  14. package/dist/index.js.map +1 -1
  15. package/dist/log-audit-core.d.ts.map +1 -1
  16. package/dist/log-audit-core.js +4 -2
  17. package/dist/log-audit-core.js.map +1 -1
  18. package/dist/regen-format.d.ts +6 -2
  19. package/dist/regen-format.d.ts.map +1 -1
  20. package/dist/regen-format.js +32 -13
  21. package/dist/regen-format.js.map +1 -1
  22. package/dist/remote-client.d.ts +10 -5
  23. package/dist/remote-client.d.ts.map +1 -1
  24. package/dist/remote-client.js +20 -33
  25. package/dist/remote-client.js.map +1 -1
  26. package/dist/server-handshake.d.ts +1 -1
  27. package/dist/tool-manifest.d.ts.map +1 -1
  28. package/dist/tool-manifest.js +40 -25
  29. package/dist/tool-manifest.js.map +1 -1
  30. package/dist/tool-scope.d.ts +1 -1
  31. package/dist/tool-scope.d.ts.map +1 -1
  32. package/dist/tool-scope.js +1 -0
  33. package/dist/tool-scope.js.map +1 -1
  34. package/docs/DOCUMENTS.md +4 -2
  35. package/docs/RELEASING.md +4 -2
  36. package/package.json +2 -2
  37. package/src/cli-help.ts +4 -1
  38. package/src/direct-log.ts +25 -8
  39. package/src/docs-sections.ts +3 -3
  40. package/src/index.ts +38 -12
  41. package/src/log-audit-core.ts +4 -2
  42. package/src/regen-format.ts +34 -13
  43. package/src/remote-client.ts +48 -54
  44. package/src/server-handshake.ts +1 -1
  45. package/src/tool-manifest.ts +41 -25
  46. package/src/tool-scope.ts +1 -0
  47. package/dist/local-log-routing.d.ts +0 -19
  48. package/dist/local-log-routing.d.ts.map +0 -1
  49. package/dist/local-log-routing.js +0 -67
  50. package/dist/local-log-routing.js.map +0 -1
  51. package/src/local-log-routing.ts +0 -106
@@ -208,7 +208,8 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
208
208
  'this reads oldest-unread-first from your server cursor and ' +
209
209
  'advances the watermark so bursts are not skipped. Optional `since` is a strict-after ' +
210
210
  'cursor for explicit bounded reads only; do not use it with the same timestamp as a ' +
211
- 'notification preview because it can skip the boundary entry.',
211
+ 'notification preview because it can skip the boundary entry. Use `borg_read-entry` ' +
212
+ 'to fetch one known entry without changing the unread cursor.',
212
213
  inputSchema: {
213
214
  type: 'object',
214
215
  properties: {
@@ -229,6 +230,22 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
229
230
  },
230
231
  },
231
232
  },
233
+ {
234
+ name: 'borg_read-entry',
235
+ description:
236
+ 'Read one complete activity-log entry by its canonical UUID or unique 8-hex prefix without changing the unread cursor. Returns the same entry shape and structured routing recipients as borg_read-log. Use borg_read-log unread_only=true for routine wake drains. Direct routing controls delivery and wakes, not read confidentiality inside the cube.',
237
+ inputSchema: {
238
+ type: 'object',
239
+ properties: {
240
+ entry_id: {
241
+ type: 'string',
242
+ pattern: '^(?:[0-9A-Fa-f]{8}|[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[1-8][0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12})$',
243
+ description: 'Canonical activity-log entry UUID or unique 8-hex prefix.',
244
+ },
245
+ },
246
+ required: ['entry_id'],
247
+ },
248
+ },
232
249
  {
233
250
  name: 'borg_ack',
234
251
  description:
@@ -385,26 +402,29 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
385
402
  {
386
403
  name: 'borg_log',
387
404
  description:
388
- 'Append a message to the cube\'s activity log. By default entries broadcast to all drones. Cite durable cube documents by passing their full ids in `documents`; citations carry current metadata but do not inline document content. When a cube declares a message taxonomy, borg_log applies class-based smart defaults: prefix-matched directed classes route to their default recipients unless you pass `to:`, `class`, or explicit visibility. Pass `to: [...]` to direct by exact drone label, drone id, the 8-hex short-uuid (the `id:` token shown in roster/read-log a drone_id prefix that is STABLE across label renumber), role name, or role slug.',
405
+ 'Append a message to the cube\'s activity log with an explicit audience. Every call must pass `to: "broadcast"` for all drones or a non-empty selector array for direct delivery. Selectors accept an exact drone label, drone id, the stable 8-hex `id:` token shown in roster/read-log, role name, or role slug. Message text and taxonomy classes never choose the audience; optional `class` records classification/lifecycle metadata only. Cite durable cube documents through `documents`; citations carry current metadata but do not inline document content. Direct routing controls delivery and wakes, not read confidentiality inside the cube.',
389
406
  inputSchema: {
390
407
  type: 'object',
391
408
  properties: {
392
409
  message: { type: 'string', description: 'The log message (max 10KB).' },
393
410
  to: {
394
- type: 'array',
395
- items: { type: 'string' },
411
+ oneOf: [
412
+ { type: 'string', enum: ['broadcast'] },
413
+ {
414
+ type: 'array',
415
+ items: { type: 'string', minLength: 1, maxLength: 120 },
416
+ minItems: 1,
417
+ maxItems: 100,
418
+ uniqueItems: true,
419
+ },
420
+ ],
396
421
  description:
397
- 'Optional direct-message recipients by exact drone label, drone id, the 8-hex short-uuid (`id:` token from roster/read-log; stable across label renumber), role name, or role slug (resolves to all drones in that role). Omit to let class-based routing or broadcast defaults apply.',
422
+ 'Required explicit audience: "broadcast" for every drone, or a non-empty array of exact drone labels, drone ids, stable 8-hex `id:` tokens, role names, or role slugs.',
398
423
  },
399
424
  class: {
400
425
  type: 'string',
401
426
  description:
402
- 'Optional declared message class. Overrides prefix auto-classification when the cube declares a message taxonomy.',
403
- },
404
- visibility: {
405
- type: 'string',
406
- enum: ['broadcast', 'direct'],
407
- description: 'Optional explicit visibility. Overrides class-based routing defaults.',
427
+ 'Optional declared message class for classification/lifecycle metadata. It never changes the required `to` audience.',
408
428
  },
409
429
  documents: {
410
430
  type: 'array',
@@ -415,7 +435,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
415
435
  description: 'Optional full opaque ids of 1-100 same-cube documents to cite atomically. Unknown, duplicate, or foreign ids are refused.',
416
436
  },
417
437
  },
418
- required: ['message'],
438
+ required: ['message', 'to'],
419
439
  },
420
440
  },
421
441
  {
@@ -456,14 +476,12 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
456
476
  cube_directive: { type: 'string', description: 'New cube directive markdown (optional).' },
457
477
  message_taxonomy: {
458
478
  type: 'array',
459
- description: 'New message-class taxonomy (optional). REPLACES the whole taxonomy; the worker re-validates the full array (non-overlapping prefixes, unique class names, directed classes need default_to). Pass [] to clear. To change ONE class without resending the whole array, use borg_patch-taxonomy-class instead. In default_to, pass @human-seat to route to drones in the cube human-seat role(s); literal role names/slugs/labels still work. Optional lifecycle tags mark dispatch/completion classes for stuck-dispatch detection.',
479
+ description: 'New classification/lifecycle taxonomy (optional). REPLACES the whole taxonomy; the server re-validates non-overlapping prefixes and unique class names. Pass [] to clear. To change ONE class without resending the whole array, use borg_patch-taxonomy-class instead. Optional lifecycle tags mark dispatch/completion classes for stuck-dispatch detection. Taxonomy never chooses log recipients.',
460
480
  items: {
461
481
  type: 'object',
462
482
  properties: {
463
483
  class: { type: 'string', description: 'Unique class name.' },
464
- prefixes: { type: 'array', items: { type: 'string' }, description: 'Message prefixes routed by this class.' },
465
- routing: { type: 'string', enum: ['broadcast', 'directed'], description: 'Routing mode.' },
466
- default_to: { type: 'array', items: { type: 'string' }, description: 'Default recipients (role name/slug/label, or @human-seat) for a directed class.' },
484
+ prefixes: { type: 'array', items: { type: 'string' }, description: 'Message prefixes classified by this class.' },
467
485
  lifecycle: { type: 'string', enum: ['dispatch', 'completion'], description: 'Optional lifecycle marker for stuck-dispatch detection.' },
468
486
  },
469
487
  },
@@ -475,7 +493,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
475
493
  {
476
494
  name: 'borg_patch-taxonomy-class',
477
495
  description:
478
- "Patch ONE message-class in a cube's message_taxonomy without resending the whole taxonomy (avoids clobbering). action=add|replace|remove (replace/remove match name case-insensitively). The full taxonomy is re-validated after the patch (non-overlapping prefixes, unique names, directed classes need default_to) — a patch that breaks a rule against an untouched class is rejected. In default_to, @human-seat routes to the cube's human-seat role(s); literal names/slugs/labels also work. Optional lifecycle tags mark dispatch/completion classes for stuck-dispatch detection.",
496
+ "Patch ONE classification/lifecycle class in a cube's message_taxonomy without resending the whole taxonomy (avoids clobbering). action=add|replace|remove (replace/remove match name case-insensitively). The full taxonomy is re-validated for non-overlapping prefixes and unique names. Optional lifecycle tags mark dispatch/completion classes for stuck-dispatch detection. Taxonomy never chooses log recipients.",
479
497
  inputSchema: {
480
498
  type: 'object',
481
499
  properties: {
@@ -483,15 +501,13 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
483
501
  action: { type: 'string', enum: ['add', 'replace', 'remove'], description: 'add / replace / remove a single class.' },
484
502
  class_def: {
485
503
  type: 'object',
486
- description: 'The class definition (for add/replace). Shape: { class, prefixes?, routing: "broadcast"|"directed", default_to?, lifecycle? }.',
504
+ description: 'The class definition (for add/replace). Shape: { class, prefixes?, lifecycle? }.',
487
505
  properties: {
488
506
  class: { type: 'string', description: 'Unique class name.' },
489
- prefixes: { type: 'array', items: { type: 'string' }, description: 'Message prefixes routed by this class.' },
490
- routing: { type: 'string', enum: ['broadcast', 'directed'], description: 'Routing mode.' },
491
- default_to: { type: 'array', items: { type: 'string' }, description: 'Default recipients (required for directed classes): role name/slug/label, or @human-seat.' },
507
+ prefixes: { type: 'array', items: { type: 'string' }, description: 'Message prefixes classified by this class.' },
492
508
  lifecycle: { type: 'string', enum: ['dispatch', 'completion'], description: 'Optional lifecycle marker for stuck-dispatch detection.' },
493
509
  },
494
- required: ['class', 'routing'],
510
+ required: ['class'],
495
511
  },
496
512
  class: { type: 'string', description: 'For remove only: the name of the class to drop (case-insensitive).' },
497
513
  },
@@ -524,7 +540,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
524
540
  is_mandatory: { type: 'boolean', description: 'If true, role-less assimilation prioritizes this unoccupied role before ordinary worker roles. Platform-wide management roles are never auto-assigned; a mandatory human-operator role is selected first until occupied.' },
525
541
  is_human_seat: { type: 'boolean', description: 'If true, this role represents the cube\'s human-occupied seat (where the human Queen sits directly). The class-hierarchy guard in reassign-drone allows promotion FROM a human-seat role TO the platform Queen role; promotion from non-human-seat roles is rejected.' },
526
542
  can_broadcast: { type: 'boolean', description: 'If true, drones in this role may post broadcast log entries when strict broadcast gating is enabled.' },
527
- receives_all_direct: { type: 'boolean', description: 'If true, drones in this role can see direct log entries as observer/audit recipients.' },
543
+ receives_all_direct: { type: 'boolean', description: 'If true, drones in this role are included as observer/audit recipients for every direct route.' },
528
544
  },
529
545
  required: ['cube_id', 'name', 'short_description', 'detailed_description'],
530
546
  },
@@ -543,7 +559,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
543
559
  is_mandatory: { type: 'boolean', description: 'Set true/false to prioritize this unoccupied role during role-less assimilation. Platform-wide management roles are never auto-assigned.' },
544
560
  is_human_seat: { type: 'boolean', description: 'Set true/false to mark/unmark this as the cube\'s human-occupied seat (the elevation source for the platform Queen role).' },
545
561
  can_broadcast: { type: 'boolean', description: 'Set true/false to allow or deny broadcast log entries when strict broadcast gating is enabled.' },
546
- receives_all_direct: { type: 'boolean', description: 'Set true/false to grant or remove observer visibility into direct log entries.' },
562
+ receives_all_direct: { type: 'boolean', description: 'Set true/false to include or remove this role as observer/audit recipients for every direct route.' },
547
563
  },
548
564
  required: ['role_id'],
549
565
  },
@@ -566,7 +582,7 @@ export const TOOL_MANIFEST: ToolManifestEntry[] = [
566
582
  },
567
583
  {
568
584
  name: 'borg_delete-role',
569
- description: 'Delete a role using the selected local client\'s cube-management grant. Unknown or inaccessible roles refuse. Also refuses for the default, mandatory, or human-seat role; a role referenced by message-taxonomy routing; or a role assigned to an active drone. Reassign active drones with borg_reassign-drone or remove them with borg_evict-drone first. Evicted drones that held the deleted role are reassigned to the cube\'s default role; their activity-log attribution is unaffected.',
585
+ description: 'Delete a role using the selected local client\'s cube-management grant. Unknown or inaccessible roles refuse. Also refuses for the default, mandatory, or human-seat role, or a role assigned to an active drone. Reassign active drones with borg_reassign-drone or remove them with borg_evict-drone first. Evicted drones that held the deleted role are reassigned to the cube\'s default role; their activity-log attribution is unaffected.',
570
586
  inputSchema: {
571
587
  type: 'object',
572
588
  properties: {
package/src/tool-scope.ts CHANGED
@@ -20,6 +20,7 @@ export const UNIVERSAL_TOOLS = [
20
20
  'borg_regen',
21
21
  'borg_log',
22
22
  'borg_read-log',
23
+ 'borg_read-entry',
23
24
  'borg_roster',
24
25
  'borg_stream-status',
25
26
  'borg_whoami',
@@ -1,19 +0,0 @@
1
- export interface LocalRoutingDrone {
2
- id: string;
3
- label?: string | null;
4
- role_id?: string | null;
5
- }
6
- export interface LocalRoutingRole {
7
- id: string;
8
- name: string;
9
- is_human_seat?: boolean;
10
- }
11
- /**
12
- * Resolve the public `borg_log to:` addressing forms against the local
13
- * server's authoritative cube roster. This intentionally mirrors the hosted
14
- * route resolver: exact drone id/label first, then displayed short UUID, then
15
- * role name/slug expansion. Every failure is closed before the log POST so a
16
- * miss can never degrade into a broadcast.
17
- */
18
- export declare function resolveLocalLogRecipients(recipients: readonly string[], drones: readonly LocalRoutingDrone[], roles: readonly LocalRoutingRole[]): string[];
19
- //# sourceMappingURL=local-log-routing.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"local-log-routing.d.ts","sourceRoot":"","sources":["../src/local-log-routing.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,MAAM,EAAE,SAAS,iBAAiB,EAAE,EACpC,KAAK,EAAE,SAAS,gBAAgB,EAAE,GACjC,MAAM,EAAE,CAgFV"}
@@ -1,67 +0,0 @@
1
- import { roleSlug } from './role-resolver.js';
2
- /**
3
- * Resolve the public `borg_log to:` addressing forms against the local
4
- * server's authoritative cube roster. This intentionally mirrors the hosted
5
- * route resolver: exact drone id/label first, then displayed short UUID, then
6
- * role name/slug expansion. Every failure is closed before the log POST so a
7
- * miss can never degrade into a broadcast.
8
- */
9
- export function resolveLocalLogRecipients(recipients, drones, roles) {
10
- const ids = new Set();
11
- for (const recipient of recipients) {
12
- if (recipient === '@human-seat') {
13
- const roleIds = new Set(roles.filter((role) => role.is_human_seat === true).map((role) => role.id));
14
- const matches = drones.filter((drone) => drone.role_id !== null && drone.role_id !== undefined && roleIds.has(drone.role_id));
15
- if (roleIds.size === 0 || matches.length === 0) {
16
- throw new Error('Direct-message role recipient has no active drones: @human-seat. ' +
17
- 'Use borg_roster to find active recipients, or omit to broadcast if your role can broadcast.');
18
- }
19
- for (const drone of matches)
20
- ids.add(drone.id);
21
- continue;
22
- }
23
- const droneMatches = drones.filter((drone) => drone.id === recipient || drone.label === recipient);
24
- const uniqueDroneIds = [...new Set(droneMatches.map((drone) => drone.id))];
25
- if (uniqueDroneIds.length > 1) {
26
- throw new Error(`Ambiguous direct-message recipient: ${recipient} matches multiple drones`);
27
- }
28
- if (uniqueDroneIds.length === 1) {
29
- ids.add(uniqueDroneIds[0]);
30
- continue;
31
- }
32
- const shortUuid = recipient.replace(/`/g, '').replace(/^id:/i, '').toLowerCase();
33
- if (/^[0-9a-f]{8,}$/.test(shortUuid)) {
34
- const prefixMatches = drones.filter((drone) => drone.id.toLowerCase().startsWith(shortUuid));
35
- const uniquePrefixIds = [...new Set(prefixMatches.map((drone) => drone.id))];
36
- if (uniquePrefixIds.length > 1) {
37
- const listed = prefixMatches
38
- .map((drone) => `${drone.id} (${drone.label ?? 'unlabeled'})`)
39
- .join(', ');
40
- throw new Error(`Ambiguous short-uuid recipient: ${recipient} matches multiple drones — ${listed}. ` +
41
- 'Address by the full drone id.');
42
- }
43
- if (uniquePrefixIds.length === 1) {
44
- ids.add(uniquePrefixIds[0]);
45
- continue;
46
- }
47
- }
48
- const roleMatches = roles.filter((role) => roleSlug(role.name) === roleSlug(recipient));
49
- const uniqueRoleIds = [...new Set(roleMatches.map((role) => role.id))];
50
- if (uniqueRoleIds.length > 1) {
51
- throw new Error(`Ambiguous direct-message recipient: ${recipient} matches multiple roles`);
52
- }
53
- if (uniqueRoleIds.length === 0) {
54
- throw new Error(`Unknown direct-message recipient: ${recipient}. ` +
55
- 'Use an exact drone label, drone id, role name, or role slug.');
56
- }
57
- const roleDrones = drones.filter((drone) => drone.role_id === uniqueRoleIds[0]);
58
- if (roleDrones.length === 0) {
59
- throw new Error(`Direct-message role recipient has no active drones: ${recipient}. ` +
60
- 'Use borg_roster to find active recipients, or omit to broadcast if your role can broadcast.');
61
- }
62
- for (const drone of roleDrones)
63
- ids.add(drone.id);
64
- }
65
- return [...ids];
66
- }
67
- //# sourceMappingURL=local-log-routing.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"local-log-routing.js","sourceRoot":"","sources":["../src/local-log-routing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAc9C;;;;;;GAMG;AACH,MAAM,UAAU,yBAAyB,CACvC,UAA6B,EAC7B,MAAoC,EACpC,KAAkC;IAElC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAE9B,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,SAAS,KAAK,aAAa,EAAE,CAAC;YAChC,MAAM,OAAO,GAAG,IAAI,GAAG,CACrB,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAC3E,CAAC;YACF,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CACtC,KAAK,CAAC,OAAO,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CACpF,CAAC;YACF,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC/C,MAAM,IAAI,KAAK,CACb,mEAAmE;oBACnE,6FAA6F,CAC9F,CAAC;YACJ,CAAC;YACD,KAAK,MAAM,KAAK,IAAI,OAAO;gBAAE,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAC/C,SAAS;QACX,CAAC;QAED,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAChC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,CAC/D,CAAC;QACF,MAAM,cAAc,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC3E,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CACb,uCAAuC,SAAS,0BAA0B,CAC3E,CAAC;QACJ,CAAC;QACD,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAChC,GAAG,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAE,CAAC,CAAC;YAC5B,SAAS;QACX,CAAC;QAED,MAAM,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QACjF,IAAI,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;YACrC,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAC5C,KAAK,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAC7C,CAAC;YACF,MAAM,eAAe,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAC7E,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC/B,MAAM,MAAM,GAAG,aAAa;qBACzB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,EAAE,KAAK,KAAK,CAAC,KAAK,IAAI,WAAW,GAAG,CAAC;qBAC7D,IAAI,CAAC,IAAI,CAAC,CAAC;gBACd,MAAM,IAAI,KAAK,CACb,mCAAmC,SAAS,8BAA8B,MAAM,IAAI;oBACpF,+BAA+B,CAChC,CAAC;YACJ,CAAC;YACD,IAAI,eAAe,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACjC,GAAG,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAE,CAAC,CAAC;gBAC7B,SAAS;YACX,CAAC;QACH,CAAC;QAED,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;QACxF,MAAM,aAAa,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACvE,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CACb,uCAAuC,SAAS,yBAAyB,CAC1E,CAAC;QACJ,CAAC;QACD,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CACb,qCAAqC,SAAS,IAAI;gBAClD,8DAA8D,CAC/D,CAAC;QACJ,CAAC;QACD,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,KAAK,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;QAChF,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,KAAK,CACb,uDAAuD,SAAS,IAAI;gBACpE,6FAA6F,CAC9F,CAAC;QACJ,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,UAAU;YAAE,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IACpD,CAAC;IAED,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC;AAClB,CAAC"}
@@ -1,106 +0,0 @@
1
- import { roleSlug } from './role-resolver.js';
2
-
3
- export interface LocalRoutingDrone {
4
- id: string;
5
- label?: string | null;
6
- role_id?: string | null;
7
- }
8
-
9
- export interface LocalRoutingRole {
10
- id: string;
11
- name: string;
12
- is_human_seat?: boolean;
13
- }
14
-
15
- /**
16
- * Resolve the public `borg_log to:` addressing forms against the local
17
- * server's authoritative cube roster. This intentionally mirrors the hosted
18
- * route resolver: exact drone id/label first, then displayed short UUID, then
19
- * role name/slug expansion. Every failure is closed before the log POST so a
20
- * miss can never degrade into a broadcast.
21
- */
22
- export function resolveLocalLogRecipients(
23
- recipients: readonly string[],
24
- drones: readonly LocalRoutingDrone[],
25
- roles: readonly LocalRoutingRole[],
26
- ): string[] {
27
- const ids = new Set<string>();
28
-
29
- for (const recipient of recipients) {
30
- if (recipient === '@human-seat') {
31
- const roleIds = new Set(
32
- roles.filter((role) => role.is_human_seat === true).map((role) => role.id),
33
- );
34
- const matches = drones.filter((drone) =>
35
- drone.role_id !== null && drone.role_id !== undefined && roleIds.has(drone.role_id)
36
- );
37
- if (roleIds.size === 0 || matches.length === 0) {
38
- throw new Error(
39
- 'Direct-message role recipient has no active drones: @human-seat. ' +
40
- 'Use borg_roster to find active recipients, or omit to broadcast if your role can broadcast.',
41
- );
42
- }
43
- for (const drone of matches) ids.add(drone.id);
44
- continue;
45
- }
46
-
47
- const droneMatches = drones.filter(
48
- (drone) => drone.id === recipient || drone.label === recipient,
49
- );
50
- const uniqueDroneIds = [...new Set(droneMatches.map((drone) => drone.id))];
51
- if (uniqueDroneIds.length > 1) {
52
- throw new Error(
53
- `Ambiguous direct-message recipient: ${recipient} matches multiple drones`,
54
- );
55
- }
56
- if (uniqueDroneIds.length === 1) {
57
- ids.add(uniqueDroneIds[0]!);
58
- continue;
59
- }
60
-
61
- const shortUuid = recipient.replace(/`/g, '').replace(/^id:/i, '').toLowerCase();
62
- if (/^[0-9a-f]{8,}$/.test(shortUuid)) {
63
- const prefixMatches = drones.filter((drone) =>
64
- drone.id.toLowerCase().startsWith(shortUuid)
65
- );
66
- const uniquePrefixIds = [...new Set(prefixMatches.map((drone) => drone.id))];
67
- if (uniquePrefixIds.length > 1) {
68
- const listed = prefixMatches
69
- .map((drone) => `${drone.id} (${drone.label ?? 'unlabeled'})`)
70
- .join(', ');
71
- throw new Error(
72
- `Ambiguous short-uuid recipient: ${recipient} matches multiple drones — ${listed}. ` +
73
- 'Address by the full drone id.',
74
- );
75
- }
76
- if (uniquePrefixIds.length === 1) {
77
- ids.add(uniquePrefixIds[0]!);
78
- continue;
79
- }
80
- }
81
-
82
- const roleMatches = roles.filter((role) => roleSlug(role.name) === roleSlug(recipient));
83
- const uniqueRoleIds = [...new Set(roleMatches.map((role) => role.id))];
84
- if (uniqueRoleIds.length > 1) {
85
- throw new Error(
86
- `Ambiguous direct-message recipient: ${recipient} matches multiple roles`,
87
- );
88
- }
89
- if (uniqueRoleIds.length === 0) {
90
- throw new Error(
91
- `Unknown direct-message recipient: ${recipient}. ` +
92
- 'Use an exact drone label, drone id, role name, or role slug.',
93
- );
94
- }
95
- const roleDrones = drones.filter((drone) => drone.role_id === uniqueRoleIds[0]);
96
- if (roleDrones.length === 0) {
97
- throw new Error(
98
- `Direct-message role recipient has no active drones: ${recipient}. ` +
99
- 'Use borg_roster to find active recipients, or omit to broadcast if your role can broadcast.',
100
- );
101
- }
102
- for (const drone of roleDrones) ids.add(drone.id);
103
- }
104
-
105
- return [...ids];
106
- }