borgmcp-shared 0.2.2

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 (83) hide show
  1. package/CONTRIBUTING.md +40 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +2 -0
  4. package/README.md +159 -0
  5. package/SECURITY.md +25 -0
  6. package/dist/conformance/adapter.d.ts +112 -0
  7. package/dist/conformance/adapter.d.ts.map +1 -0
  8. package/dist/conformance/adapter.js +417 -0
  9. package/dist/conformance/adapter.js.map +1 -0
  10. package/dist/conformance/index.d.ts +15 -0
  11. package/dist/conformance/index.d.ts.map +1 -0
  12. package/dist/conformance/index.js +49 -0
  13. package/dist/conformance/index.js.map +1 -0
  14. package/dist/domain/index.d.ts +5 -0
  15. package/dist/domain/index.d.ts.map +1 -0
  16. package/dist/domain/index.js +4 -0
  17. package/dist/domain/index.js.map +1 -0
  18. package/dist/drone-address.d.ts +3 -0
  19. package/dist/drone-address.d.ts.map +1 -0
  20. package/dist/drone-address.js +7 -0
  21. package/dist/drone-address.js.map +1 -0
  22. package/dist/index.d.ts +5 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +5 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/log-stream-hwm.d.ts +6 -0
  27. package/dist/log-stream-hwm.d.ts.map +1 -0
  28. package/dist/log-stream-hwm.js +12 -0
  29. package/dist/log-stream-hwm.js.map +1 -0
  30. package/dist/protocol/contract.d.ts +127 -0
  31. package/dist/protocol/contract.d.ts.map +1 -0
  32. package/dist/protocol/contract.js +434 -0
  33. package/dist/protocol/contract.js.map +1 -0
  34. package/dist/protocol/coordination.d.ts +38 -0
  35. package/dist/protocol/coordination.d.ts.map +1 -0
  36. package/dist/protocol/coordination.js +176 -0
  37. package/dist/protocol/coordination.js.map +1 -0
  38. package/dist/protocol/errors.d.ts +33 -0
  39. package/dist/protocol/errors.d.ts.map +1 -0
  40. package/dist/protocol/errors.js +26 -0
  41. package/dist/protocol/errors.js.map +1 -0
  42. package/dist/protocol/index.d.ts +8 -0
  43. package/dist/protocol/index.d.ts.map +1 -0
  44. package/dist/protocol/index.js +7 -0
  45. package/dist/protocol/index.js.map +1 -0
  46. package/dist/protocol/sse.d.ts +44 -0
  47. package/dist/protocol/sse.d.ts.map +1 -0
  48. package/dist/protocol/sse.js +246 -0
  49. package/dist/protocol/sse.js.map +1 -0
  50. package/dist/protocol/types.d.ts +174 -0
  51. package/dist/protocol/types.d.ts.map +1 -0
  52. package/dist/protocol/types.js +2 -0
  53. package/dist/protocol/types.js.map +1 -0
  54. package/dist/protocol/version.d.ts +11 -0
  55. package/dist/protocol/version.d.ts.map +1 -0
  56. package/dist/protocol/version.js +14 -0
  57. package/dist/protocol/version.js.map +1 -0
  58. package/dist/role-section.d.ts +24 -0
  59. package/dist/role-section.d.ts.map +1 -0
  60. package/dist/role-section.js +114 -0
  61. package/dist/role-section.js.map +1 -0
  62. package/dist/templates.d.ts +44 -0
  63. package/dist/templates.d.ts.map +1 -0
  64. package/dist/templates.js +771 -0
  65. package/dist/templates.js.map +1 -0
  66. package/docs/compatibility.md +38 -0
  67. package/docs/releasing.md +220 -0
  68. package/package.json +89 -0
  69. package/src/conformance/adapter.ts +717 -0
  70. package/src/conformance/index.ts +68 -0
  71. package/src/domain/index.ts +9 -0
  72. package/src/drone-address.ts +25 -0
  73. package/src/index.ts +4 -0
  74. package/src/log-stream-hwm.ts +16 -0
  75. package/src/protocol/contract.ts +608 -0
  76. package/src/protocol/coordination.ts +249 -0
  77. package/src/protocol/errors.ts +36 -0
  78. package/src/protocol/index.ts +7 -0
  79. package/src/protocol/sse.ts +295 -0
  80. package/src/protocol/types.ts +201 -0
  81. package/src/protocol/version.ts +31 -0
  82. package/src/role-section.ts +270 -0
  83. package/src/templates.ts +953 -0
@@ -0,0 +1,201 @@
1
+ import type { MessageTaxonomy } from '../templates.js';
2
+
3
+ export type AgentKind = 'claude' | 'codex' | 'opencode';
4
+ export type RoleClass = 'queen' | 'worker';
5
+ export type LogVisibility = 'broadcast' | 'direct';
6
+ export type AckKind = 'ack' | 'claim';
7
+ export type WakePath = 'live' | 'degraded' | 'deaf';
8
+ export type WakePathAlertClass =
9
+ | 'dead'
10
+ | 'post-blocked'
11
+ | 'presumed-dead'
12
+ | 'systemic-post-block'
13
+ | 'wake-path-deaf'
14
+ | 'systemic-wake-path-deaf'
15
+ | 'independent';
16
+
17
+ export interface Cube {
18
+ id: string;
19
+ owner_id: string;
20
+ name: string;
21
+ cube_directive: string;
22
+ created_at: string;
23
+ updated_at: string;
24
+ message_taxonomy?: MessageTaxonomy | null;
25
+ /** Added to orientation responses; optional when reading older servers. */
26
+ directive_hash?: string;
27
+ }
28
+
29
+ export interface Role {
30
+ id: string;
31
+ cube_id: string;
32
+ name: string;
33
+ short_description: string;
34
+ detailed_description: string;
35
+ is_default: boolean;
36
+ /** Additive fields are optional for compatibility with older servers. */
37
+ is_mandatory?: boolean;
38
+ is_human_seat: boolean;
39
+ can_broadcast?: boolean;
40
+ receives_all_direct?: boolean;
41
+ role_class?: RoleClass;
42
+ created_at: string;
43
+ detailed_description_hash?: string;
44
+ }
45
+
46
+ export type PublicRole = Omit<Role, 'detailed_description' | 'detailed_description_hash'>;
47
+
48
+ export interface Drone {
49
+ id: string;
50
+ cube_id: string;
51
+ role_id: string;
52
+ label: string;
53
+ is_queen_class?: boolean;
54
+ last_seen: string;
55
+ last_log_post?: string | null;
56
+ hostname: string | null;
57
+ last_regen_at?: string | null;
58
+ regen_count?: number | string;
59
+ last_read_log_at?: string | null;
60
+ last_event_received_at?: string | null;
61
+ wake_path_client_sse_connected?: boolean | null;
62
+ wake_path_client_monitor_armed?: boolean | null;
63
+ wake_path_alert_class?: WakePathAlertClass | null;
64
+ agent_kind?: AgentKind | null;
65
+ reported_model?: string | null;
66
+ working_repo_name?: string | null;
67
+ working_repo_origin?: string | null;
68
+ evicted_at?: null;
69
+ created_at: string;
70
+ seen_since?: boolean;
71
+ }
72
+
73
+ export interface RosterDrone extends Drone {
74
+ behind_by: number;
75
+ wake_path: WakePath;
76
+ wake_path_deaf_streak?: number;
77
+ wake_path_last_challenge_at?: string | null;
78
+ wake_path_last_alert_at?: string | null;
79
+ }
80
+
81
+ export interface RegenIdentityDrone extends Drone {
82
+ frozen_at?: null;
83
+ }
84
+
85
+ export interface ActivityLogEntry {
86
+ id: string;
87
+ cube_id: string;
88
+ drone_id: string | null;
89
+ message: string;
90
+ visibility: LogVisibility;
91
+ created_at: string;
92
+ }
93
+
94
+ export interface EnrichedStreamEntry extends ActivityLogEntry {
95
+ drone_label: string | null;
96
+ role_name: string | null;
97
+ recipient_drone_ids: string[];
98
+ }
99
+
100
+ export interface AssimilateRequest {
101
+ cube_id?: string;
102
+ cube_name?: string;
103
+ role_id?: string;
104
+ role_name?: string;
105
+ prior_drone_id?: string;
106
+ hostname?: string | null;
107
+ agent_kind?: AgentKind | null;
108
+ }
109
+
110
+ export interface AssimilateResponse {
111
+ cube: Cube;
112
+ role: Role;
113
+ drone: Drone;
114
+ sessionToken: string;
115
+ reattached?: boolean;
116
+ }
117
+
118
+ export interface WhoAmIResponse {
119
+ cube_id: string;
120
+ cube_name: string;
121
+ drone_id: string;
122
+ drone_label: string;
123
+ role_id: string;
124
+ role_name: string;
125
+ }
126
+
127
+ export interface RosterResponse {
128
+ drones: RosterDrone[];
129
+ roles: PublicRole[];
130
+ message_taxonomy?: MessageTaxonomy | null;
131
+ since?: string | null;
132
+ }
133
+
134
+ export interface ReadLogClaim {
135
+ log_entry_id: string;
136
+ claimant_drone_id: string;
137
+ claimant_label: string | null;
138
+ claimant_role: string | null;
139
+ claimed_at: string;
140
+ stale: boolean;
141
+ }
142
+
143
+ export interface ReadLogResponse {
144
+ entries: ActivityLogEntry[];
145
+ drones: Drone[];
146
+ roles: PublicRole[];
147
+ behind_by?: number;
148
+ has_more?: boolean;
149
+ /** Added with advisory claims; optional when reading older servers. */
150
+ claims?: ReadLogClaim[];
151
+ }
152
+
153
+ export interface RoutingEcho {
154
+ class: string | null;
155
+ recipients: string[];
156
+ fellOpen: boolean;
157
+ message: string | null;
158
+ }
159
+
160
+ export interface AppendLogRequest {
161
+ message: string;
162
+ visibility?: LogVisibility;
163
+ recipientDroneIds?: string[];
164
+ class?: string;
165
+ to?: string[];
166
+ }
167
+
168
+ export interface AppendLogResponse {
169
+ entry: ActivityLogEntry;
170
+ routing?: RoutingEcho | null;
171
+ unreachableRecipients?: Array<{ id: string; label: string }>;
172
+ }
173
+
174
+ export interface Decision {
175
+ id: string;
176
+ cube_id: string;
177
+ topic: string;
178
+ decision: string;
179
+ rationale: string | null;
180
+ ratified_by?: string | null;
181
+ status?: 'active' | 'superseded' | 'removed';
182
+ supersedes?: string | null;
183
+ created_at: string;
184
+ }
185
+
186
+ export interface RecordDecisionRequest {
187
+ topic: string;
188
+ decision: string;
189
+ rationale?: string;
190
+ }
191
+
192
+ export interface RegenResponse {
193
+ cube: Cube;
194
+ role: Role;
195
+ drone: RegenIdentityDrone;
196
+ roles: PublicRole[];
197
+ drones: Drone[];
198
+ decisions?: Decision[];
199
+ recentLog?: ActivityLogEntry[];
200
+ behind_by?: number;
201
+ }
@@ -0,0 +1,31 @@
1
+ /** Current Borg coordination protocol generation. */
2
+ export const PROTOCOL_VERSION = '1' as const;
3
+
4
+ /** Protocol generations accepted by this package release. */
5
+ export const SUPPORTED_PROTOCOL_VERSIONS = [PROTOCOL_VERSION] as const;
6
+
7
+ export type ProtocolVersion = (typeof SUPPORTED_PROTOCOL_VERSIONS)[number];
8
+
9
+ export interface CompatibilityEntry {
10
+ packageRange: string;
11
+ protocolVersions: readonly ProtocolVersion[];
12
+ notes: string;
13
+ }
14
+
15
+ /**
16
+ * Compatibility table for published package releases. Pre-1.0 package
17
+ * releases may add contracts, but do not change an existing wire shape without
18
+ * a documented migration path.
19
+ */
20
+ export const COMPATIBILITY_MATRIX: readonly CompatibilityEntry[] = [
21
+ {
22
+ packageRange: '>=0.2.0 <0.3.0',
23
+ protocolVersions: SUPPORTED_PROTOCOL_VERSIONS,
24
+ notes: 'Versioned envelope, codecs, and adapter conformance for Borg MCP servers.',
25
+ },
26
+ ];
27
+
28
+ export function isProtocolVersionSupported(value: unknown): value is ProtocolVersion {
29
+ return typeof value === 'string' &&
30
+ (SUPPORTED_PROTOCOL_VERSIONS as readonly string[]).includes(value);
31
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Role-text section parsing and granular section patching.
3
+ *
4
+ * This module remains platform-dependency-free so clients, servers, and tests
5
+ * all use one canonical parser without importing runtime or storage adapters.
6
+ *
7
+ * ## Why not markdown `##` headings
8
+ *
9
+ * Real role `detailed_description` text (see `client/src/templates.ts`)
10
+ * does NOT use `##` headings. It is structured as:
11
+ *
12
+ * - a PREAMBLE — free prose before the first label line
13
+ * (e.g. "You implement changes to the codebase…");
14
+ * - one or more SECTIONS, each headed by a PLAIN-LABEL LINE — a line
15
+ * that is a short label terminated by a single trailing colon and
16
+ * nothing else (e.g. `Workflow:`, `Project conventions:`,
17
+ * `Your job:`, `Log conventions you use:`);
18
+ * - TRAILING inlined `${ESCALATION_DISCIPLINE}` / `${...}` constant
19
+ * text woven onto the end of the field. That text uses `**bold:**`
20
+ * markdown emphasis, NOT bare label lines, so it is NOT a section
21
+ * boundary — it falls into the last label section as literal text.
22
+ *
23
+ * A parser that splits on `##` finds ZERO sections in real role text.
24
+ * The label-line delimiter below matches the ACTUAL structure.
25
+ *
26
+ * ## Byte-identical round-trip invariant
27
+ *
28
+ * `serializeSections(parseRoleSections(text)) === text` for ANY input.
29
+ * The parser does not trim, normalize newlines, or otherwise rewrite
30
+ * bytes — each section's `body` retains the exact slice (including its
31
+ * heading line and the newline that terminates it) so concatenation
32
+ * reproduces the original field verbatim. The single named-fragment
33
+ * mutation is therefore the ONLY byte-level change a patch makes.
34
+ */
35
+
36
+ /** What kind of label heads a section. */
37
+ export type RoleSectionKind = 'preamble' | 'label';
38
+
39
+ export interface RoleSection {
40
+ /**
41
+ * The section's label WITHOUT the trailing colon, trimmed
42
+ * (e.g. "Workflow", "Project conventions"). `null` for the preamble
43
+ * (the pre-first-label span). Used as the addressing key for patches.
44
+ */
45
+ heading: string | null;
46
+ kind: RoleSectionKind;
47
+ /**
48
+ * The EXACT source slice for this section, including its heading line
49
+ * (for label sections) and every byte through to (but not including)
50
+ * the next section's heading line. Concatenating every section's
51
+ * `body` in order reproduces the original text byte-for-byte.
52
+ */
53
+ body: string;
54
+ }
55
+
56
+ export type RoleSectionPatchOp =
57
+ | { action: 'replace'; heading: string; body: string }
58
+ | { action: 'insert'; heading: string; body: string; after?: string | null }
59
+ | { action: 'delete'; heading: string };
60
+
61
+ /**
62
+ * Test whether a single line is a plain-label section heading.
63
+ *
64
+ * Rules (deliberately narrow to avoid false positives against prose
65
+ * that merely contains a colon):
66
+ * - no leading whitespace (label lines start at column 0);
67
+ * - the line ends with exactly one `:`;
68
+ * - the text before the colon is non-empty, contains no other `:`,
69
+ * and is "short" (≤ 60 chars) — long colon-terminated prose lines
70
+ * are not labels;
71
+ * - the label is not a markdown emphasis run (does not start with
72
+ * `*`, `-`, `#`, `>` or backtick) — those are list/markdown lines,
73
+ * not plain labels (the woven `**Dense communication discipline:**`
74
+ * constant headings must NOT be treated as section boundaries).
75
+ */
76
+ export function isLabelLine(line: string): boolean {
77
+ // Leading whitespace disqualifies (label lines are flush-left).
78
+ if (/^\s/.test(line)) return false;
79
+ if (!line.endsWith(':')) return false;
80
+ const label = line.slice(0, -1);
81
+ if (label.length === 0) return false;
82
+ if (label.length > 60) return false;
83
+ // Exactly one colon total (the trailing one).
84
+ if (label.includes(':')) return false;
85
+ // Markdown/list/quote/code lead-ins are not plain labels.
86
+ if (/^[*\-#>`]/.test(label)) return false;
87
+ return true;
88
+ }
89
+
90
+ /**
91
+ * Parse role `detailed_description` text into ordered sections.
92
+ *
93
+ * The result always round-trips byte-identical through
94
+ * `serializeSections`. An empty string yields a single empty preamble
95
+ * section so callers always have a stable shape to address.
96
+ */
97
+ export function parseRoleSections(text: string): RoleSection[] {
98
+ const sections: RoleSection[] = [];
99
+ const lines = text.split('\n');
100
+
101
+ // Reconstruct each line WITH its terminating newline so that the
102
+ // concatenation is lossless. `split('\n')` drops the separators; we
103
+ // re-append '\n' to every line except the last (which had no trailing
104
+ // newline in the source unless the source ended with one — in which
105
+ // case the final split element is '' and contributes nothing).
106
+ const lineWithSep = (idx: number): string =>
107
+ idx < lines.length - 1 ? lines[idx] + '\n' : lines[idx];
108
+
109
+ let currentHeading: string | null = null;
110
+ let currentKind: RoleSectionKind = 'preamble';
111
+ let currentBody = '';
112
+ let started = false;
113
+
114
+ const flush = () => {
115
+ sections.push({ heading: currentHeading, kind: currentKind, body: currentBody });
116
+ };
117
+
118
+ for (let i = 0; i < lines.length; i++) {
119
+ const raw = lines[i];
120
+ if (isLabelLine(raw)) {
121
+ // Close the in-progress section (preamble or previous label) and
122
+ // open a new label section headed by this line.
123
+ flush();
124
+ currentHeading = raw.slice(0, -1).trim();
125
+ currentKind = 'label';
126
+ currentBody = lineWithSep(i);
127
+ started = true;
128
+ } else {
129
+ currentBody += lineWithSep(i);
130
+ started = true;
131
+ }
132
+ }
133
+
134
+ // Always emit the final in-progress section. For empty input this is a
135
+ // single empty preamble (started === false, currentBody === '').
136
+ if (started || sections.length === 0) {
137
+ flush();
138
+ }
139
+
140
+ return sections;
141
+ }
142
+
143
+ /** Concatenate sections back into the original field text. */
144
+ export function serializeSections(sections: RoleSection[]): string {
145
+ return sections.map((s) => s.body).join('');
146
+ }
147
+
148
+ function normalizeHeading(value: string): string {
149
+ return value.trim().toLowerCase();
150
+ }
151
+
152
+ /**
153
+ * Build the body for a freshly inserted/replaced label section from a
154
+ * heading + caller-supplied body text.
155
+ *
156
+ * The heading line is rendered as `<heading>:` on its own line. The
157
+ * caller's `body` is the text BELOW the heading. We guarantee the
158
+ * section body ends with a single trailing newline so adjacent sections
159
+ * stay separated when re-serialized (matching how real role text
160
+ * separates label sections with blank lines is the caller's choice via
161
+ * their body text; we only ensure the structural newline).
162
+ */
163
+ function renderLabelSection(heading: string, body: string): string {
164
+ const headingLine = `${heading.trim()}:\n`;
165
+ if (body === '') return headingLine;
166
+ // Ensure the body ends with a newline so the next section's heading
167
+ // starts on its own line.
168
+ const normalizedBody = body.endsWith('\n') ? body : body + '\n';
169
+ return headingLine + normalizedBody;
170
+ }
171
+
172
+ /**
173
+ * Guarantee the section at `idx` ends in a newline before a new section
174
+ * heading is glued on after it.
175
+ *
176
+ * Real role `detailed_description` text (every template role) ends WITHOUT
177
+ * a trailing newline — it closes on the woven `${...DISCIPLINE}` constant.
178
+ * Appending a `<heading>:` line directly onto such text would fuse the
179
+ * heading onto the prior section's last line, so the heading is no longer
180
+ * a flush-left label and the inserted section is LOST on re-parse. Adding
181
+ * the structural newline here keeps the inserted heading on its own line.
182
+ *
183
+ * No-ops when the preceding body is empty (an empty preamble needs no
184
+ * separator) or already ends in '\n' (preserves the already-correct case
185
+ * byte-for-byte).
186
+ */
187
+ function ensureTrailingNewline(sections: RoleSection[], idx: number): void {
188
+ if (idx < 0 || idx >= sections.length) return;
189
+ const prev = sections[idx];
190
+ if (prev.body !== '' && !prev.body.endsWith('\n')) {
191
+ sections[idx] = { ...prev, body: prev.body + '\n' };
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Apply a single section-level patch to role text, returning the new
197
+ * full field text. Every other byte is preserved.
198
+ *
199
+ * - replace: overwrite the matched section's body with a re-rendered
200
+ * `<heading>:\n<body>` block. The heading match is case-insensitive
201
+ * on the label; the re-rendered heading uses the supplied casing.
202
+ * - insert: add a new label section. Placed after the section named by
203
+ * `after` (case-insensitive), or appended at the end when `after` is
204
+ * omitted/null. Rejects if a section with the same heading already
205
+ * exists (use replace instead).
206
+ * - delete: drop the matched section entirely.
207
+ *
208
+ * Throws when the target section is missing (replace/delete) or already
209
+ * present (insert), or when `after` does not resolve.
210
+ */
211
+ export function patchRoleSectionText(text: string, op: RoleSectionPatchOp): string {
212
+ const sections = parseRoleSections(text);
213
+ const targetKey = normalizeHeading(op.heading);
214
+
215
+ if (op.action === 'replace') {
216
+ const idx = sections.findIndex(
217
+ (s) => s.kind === 'label' && s.heading != null && normalizeHeading(s.heading) === targetKey
218
+ );
219
+ if (idx === -1) {
220
+ throw new Error(`Role section "${op.heading}" not found. Use action="insert" to add it.`);
221
+ }
222
+ sections[idx] = {
223
+ heading: op.heading.trim(),
224
+ kind: 'label',
225
+ body: renderLabelSection(op.heading, op.body),
226
+ };
227
+ return serializeSections(sections);
228
+ }
229
+
230
+ if (op.action === 'delete') {
231
+ const idx = sections.findIndex(
232
+ (s) => s.kind === 'label' && s.heading != null && normalizeHeading(s.heading) === targetKey
233
+ );
234
+ if (idx === -1) {
235
+ throw new Error(`Role section "${op.heading}" not found.`);
236
+ }
237
+ sections.splice(idx, 1);
238
+ return serializeSections(sections);
239
+ }
240
+
241
+ // insert
242
+ const exists = sections.some(
243
+ (s) => s.kind === 'label' && s.heading != null && normalizeHeading(s.heading) === targetKey
244
+ );
245
+ if (exists) {
246
+ throw new Error(`Role section "${op.heading}" already exists. Use action="replace" to overwrite it.`);
247
+ }
248
+ const newSection: RoleSection = {
249
+ heading: op.heading.trim(),
250
+ kind: 'label',
251
+ body: renderLabelSection(op.heading, op.body),
252
+ };
253
+
254
+ if (op.after == null) {
255
+ ensureTrailingNewline(sections, sections.length - 1);
256
+ sections.push(newSection);
257
+ return serializeSections(sections);
258
+ }
259
+
260
+ const afterKey = normalizeHeading(op.after);
261
+ const afterIdx = sections.findIndex(
262
+ (s) => s.kind === 'label' && s.heading != null && normalizeHeading(s.heading) === afterKey
263
+ );
264
+ if (afterIdx === -1) {
265
+ throw new Error(`Cannot insert after section "${op.after}" — it does not exist.`);
266
+ }
267
+ ensureTrailingNewline(sections, afterIdx);
268
+ sections.splice(afterIdx + 1, 0, newSection);
269
+ return serializeSections(sections);
270
+ }