@oxygen-agent/cli 1.1011.322 → 1.1011.405

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 (48) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +7 -0
  3. package/dist/connectors-commands.d.ts +4 -1
  4. package/dist/connectors-commands.js +81 -10
  5. package/dist/contract-flags.d.ts +2 -0
  6. package/dist/contract-flags.js +4 -2
  7. package/dist/crm-support-flags.d.ts +11 -0
  8. package/dist/crm-support-flags.js +60 -0
  9. package/dist/help.js +10 -0
  10. package/dist/index.js +175 -65
  11. package/dist/settings-commands.d.ts +11 -0
  12. package/dist/settings-commands.js +72 -0
  13. package/node_modules/@oxygen/shared/dist/bounded-read-result.d.ts +4 -0
  14. package/node_modules/@oxygen/shared/dist/bounded-read-result.js +25 -0
  15. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +30 -0
  16. package/node_modules/@oxygen/shared/dist/capability-discovery.js +277 -16
  17. package/node_modules/@oxygen/shared/dist/contracts/connectors.d.ts +32 -6
  18. package/node_modules/@oxygen/shared/dist/contracts/connectors.js +56 -4
  19. package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +117 -0
  20. package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +43 -0
  21. package/node_modules/@oxygen/shared/dist/contracts/dashboard-tabs.d.ts +3 -0
  22. package/node_modules/@oxygen/shared/dist/contracts/dashboard-tabs.js +2 -0
  23. package/node_modules/@oxygen/shared/dist/contracts/integration-connect.d.ts +4 -0
  24. package/node_modules/@oxygen/shared/dist/contracts/integration-connect.js +7 -0
  25. package/node_modules/@oxygen/shared/dist/contracts/operations.js +8 -2
  26. package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.d.ts +16 -0
  27. package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.js +37 -0
  28. package/node_modules/@oxygen/shared/dist/decision-ledger-parser.d.ts +32 -0
  29. package/node_modules/@oxygen/shared/dist/decision-ledger-parser.js +303 -0
  30. package/node_modules/@oxygen/shared/dist/decision-ledger.d.ts +10 -0
  31. package/node_modules/@oxygen/shared/dist/decision-ledger.generated.d.ts +2 -0
  32. package/node_modules/@oxygen/shared/dist/decision-ledger.generated.js +717 -0
  33. package/node_modules/@oxygen/shared/dist/decision-ledger.js +20 -0
  34. package/node_modules/@oxygen/shared/dist/mcp-event-contract.d.ts +10 -7
  35. package/node_modules/@oxygen/shared/dist/mcp-event-contract.js +11 -7
  36. package/node_modules/@oxygen/shared/dist/mcp-table-widget.d.ts +50 -0
  37. package/node_modules/@oxygen/shared/dist/mcp-table-widget.js +24 -0
  38. package/node_modules/@oxygen/shared/dist/plan-band.d.ts +8 -0
  39. package/node_modules/@oxygen/shared/dist/plan-band.js +12 -0
  40. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +21 -0
  41. package/node_modules/@oxygen/shared/dist/plan-limits.js +41 -0
  42. package/node_modules/@oxygen/shared/dist/sequences.d.ts +33 -4
  43. package/node_modules/@oxygen/shared/dist/sequences.js +48 -13
  44. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +17 -0
  45. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  46. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  47. package/node_modules/@oxygen/shared/package.json +5 -0
  48. package/package.json +1 -1
@@ -0,0 +1,303 @@
1
+ // Parser for the routing program's decision ledger (docs/gtm-program/decisions.md).
2
+ //
3
+ // Registry seeds cite ledger ids, and staff surfaces explain a provider choice
4
+ // by them, so the ledger is read as data rather than prose. The parser keys on
5
+ // the three table sections and their exact header rows and throws on anything
6
+ // else: a reshaped ledger has to break the generator, not lose decisions.
7
+ //
8
+ // This module imports nothing, not even a sibling, so
9
+ // scripts/docs/generate-decision-ledger.mjs can load it through Node's own type
10
+ // stripping without a build, and so it stays browser-safe. Consumers read the
11
+ // parsed entries through @oxygen/shared/decision-ledger.
12
+ const TABLE_LAYOUTS = [
13
+ { table: "ratified", heading: /^Ratified$/, columns: ["#", "Decision", "Ratified answer", "Shipped / lands"] },
14
+ { table: "open", heading: /^Open$/, columns: ["#", "Decision", "Status", "Owner / forcing function"] },
15
+ { table: "proposed", heading: /^Proposed\b/, columns: ["#", "Decision", "Proposed answer", "Status"] },
16
+ ];
17
+ const DECISION_ID = String.raw `D\d+[a-z]?(?:\([a-z]\))?`;
18
+ const DECISION_ID_LIST = String.raw `${DECISION_ID}(?:(?:\s*,\s*(?:and\s+|or\s+)?|\s*[/&]\s*|\s+(?:and|or)\s+)${DECISION_ID})*`;
19
+ // "Superseded by D26", "Superseded by D55/D56" — this row is replaced.
20
+ const SUPERSEDED_BY = new RegExp(String.raw `\b[Ss]uperseded\s+by\s+(${DECISION_ID_LIST})`, "g");
21
+ // "Supersedes D24(a)", "Supersedes only D19(a)'s clause", "Supersedes the D32 chain".
22
+ const SUPERSEDES = new RegExp(String.raw `\b[Ss]upersedes\s+(only\s+|the\s+)?(${DECISION_ID_LIST})`, "g");
23
+ // What follows a superseded id when only part of it is replaced: a possessive
24
+ // ("D55's company half"), a capacity ("D18(a) as a routing posture") or "in
25
+ // part", optionally after the date the row records ("D29 (2026-07-25) as a …").
26
+ const PARTIAL_SCOPE = /^\s*(?:\([^)]*\)\s*)?(?:['\u2019]s\b|as\s+an?\s|in\s+part\b)/;
27
+ // "Partially superseded by D40": the row itself says only part of it went.
28
+ const PARTIAL_PREFIX = /\b(?:[Pp]artially|[Pp]artly)\s+$/;
29
+ // A decision id with an optional attached letter (D34a) and an optional answer
30
+ // option in parentheses (D19(a)); the option names a clause, not a decision.
31
+ const ID_MENTION = /\bD(\d+)([a-z]?)(?:\([a-z]\))?(?![\w(])/g;
32
+ const ID_RANGE = /\bD(\d+)\s*[–—-]\s*D(\d+)\b/g;
33
+ /** Parse the ledger markdown into entries sorted by decision id. */
34
+ export function parseDecisionLedger(markdown) {
35
+ const lines = markdown.split(/\r?\n/);
36
+ const rows = [];
37
+ const events = [];
38
+ const tablesSeen = new Set();
39
+ let section = null;
40
+ let index = 0;
41
+ while (index < lines.length) {
42
+ const line = lines[index] ?? "";
43
+ const heading = /^##\s+(.+?)\s*#*\s*$/.exec(line);
44
+ if (heading) {
45
+ const title = plainText(heading[1] ?? "");
46
+ section = { title, layout: TABLE_LAYOUTS.find((layout) => layout.heading.test(title)) ?? null, hasTable: false };
47
+ index += 1;
48
+ continue;
49
+ }
50
+ if (/^\s*\|/.test(line)) {
51
+ const where = section ? `"## ${section.title}"` : "the preamble";
52
+ if (!section?.layout) {
53
+ throw new Error(`Decision ledger line ${index + 1}: a table under ${where}. The parser reads only the Ratified, Open and Proposed tables and will not guess what this one holds; extend parseDecisionLedger first.`);
54
+ }
55
+ if (section.hasTable || tablesSeen.has(section.layout.table)) {
56
+ throw new Error(`Decision ledger line ${index + 1}: a second ${section.layout.table} table under ${where}.`);
57
+ }
58
+ section.hasTable = true;
59
+ tablesSeen.add(section.layout.table);
60
+ index = readTable(lines, index, section, section.layout, rows);
61
+ continue;
62
+ }
63
+ if (section === null) {
64
+ // Preamble: "- **2026-06-10 (T0 build):** D7, D11, ...", possibly wrapped.
65
+ const event = /^\s*[-*+]\s+\*\*(\d{4}-\d{2}-\d{2})\b[^*]*\*\*(.*)$/.exec(line);
66
+ if (event) {
67
+ let text = event[2] ?? "";
68
+ while (index + 1 < lines.length && /^\s+\S/.test(lines[index + 1] ?? "") && !/^\s*[-*+]\s/.test(lines[index + 1] ?? "")) {
69
+ index += 1;
70
+ text += ` ${lines[index] ?? ""}`;
71
+ }
72
+ events.push({ date: event[1] ?? "", text });
73
+ }
74
+ }
75
+ index += 1;
76
+ }
77
+ if (rows.length === 0)
78
+ throw new Error("Decision ledger has no Ratified, Open or Proposed table rows.");
79
+ return buildEntries(rows, events);
80
+ }
81
+ function readTable(lines, start, section, layout, rows) {
82
+ const header = splitRow(lines[start] ?? "").map(plainText);
83
+ if (header.join(" | ") !== layout.columns.join(" | ")) {
84
+ throw new Error(`The "## ${section.title}" table (line ${start + 1}) has columns "${header.join(" | ")}"; the parser expects "${layout.columns.join(" | ")}". Update parseDecisionLedger for the new layout rather than letting it guess.`);
85
+ }
86
+ const delimiter = splitRow(lines[start + 1] ?? "");
87
+ if (delimiter.length !== header.length || !delimiter.every((cell) => /^:?-{3,}:?$/.test(cell))) {
88
+ throw new Error(`The "## ${section.title}" table (line ${start + 2}) has no delimiter row under its header.`);
89
+ }
90
+ let index = start + 2;
91
+ while (index < lines.length && /^\s*\|/.test(lines[index] ?? "")) {
92
+ const cells = splitRow(lines[index] ?? "");
93
+ if (cells.length !== header.length) {
94
+ throw new Error(`Decision ledger row ${plainText(cells[0] ?? "") || "?"} in the ${layout.table} table (line ${index + 1}) has ${cells.length} cells; its header has ${header.length}.`);
95
+ }
96
+ rows.push({ table: layout.table, cells, line: index + 1 });
97
+ index += 1;
98
+ }
99
+ return index;
100
+ }
101
+ /** Split a GFM table row on unescaped pipes; `\|` stays escaped for plainText. */
102
+ function splitRow(line) {
103
+ let body = line.trim();
104
+ if (body.startsWith("|"))
105
+ body = body.slice(1);
106
+ if (body.endsWith("|") && !body.endsWith("\\|"))
107
+ body = body.slice(0, -1);
108
+ const cells = [];
109
+ let current = "";
110
+ for (let position = 0; position < body.length; position += 1) {
111
+ const char = body[position];
112
+ if (char === "\\" && body[position + 1] === "|") {
113
+ current += "\\|";
114
+ position += 1;
115
+ }
116
+ else if (char === "|") {
117
+ cells.push(current.trim());
118
+ current = "";
119
+ }
120
+ else {
121
+ current += char;
122
+ }
123
+ }
124
+ cells.push(current.trim());
125
+ return cells;
126
+ }
127
+ /**
128
+ * Markdown inline text to plain text: code spans keep their content verbatim,
129
+ * emphasis and strikethrough markers go, links keep their text, escapes resolve,
130
+ * whitespace collapses. Intraword underscores (find_email) are never emphasis.
131
+ */
132
+ function plainText(markdown) {
133
+ const held = [];
134
+ const hold = (value) => `\u0000${held.push(value) - 1}\u0000`;
135
+ let text = markdown
136
+ // GFM resolves `\|` inside a table cell before any inline parsing, code spans included.
137
+ .replace(/\\\|/g, "|")
138
+ .replace(/(`+)([\s\S]*?[^`])\1(?!`)/g, (_match, _ticks, code) => hold(code.length > 2 && code.startsWith(" ") && code.endsWith(" ") ? code.slice(1, -1) : code))
139
+ .replace(/\\([!-/:-@[-`{-~])/g, (_match, char) => hold(char))
140
+ .replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
141
+ .replace(/<((?:https?|mailto):[^>\s]+)>/g, "$1");
142
+ for (;;) {
143
+ const next = text
144
+ .replace(/\*\*(?=\S)([\s\S]*?\S)\*\*/g, "$1")
145
+ .replace(/(^|\W)__(?=\S)([\s\S]*?\S)__(?!\w)/g, "$1$2")
146
+ .replace(/(^|[^\w*])\*(?=[^\s*])([\s\S]*?[^\s*])\*(?![\w*])/g, "$1$2")
147
+ .replace(/(^|\W)_(?=[^\s_])([\s\S]*?[^\s_])_(?!\w)/g, "$1$2")
148
+ .replace(/~~(?=\S)([\s\S]*?\S)~~/g, "$1");
149
+ if (next === text)
150
+ break;
151
+ text = next;
152
+ }
153
+ return text
154
+ .replace(/\u0000(\d+)\u0000/g, (_match, slot) => held[Number(slot)] ?? "")
155
+ .replace(/\s+/g, " ")
156
+ .trim();
157
+ }
158
+ /** Sentences of plain text; a period followed by a lowercase word ("i.e. we") does not split. */
159
+ function sentences(text) {
160
+ return text.split(/(?<=[.!?])\s+(?=[A-Z0-9("'`])/);
161
+ }
162
+ /** Decision ids mentioned in text, as `D<n>` plus any attached letter; "D19(a)" is "D19". */
163
+ function mentionedIds(text, options = {}) {
164
+ const ids = [];
165
+ if (options.expandRanges) {
166
+ for (const range of text.matchAll(ID_RANGE)) {
167
+ const from = Number(range[1]);
168
+ const to = Number(range[2]);
169
+ if (to < from || to - from > 500)
170
+ continue;
171
+ for (let number = from; number <= to; number += 1)
172
+ ids.push(`D${number}`);
173
+ }
174
+ }
175
+ for (const mention of text.matchAll(ID_MENTION))
176
+ ids.push(`D${mention[1]}${mention[2] ?? ""}`);
177
+ return ids;
178
+ }
179
+ /** Exact id first ("D34a"), then the base id for an option-lettered mention ("D19a" -> "D19"). */
180
+ function resolveId(mention, known) {
181
+ if (known.has(mention))
182
+ return mention;
183
+ const base = /^(D\d+)[a-z]$/.exec(mention)?.[1];
184
+ return base && known.has(base) ? base : null;
185
+ }
186
+ function compareIds(left, right) {
187
+ const a = /^D(\d+)([a-z]?)$/.exec(left);
188
+ const b = /^D(\d+)([a-z]?)$/.exec(right);
189
+ const byNumber = Number(a?.[1] ?? 0) - Number(b?.[1] ?? 0);
190
+ if (byNumber !== 0)
191
+ return byNumber;
192
+ return (a?.[2] ?? "").localeCompare(b?.[2] ?? "");
193
+ }
194
+ function nullIfEmpty(text) {
195
+ return text.length > 0 ? text : null;
196
+ }
197
+ /** The status cell of a Proposed-table row: "Proposed" or "Ratified in chat <date> (<who>); <lands>". */
198
+ function readProposedStatus(id, line, cell) {
199
+ const ratified = /^Ratified in chat (\d{4}-\d{2}-\d{2})\b(?:\s*\([^)]*\))?\s*(?:[;,.:—–-]\s*)?(.*)$/i.exec(cell);
200
+ if (ratified)
201
+ return { status: "ratified", ratifiedOn: ratified[1] ?? null, lands: nullIfEmpty(ratified[2] ?? "") };
202
+ const proposed = /^Proposed\b\s*(?:[;,.:—–-]\s*)?(.*)$/i.exec(cell);
203
+ if (proposed)
204
+ return { status: "proposed", ratifiedOn: null, lands: nullIfEmpty(proposed[1] ?? "") };
205
+ throw new Error(`Decision ${id} (line ${line}) has a Proposed-table status the parser does not recognise: "${cell}". Expected "Proposed" or "Ratified in chat YYYY-MM-DD".`);
206
+ }
207
+ function buildEntries(rows, events) {
208
+ const entries = [];
209
+ const lineById = new Map();
210
+ const searchText = new Map();
211
+ for (const row of rows) {
212
+ const [idCell = "", titleCell = "", answerCell = "", lastCell = ""] = row.cells.map(plainText);
213
+ if (!/^D\d+[a-z]?$/.test(idCell)) {
214
+ throw new Error(`Decision ledger line ${row.line}: "${idCell}" is not a decision id (D<number>, optionally one letter).`);
215
+ }
216
+ const firstLine = lineById.get(idCell);
217
+ if (firstLine !== undefined) {
218
+ throw new Error(`Decision ${idCell} appears more than once in the ledger (lines ${firstLine} and ${row.line}); ids are never reused.`);
219
+ }
220
+ lineById.set(idCell, row.line);
221
+ const base = {
222
+ id: idCell,
223
+ title: titleCell,
224
+ answer: answerCell,
225
+ table: row.table,
226
+ supersededBy: [],
227
+ partlySupersededBy: [],
228
+ };
229
+ if (row.table === "proposed") {
230
+ entries.push({ ...base, ...readProposedStatus(idCell, row.line, lastCell) });
231
+ }
232
+ else {
233
+ entries.push({ ...base, status: row.table, ratifiedOn: null, lands: nullIfEmpty(lastCell) });
234
+ }
235
+ searchText.set(idCell, [titleCell, answerCell, lastCell].join(" "));
236
+ }
237
+ const known = new Set(entries.map((entry) => entry.id));
238
+ // Ratification events date the Ratified table. A sentence that records a
239
+ // supersession ("D15 ... superseded in place") is not a ratification.
240
+ const eventDates = new Map();
241
+ for (const event of events) {
242
+ for (const sentence of sentences(plainText(event.text))) {
243
+ if (/supersed/i.test(sentence))
244
+ continue;
245
+ for (const mention of mentionedIds(sentence, { expandRanges: true })) {
246
+ const id = resolveId(mention, known);
247
+ if (!id)
248
+ continue;
249
+ const current = eventDates.get(id);
250
+ if (current === undefined || event.date < current)
251
+ eventDates.set(id, event.date);
252
+ }
253
+ }
254
+ }
255
+ for (const entry of entries) {
256
+ if (entry.table !== "ratified")
257
+ continue;
258
+ // Otherwise the row's own clause: "Ratified 2026-07-25 (moved from Open)", "Closed 2026-07-25".
259
+ const rowDate = /\b(?:Ratified|Closed)\b[^.;:]*?(\d{4}-\d{2}-\d{2})/.exec(entry.answer)?.[1];
260
+ entry.ratifiedOn = eventDates.get(entry.id) ?? rowDate ?? null;
261
+ }
262
+ // Supersession, both directions: "Superseded by D26" on the row itself, and
263
+ // "Supersedes D24(a)" on the row that replaced it. A statement that names a
264
+ // part ("only", a possessive, "as a …") replaces only that part.
265
+ const replaced = { whole: new Map(), partly: new Map() };
266
+ const record = (superseded, by, partly) => {
267
+ if (superseded === by)
268
+ return;
269
+ const target = partly ? replaced.partly : replaced.whole;
270
+ const set = target.get(superseded) ?? new Set();
271
+ set.add(by);
272
+ target.set(superseded, set);
273
+ };
274
+ const resolveOrThrow = (mention, owner, verb) => {
275
+ const id = resolveId(mention, known);
276
+ if (!id)
277
+ throw new Error(`Decision ${owner} says it ${verb} ${mention}, which the ledger does not define.`);
278
+ return id;
279
+ };
280
+ const after = (text, match) => text.slice((match.index ?? 0) + match[0].length);
281
+ for (const entry of entries) {
282
+ const text = searchText.get(entry.id) ?? "";
283
+ for (const match of text.matchAll(SUPERSEDED_BY)) {
284
+ const partly = PARTIAL_PREFIX.test(text.slice(0, match.index ?? 0)) || PARTIAL_SCOPE.test(after(text, match));
285
+ for (const mention of mentionedIds(match[1] ?? "")) {
286
+ record(entry.id, resolveOrThrow(mention, entry.id, "is superseded by"), partly);
287
+ }
288
+ }
289
+ for (const match of text.matchAll(SUPERSEDES)) {
290
+ const partly = (match[1] ?? "").trim() === "only" || PARTIAL_SCOPE.test(after(text, match));
291
+ for (const mention of mentionedIds(match[2] ?? "")) {
292
+ record(resolveOrThrow(mention, entry.id, "supersedes"), entry.id, partly);
293
+ }
294
+ }
295
+ }
296
+ for (const entry of entries) {
297
+ const whole = replaced.whole.get(entry.id) ?? new Set();
298
+ entry.supersededBy = [...whole].sort(compareIds);
299
+ // One unqualified statement settles it: a decision a row calls replaced is replaced.
300
+ entry.partlySupersededBy = [...(replaced.partly.get(entry.id) ?? [])].filter((id) => !whole.has(id)).sort(compareIds);
301
+ }
302
+ return entries.sort((left, right) => compareIds(left.id, right.id));
303
+ }
@@ -0,0 +1,10 @@
1
+ import type { DecisionLedgerEntry } from "./decision-ledger-parser.js";
2
+ export { parseDecisionLedger, type DecisionLedgerEntry, type DecisionLedgerStatus } from "./decision-ledger-parser.js";
3
+ /**
4
+ * The ledger entry for a decision id. An exact id wins ("D34a" is its own
5
+ * decision); otherwise an answer-option suffix resolves to its base decision
6
+ * ("D19a" and "D19(a)" are D19's option (a)).
7
+ */
8
+ export declare function getDecision(id: string): DecisionLedgerEntry | null;
9
+ /** Every ledger entry, sorted by decision id. */
10
+ export declare function listDecisions(): readonly DecisionLedgerEntry[];
@@ -0,0 +1,2 @@
1
+ import type { DecisionLedgerEntry } from "./decision-ledger-parser.js";
2
+ export declare const DECISION_LEDGER_ENTRIES: readonly DecisionLedgerEntry[];