@cspeach/cli 1.1.27 → 1.1.29

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 (68) hide show
  1. package/dist/agent/loop.js +33 -4
  2. package/dist/agent/provider-shape.js +9 -2
  3. package/dist/agent/providers/ai-hub-provider.js +2 -2
  4. package/dist/agent/providers/byok-provider.js +2 -2
  5. package/dist/agent/providers/local-provider.js +4 -4
  6. package/dist/agent/system-prompt.js +34 -7
  7. package/dist/agent/tool-dispatch.js +20 -1
  8. package/dist/agent/turn-error-ux.js +40 -0
  9. package/dist/approvals/grant-claims.js +70 -0
  10. package/dist/approvals/jwt.js +21 -1
  11. package/dist/approvals/plan-grant.js +297 -0
  12. package/dist/commands/config-set.js +12 -0
  13. package/dist/commands/deploy-card.js +10 -5
  14. package/dist/commands/export-audit.js +1 -1
  15. package/dist/commands/plan-audit-evidence.js +47 -1
  16. package/dist/commands/plan-audit.js +23 -8
  17. package/dist/commands/plan-batch-audit.js +349 -0
  18. package/dist/commands/plan-boundary.js +88 -0
  19. package/dist/commands/plan-chain.js +409 -566
  20. package/dist/commands/plan-continue.js +21 -7
  21. package/dist/commands/plan-gate.js +28 -49
  22. package/dist/commands/plan-reground.js +174 -0
  23. package/dist/commands/plan-resume.js +227 -103
  24. package/dist/commands/plan-sap-pause.js +105 -0
  25. package/dist/commands/plan-startup.js +218 -0
  26. package/dist/config/loader.js +9 -0
  27. package/dist/doctor/checks/forge-rules.js +67 -85
  28. package/dist/doctor/run.js +4 -4
  29. package/dist/lane/app-chain.js +88 -9
  30. package/dist/one-shot.js +1 -0
  31. package/dist/projects/extract-plan.js +34 -1
  32. package/dist/projects/handover-md.js +1 -1
  33. package/dist/projects/plan-objects.js +75 -0
  34. package/dist/projects/plan-run.js +74 -18
  35. package/dist/projects/plan-schema.js +6 -0
  36. package/dist/projects/plan-sources.js +199 -0
  37. package/dist/projects/promote-command.js +1 -1
  38. package/dist/projects/save-command.js +11 -13
  39. package/dist/projects/saved-lines.js +36 -0
  40. package/dist/renderer/checklist-format.js +5 -5
  41. package/dist/renderer/machine-blocks.js +141 -0
  42. package/dist/renderer/tool-labels.js +3 -0
  43. package/dist/repl/plan-hop.js +22 -0
  44. package/dist/repl/rule8-detector.js +16 -1
  45. package/dist/repl.js +320 -55
  46. package/dist/router/router-core.js +2 -2
  47. package/dist/sap/reachability-state.js +25 -0
  48. package/dist/session/audit-export.js +10 -16
  49. package/dist/session/recap.js +16 -26
  50. package/dist/skills/bundled-skills.js +81 -81
  51. package/dist/tools/_filesystem-shared.js +37 -0
  52. package/dist/tools/approval.js +48 -2
  53. package/dist/tools/dispatch-skill.js +3 -2
  54. package/dist/tools/filesystem/file-read.js +5 -3
  55. package/dist/tools/filesystem/glob.js +27 -4
  56. package/dist/tools/fiori/tools.js +6 -0
  57. package/dist/tools/lane/app-lane-resolve.js +1 -1
  58. package/dist/tools/sap-read.js +37 -3
  59. package/dist/tools/sap-write.js +65 -0
  60. package/dist/tools/text-elements/pool.js +250 -0
  61. package/dist/tools/text-elements/tools.js +433 -0
  62. package/dist/tools/tool-loading.js +3 -0
  63. package/dist/tools/transport.js +26 -5
  64. package/dist/tools/write-mode.js +4 -3
  65. package/dist/ui/app.js +4 -0
  66. package/dist/ui/widgets/hold-raw-mode.js +26 -0
  67. package/package.json +120 -121
  68. package/dist/projects/email-template.js +0 -56
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Program text pool — pure logic for selection texts, text symbols and list
3
+ * headings. No SAP, no fs.
4
+ *
5
+ * ADT serves each part as a plain-text source (sap-client
6
+ * getTextElementsSource) and a PUT replaces the WHOLE part. So a write is
7
+ * always read → merge → write: the texts already there are kept byte for
8
+ * byte, only the named entries are added or changed. Never a blind replace.
9
+ *
10
+ * Source formats (S4H read-only capture 2026-10-04 + SAP_BASIS 754 captures):
11
+ * selections `P_MAXROW=Maximum Rows`, blank line between entries; SAP pads
12
+ * the name to 8 chars on read (`S_BUDAT =Posting Date`). A
13
+ * dictionary-referenced text carries a bare `@DDICReference`
14
+ * line right above it. Some releases list screen fields with no
15
+ * text as `NAME=?...` (754); S4H leaves them out.
16
+ * symbols `@MaxLength:N` then `001=Text`, blank line between entries.
17
+ * headings `listHeader=…`, blank line, `columnHeader_1..4=…` — always
18
+ * all five lines.
19
+ */
20
+ export const TEXT_ELEMENT_KINDS = ['selections', 'symbols', 'headings'];
21
+ export const SELECTION_TEXT_MAX = 30;
22
+ export const SYMBOL_TEXT_MAX = 132;
23
+ export const LIST_HEADER_MAX = 70;
24
+ export const COLUMN_HEADER_MAX = 132;
25
+ /** SAP's marker for a selection-screen field that has no text yet. */
26
+ export const UNTEXTED_MARKER = '?...';
27
+ const HEADING_KEYS = ['listHeader', 'columnHeader_1', 'columnHeader_2', 'columnHeader_3', 'columnHeader_4'];
28
+ export function isTextElementKind(k) {
29
+ return typeof k === 'string' && TEXT_ELEMENT_KINDS.includes(k);
30
+ }
31
+ /** Canonical key: selections/symbols upper-cased, headings in SAP's camelCase. */
32
+ export function canonicalKey(kind, key) {
33
+ const k = key.trim();
34
+ if (kind !== 'headings')
35
+ return k.toUpperCase();
36
+ const hit = HEADING_KEYS.find((h) => h.toLowerCase() === k.toLowerCase());
37
+ return hit ?? k;
38
+ }
39
+ export function parseTextPool(kind, body) {
40
+ const out = [];
41
+ let pendingMax;
42
+ let pendingAnn = [];
43
+ for (const raw of body.split(/\r\n|\r|\n/)) {
44
+ if (raw.trim() === '')
45
+ continue;
46
+ if (raw.startsWith('@')) {
47
+ const m = /^@MaxLength:\s*(\d+)\s*$/i.exec(raw);
48
+ if (m && kind === 'symbols')
49
+ pendingMax = parseInt(m[1], 10);
50
+ else
51
+ pendingAnn.push(raw);
52
+ continue;
53
+ }
54
+ const eq = raw.indexOf('=');
55
+ if (eq < 0)
56
+ continue;
57
+ const key = canonicalKey(kind, raw.slice(0, eq));
58
+ const text = raw.slice(eq + 1);
59
+ const entry = { key, text };
60
+ if (kind === 'symbols' && pendingMax !== undefined)
61
+ entry.maxLength = pendingMax;
62
+ if (kind === 'selections' && text === UNTEXTED_MARKER) {
63
+ entry.text = '';
64
+ entry.untexted = true;
65
+ }
66
+ if (pendingAnn.length > 0)
67
+ entry.annotations = pendingAnn;
68
+ out.push(entry);
69
+ pendingMax = undefined;
70
+ pendingAnn = [];
71
+ }
72
+ return out;
73
+ }
74
+ export function formatTextPool(kind, entries) {
75
+ if (kind === 'headings') {
76
+ const byKey = new Map(entries.map((e) => [canonicalKey('headings', e.key), e.text]));
77
+ const v = (k) => byKey.get(k) ?? '';
78
+ return `listHeader=${v('listHeader')}\n\n`
79
+ + HEADING_KEYS.slice(1).map((k) => `${k}=${v(k)}\n`).join('');
80
+ }
81
+ if (kind === 'symbols') {
82
+ if (entries.length === 0)
83
+ return '';
84
+ return entries
85
+ .map((e) => {
86
+ const max = Math.min(Math.max(e.maxLength ?? 0, e.text.length, 1), SYMBOL_TEXT_MAX);
87
+ return [`@MaxLength:${max}`, ...(e.annotations ?? []), `${e.key}=${e.text}`].join('\n');
88
+ })
89
+ .join('\n\n') + '\n';
90
+ }
91
+ // selections — fields without a text are SAP's placeholder, not a text: never written back.
92
+ // Blank line between entries, `@DDICReference` right above its entry: the shape S4H serves.
93
+ const texted = entries.filter((e) => !e.untexted && e.text !== '');
94
+ if (texted.length === 0)
95
+ return '';
96
+ return texted
97
+ .map((e) => [...(e.annotations ?? []), `${e.key}=${e.text}`].join('\n'))
98
+ .join('\n\n') + '\n';
99
+ }
100
+ export function validateUpdates(kind, updates) {
101
+ const errs = [];
102
+ if (!Array.isArray(updates) || updates.length === 0)
103
+ return ['texts must name at least one entry'];
104
+ const seen = new Set();
105
+ for (const u of updates) {
106
+ if (typeof u?.key !== 'string' || typeof u?.text !== 'string') {
107
+ errs.push('every entry needs a key and a text (strings)');
108
+ continue;
109
+ }
110
+ const key = canonicalKey(kind, u.key);
111
+ const text = u.text;
112
+ if (seen.has(key))
113
+ errs.push(`${key}: named twice`);
114
+ seen.add(key);
115
+ if (/[\r\n]/.test(text))
116
+ errs.push(`${key}: a text must be one line`);
117
+ if (kind === 'selections') {
118
+ if (!/^[A-Z][A-Z0-9_]{0,7}$/.test(key))
119
+ errs.push(`${key}: not a selection-screen name (1-8 letters, digits, _)`);
120
+ if (text.trim() === '')
121
+ errs.push(`${key}: text is empty`);
122
+ if (text.length > SELECTION_TEXT_MAX)
123
+ errs.push(`${key}: selection text is ${text.length} chars — the limit is ${SELECTION_TEXT_MAX}`);
124
+ }
125
+ else if (kind === 'symbols') {
126
+ if (!/^[A-Z0-9]{3}$/.test(key))
127
+ errs.push(`${key}: a text symbol id is 3 letters/digits (TEXT-001 → 001)`);
128
+ if (text.trim() === '')
129
+ errs.push(`${key}: text is empty`);
130
+ if (text.length > SYMBOL_TEXT_MAX)
131
+ errs.push(`${key}: text symbol is ${text.length} chars — the limit is ${SYMBOL_TEXT_MAX}`);
132
+ if (u.max_length !== undefined) {
133
+ if (!Number.isInteger(u.max_length) || u.max_length < text.length || u.max_length > SYMBOL_TEXT_MAX) {
134
+ errs.push(`${key}: max_length must be a whole number from the text length (${text.length}) to ${SYMBOL_TEXT_MAX}`);
135
+ }
136
+ }
137
+ }
138
+ else {
139
+ if (!HEADING_KEYS.includes(key))
140
+ errs.push(`${key}: headings are ${HEADING_KEYS.join(', ')}`);
141
+ const limit = key === 'listHeader' ? LIST_HEADER_MAX : COLUMN_HEADER_MAX;
142
+ if (text.length > limit)
143
+ errs.push(`${key}: heading is ${text.length} chars — the limit is ${limit}`);
144
+ }
145
+ }
146
+ return errs;
147
+ }
148
+ /** Merge updates into the existing entries. Validate first (validateUpdates). */
149
+ export function mergeTextPool(kind, existing, updates) {
150
+ const entries = existing.map((e) => ({ ...e, ...(e.annotations ? { annotations: [...e.annotations] } : {}) }));
151
+ const index = new Map(entries.map((e, i) => [e.key, i]));
152
+ const r = { entries, added: [], changed: [], unchanged: [], kept: [], dictionaryReferenceDropped: [] };
153
+ const touched = new Set();
154
+ for (const u of updates) {
155
+ const key = canonicalKey(kind, u.key);
156
+ touched.add(key);
157
+ const at = index.get(key);
158
+ if (at === undefined) {
159
+ const e = { key, text: u.text };
160
+ if (kind === 'symbols')
161
+ e.maxLength = u.max_length ?? u.text.length;
162
+ entries.push(e);
163
+ index.set(key, entries.length - 1);
164
+ if (kind !== 'headings' || u.text !== '')
165
+ r.added.push(key);
166
+ continue;
167
+ }
168
+ const cur = entries[at];
169
+ const newMax = kind === 'symbols' ? (u.max_length ?? Math.max(cur.maxLength ?? 0, u.text.length)) : undefined;
170
+ const sameText = !cur.untexted && cur.text === u.text;
171
+ if (sameText && (kind !== 'symbols' || newMax === cur.maxLength)) {
172
+ r.unchanged.push(key);
173
+ continue;
174
+ }
175
+ if (cur.untexted || (kind === 'headings' && cur.text === ''))
176
+ r.added.push(key);
177
+ else
178
+ r.changed.push(key);
179
+ const next = { ...cur, text: u.text };
180
+ if (kind === 'selections')
181
+ next.untexted = false;
182
+ if (kind === 'symbols')
183
+ next.maxLength = newMax;
184
+ if (kind === 'selections' && cur.annotations?.some((a) => /^@DDIC/i.test(a))) {
185
+ next.annotations = cur.annotations.filter((a) => !/^@DDIC/i.test(a));
186
+ r.dictionaryReferenceDropped.push(key);
187
+ }
188
+ entries[at] = next;
189
+ }
190
+ for (const e of existing) {
191
+ if (touched.has(e.key))
192
+ continue;
193
+ if (e.untexted || e.text === '')
194
+ continue;
195
+ r.kept.push(e.key);
196
+ }
197
+ return r;
198
+ }
199
+ /**
200
+ * Selection-screen field names declared in a program source (PARAMETERS /
201
+ * SELECT-OPTIONS, chained or not). Comments are ignored. `hasIncludes` says
202
+ * the screen may also be declared in an include this scan cannot see.
203
+ */
204
+ export function selectionScreenNames(source) {
205
+ const lines = source.split(/\r\n|\r|\n/).filter((l) => !l.startsWith('*')).map(stripLineComment);
206
+ const code = lines.join('\n');
207
+ const names = new Set();
208
+ const re = /(?:^|[\s.])(PARAMETERS|PARAMETER|SELECT-OPTIONS|SELECT-OPTION)\b\s*(:?)([^.]*)\./gim;
209
+ let m;
210
+ while ((m = re.exec(code)) !== null) {
211
+ const parts = m[2] === ':' ? m[3].split(',') : [m[3]];
212
+ for (const p of parts) {
213
+ const id = /^\s*([A-Za-z_][A-Za-z0-9_]*)/.exec(p);
214
+ if (id)
215
+ names.add(id[1].toUpperCase());
216
+ }
217
+ }
218
+ const hasIncludes = /^\s*INCLUDE\s+(?!STRUCTURE\b|TYPE\b)[A-Za-z0-9_/]+/im.test(code);
219
+ return { names, hasIncludes };
220
+ }
221
+ /** Drop a `"` comment, leaving quotes inside '…' and `…` literals alone. */
222
+ function stripLineComment(line) {
223
+ let inQ = null;
224
+ for (let i = 0; i < line.length; i++) {
225
+ const c = line[i];
226
+ if (inQ) {
227
+ if (c === inQ)
228
+ inQ = null;
229
+ }
230
+ else if (c === "'" || c === '`' || c === '|') {
231
+ inQ = c;
232
+ }
233
+ else if (c === '"') {
234
+ return line.slice(0, i);
235
+ }
236
+ }
237
+ return line;
238
+ }
239
+ /**
240
+ * Snapshot-store key for one text-pool part: `PROG.TEXT_<KIND>`, name = the
241
+ * program. A dot keeps the store's type/name layout and can never collide
242
+ * with a real ABAP type code (same scheme as class includes, CLAS.<INCLUDE>).
243
+ */
244
+ export function textElementsSnapshotType(kind) {
245
+ return `PROG.TEXT_${kind.toUpperCase()}`;
246
+ }
247
+ export function parseTextElementsSnapshotType(type) {
248
+ const m = /^PROG\.TEXT_(SELECTIONS|SYMBOLS|HEADINGS)$/i.exec(type);
249
+ return m ? m[1].toLowerCase() : null;
250
+ }