@cspeach/cli 1.1.51 → 1.1.53
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/dist/agent/chat-prompt.js +4 -1
- package/dist/agent/conversation-prompt.js +31 -13
- package/dist/agent/inline-skill-block.js +341 -0
- package/dist/agent/loop.js +304 -49
- package/dist/agent/maybe-build-project-context.js +11 -2
- package/dist/agent/provider-shape.js +8 -4
- package/dist/agent/providers/ai-hub-provider.js +2 -2
- package/dist/agent/providers/azure-provider.js +2 -2
- package/dist/agent/providers/byok-provider.js +2 -2
- package/dist/agent/providers/local-provider.js +2 -2
- package/dist/agent/repair-partial.js +21 -0
- package/dist/agent/skill-block-key.js +134 -0
- package/dist/agent/steering-queue.js +50 -14
- package/dist/agent/system-prompt.js +22 -0
- package/dist/agent/tool-dispatch.js +25 -9
- package/dist/agent/turn-watchdog.js +1 -1
- package/dist/approvals/advisory-render.js +6 -3
- package/dist/approvals/approval-prompt.js +9 -4
- package/dist/approvals/op-labels.js +20 -3
- package/dist/approvals/plan-grant.js +8 -0
- package/dist/approvals/render.js +22 -10
- package/dist/approvals/risk-floor.js +32 -4
- package/dist/approvals/visible-changes.js +50 -0
- package/dist/cli.js +10 -2
- package/dist/commands/compact.js +6 -1
- package/dist/commands/config-set.js +13 -0
- package/dist/commands/config-show.js +16 -4
- package/dist/commands/export-conversation.js +7 -4
- package/dist/commands/finish-check-guard.js +124 -0
- package/dist/commands/plan-chain.js +2 -2
- package/dist/commands/plan-reground.js +27 -4
- package/dist/commands/plan-resume.js +113 -3
- package/dist/commands/plan-startup.js +3 -3
- package/dist/config/loader.js +5 -1
- package/dist/cost/cost-log.js +6 -0
- package/dist/cost/system-block.js +6 -0
- package/dist/docx/doc-meta.js +1 -0
- package/dist/docx/markdown-to-docx.js +2 -0
- package/dist/lane/app-chain.js +13 -6
- package/dist/models/server-config.js +1 -1
- package/dist/one-shot.js +1 -0
- package/dist/project-context/conventions.js +8 -1
- package/dist/project-context/index-files.js +97 -34
- package/dist/project-context/index.js +1 -1
- package/dist/project-context/render.js +38 -4
- package/dist/projects/answer-blockers.js +107 -16
- package/dist/projects/blocker-answers-note.js +89 -0
- package/dist/projects/change-options.js +70 -0
- package/dist/projects/document-save.js +86 -0
- package/dist/projects/extract-design.js +4 -0
- package/dist/projects/extract-estimate.js +4 -0
- package/dist/projects/extract-spec-gap.js +8 -0
- package/dist/projects/index.js +1 -1
- package/dist/projects/plan-objects.js +24 -0
- package/dist/projects/plan-run.js +12 -2
- package/dist/projects/save-command.js +50 -18
- package/dist/projects/yes-no.js +5 -1
- package/dist/renderer/change-card.js +17 -1
- package/dist/renderer/html-entities.js +25 -0
- package/dist/renderer/machine-blocks.js +54 -7
- package/dist/renderer/steering-echo.js +53 -8
- package/dist/renderer/tool-labels.js +5 -1
- package/dist/renderer/tool-widget.js +20 -5
- package/dist/repl/chat-lane.js +15 -3
- package/dist/repl/inline-mode.js +51 -0
- package/dist/repl/leftover.js +90 -0
- package/dist/repl/reroute-turn.js +7 -1
- package/dist/repl/turn-stop.js +64 -0
- package/dist/repl.js +155 -24
- package/dist/router/classifier.js +1 -1
- package/dist/sap/reachability-state.js +7 -0
- package/dist/sap/unreachable.js +2 -2
- package/dist/session/fresh-start.js +4 -0
- package/dist/session/queued-step.js +3 -1
- package/dist/session/user-prompt.js +12 -1
- package/dist/skills/manifest-client.js +3 -2
- package/dist/skills/preamble.js +6 -4
- package/dist/skills/source-authed.js +3 -2
- package/dist/tools/approval.js +49 -8
- package/dist/tools/dispatch-skill.js +1 -1
- package/dist/tools/hand-off.js +49 -7
- package/dist/tools/lane/app-lane-resolve.js +9 -0
- package/dist/tools/method-only-change.js +241 -0
- package/dist/tools/project/spec_answers_record.js +190 -0
- package/dist/tools/rap/skeleton-resolve.js +4 -0
- package/dist/tools/sap-write.js +78 -3
- package/dist/tools/snapshot.js +7 -2
- package/dist/tools/subagent/agent_run.js +14 -0
- package/dist/tools/tool-loading.js +18 -8
- package/dist/tools/transport-fit-check.js +70 -0
- package/dist/tools/transport.js +17 -8
- package/dist/tools/write-pipeline.js +46 -5
- package/dist/ui/app.js +112 -13
- package/dist/ui/approval-modal.js +13 -7
- package/dist/ui/ascii-chars.js +21 -3
- package/dist/ui/footer.js +128 -7
- package/dist/ui/interrupt.js +35 -1
- package/dist/ui/live-area.js +21 -4
- package/dist/ui/queue-box-emitter.js +14 -0
- package/dist/ui/rewind-panel.js +2 -1
- package/dist/ui/session-timeline.js +2 -1
- package/dist/ui/text-input.js +31 -2
- package/dist/ui/widgets/ask-card.js +171 -29
- package/dist/ui/widgets/bar-chart.js +2 -1
- package/dist/ui/widgets/confirm-card.js +2 -1
- package/dist/ui/widgets/dep-graph.js +2 -1
- package/dist/ui/widgets/diff-viewer.js +2 -1
- package/dist/ui/widgets/question-card.js +2 -1
- package/dist/ui/widgets/shortcuts-sheet.js +2 -2
- package/dist/ui/widgets/stack-frames.js +2 -1
- package/dist/util/connect-retry.js +65 -0
- package/package.json +2 -2
|
@@ -22,7 +22,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
22
22
|
* the text input is not focused) · Esc cancels; on the free-text row Esc
|
|
23
23
|
* first returns to the options.
|
|
24
24
|
*/
|
|
25
|
-
import { useMemo, useRef, useState } from 'react';
|
|
25
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
26
26
|
import { Box, Text, useInput } from 'ink';
|
|
27
27
|
import TextInput from '../text-input.js';
|
|
28
28
|
import { useReclaimRawStdin } from './reclaim-raw-stdin.js';
|
|
@@ -34,14 +34,31 @@ import { sanitizeControlChars } from '../../renderer/sanitize.js';
|
|
|
34
34
|
import { wrapRows } from '../../renderer/wrap-rows.js';
|
|
35
35
|
import { answerDisplayText } from '../ask-answer-text.js';
|
|
36
36
|
import { CardFrame } from './confirm-card.js';
|
|
37
|
+
import { emitTrace } from '../../renderer/trace.js';
|
|
37
38
|
/** Option rows visible at once — the rest scroll. */
|
|
38
39
|
export const VISIBLE_OPTION_ROWS = 5;
|
|
39
40
|
/**
|
|
40
41
|
* Single-key cards (the Yes/No agent confirm) drop keys received in the first
|
|
41
42
|
* GUARD_MS after mount, so a key typed ahead while output streamed cannot
|
|
42
43
|
* answer the card.
|
|
44
|
+
*
|
|
45
|
+
* 1.1.53 Task 20 (F-P) — every card, keys or not, also ignores Enter, digit
|
|
46
|
+
* picks and alias keys for GUARD_MS after each screen appears (the card, each
|
|
47
|
+
* next question of a form, the Review step), so the Enter of a "pick, then
|
|
48
|
+
* Enter" habit cannot answer the card that comes next. Typing into the
|
|
49
|
+
* typed-answer row is never dropped.
|
|
43
50
|
*/
|
|
44
51
|
const GUARD_MS = 150;
|
|
52
|
+
/** Task 20 — the per-screen pick window; GUARD_MS outside tests. */
|
|
53
|
+
let pickGuardMs = GUARD_MS;
|
|
54
|
+
/**
|
|
55
|
+
* Test seam only: interaction tests that drive keys faster than a person can
|
|
56
|
+
* type set 0; undefined restores GUARD_MS. The window itself is pinned by
|
|
57
|
+
* ask-card-stray-enter.test.tsx. The y/n mount guard is not affected.
|
|
58
|
+
*/
|
|
59
|
+
export function setAskPickGuardMsForTest(ms) {
|
|
60
|
+
pickGuardMs = ms ?? GUARD_MS;
|
|
61
|
+
}
|
|
45
62
|
/**
|
|
46
63
|
* Card text is model- or tool-supplied. Tabs and newlines are flattened by
|
|
47
64
|
* truncateToWidth; every OTHER control character is neutralised by the
|
|
@@ -56,7 +73,12 @@ const sanitize = sanitizeControlChars;
|
|
|
56
73
|
function fit(text, cells) {
|
|
57
74
|
return truncateToWidth(sanitize(text), cells);
|
|
58
75
|
}
|
|
59
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* Round 4 item 8b — the question's base rows. 1.1.53 Task 14: no longer the
|
|
78
|
+
* cap. With a row budget (App) the current question grows past it to as many
|
|
79
|
+
* rows as it needs while the card still fits (askCardLayout); without one it
|
|
80
|
+
* shows whole. Only a question that still does not fit is cut.
|
|
81
|
+
*/
|
|
60
82
|
export const QUESTION_MAX_ROWS = 3;
|
|
61
83
|
/** Context wraps too; it gets at least one row per line and at most this many in all (or its line count, if larger). */
|
|
62
84
|
export const CONTEXT_MAX_ROWS = 6;
|
|
@@ -70,6 +92,8 @@ const DESCRIPTION_INDENT = ' ';
|
|
|
70
92
|
export const CONTEXT_FOLDED_ROWS = 3;
|
|
71
93
|
/** …and then the question to this many. */
|
|
72
94
|
export const QUESTION_FOLDED_ROWS = 2;
|
|
95
|
+
/** 1.1.53 Task 14 — the dim last row of a cut question; the whole question is printed in the scrollback above. */
|
|
96
|
+
export const FULL_QUESTION_NOTE = 'The full question is printed above.';
|
|
73
97
|
/** The question rows (at most `maxRows`), with the multi-select suffix kept whole on the last row. */
|
|
74
98
|
function questionRows(question, suffix, cells, maxRows = QUESTION_MAX_ROWS) {
|
|
75
99
|
if (!suffix)
|
|
@@ -87,6 +111,58 @@ function questionRows(question, suffix, cells, maxRows = QUESTION_MAX_ROWS) {
|
|
|
87
111
|
}
|
|
88
112
|
return rows;
|
|
89
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* 1.1.53 Task 14 — the question as plain wrapped text for the scrollback:
|
|
116
|
+
* each line of it wrapped at `cells`, nothing cut. Printed above the card
|
|
117
|
+
* when the card has to cut the question, so the person can read it whole.
|
|
118
|
+
*/
|
|
119
|
+
export function fullQuestionText(question, cells) {
|
|
120
|
+
return question.split(/\r?\n/).map((line) => wrapRows(line, cells, Number.MAX_SAFE_INTEGER).join('\n')).join('\n');
|
|
121
|
+
}
|
|
122
|
+
/** The multi-select suffix, when the width has room for it (kept whole on the question's last row). */
|
|
123
|
+
function questionSuffix(q, cells) {
|
|
124
|
+
return q.multiSelect && cells - MULTI_SUFFIX.length >= 1 ? MULTI_SUFFIX : '';
|
|
125
|
+
}
|
|
126
|
+
/** Rows the whole question needs (suffix included). */
|
|
127
|
+
function questionNeed(q, cells) {
|
|
128
|
+
// The layout asks this many times per render (folds, growth, estimate): memoised.
|
|
129
|
+
const key = `${cells}|${q.multiSelect ? 1 : 0}|${q.question}`;
|
|
130
|
+
let need = needCache.get(key);
|
|
131
|
+
if (need === undefined) {
|
|
132
|
+
need = questionRows(q.question, questionSuffix(q, cells), cells, Number.MAX_SAFE_INTEGER).length;
|
|
133
|
+
if (needCache.size >= 64)
|
|
134
|
+
needCache.clear();
|
|
135
|
+
needCache.set(key, need);
|
|
136
|
+
}
|
|
137
|
+
return need;
|
|
138
|
+
}
|
|
139
|
+
const needCache = new Map();
|
|
140
|
+
/** Fix round 2 (I-1) — a cut question gets the pointer row only from this many rows; below, the old cut rows. */
|
|
141
|
+
const POINTER_MIN_ROWS = 3;
|
|
142
|
+
/**
|
|
143
|
+
* 1.1.53 Task 14 — the question as drawn in at most `cap` rows. It fits: the
|
|
144
|
+
* same rows as before. It does not: with `cap` of POINTER_MIN_ROWS or more it
|
|
145
|
+
* is cut to `cap - 1` rows and the last row is the FULL_QUESTION_NOTE pointer;
|
|
146
|
+
* with fewer (the short-screen fold) it is cut to `cap` rows exactly as
|
|
147
|
+
* before, no pointer. Either way the block is min(need, cap) rows tall, so
|
|
148
|
+
* the height math is today's.
|
|
149
|
+
*/
|
|
150
|
+
function questionBlock(q, cells, cap) {
|
|
151
|
+
// Memoised like questionNeed: the fold and growth loops ask for many caps.
|
|
152
|
+
const key = `${cells}|${q.multiSelect ? 1 : 0}|${cap}|${q.question}`;
|
|
153
|
+
const hit = blockCache.get(key);
|
|
154
|
+
if (hit)
|
|
155
|
+
return hit;
|
|
156
|
+
const suffix = questionSuffix(q, cells);
|
|
157
|
+
const block = questionNeed(q, cells) <= cap
|
|
158
|
+
? { lines: questionRows(q.question, suffix, cells, cap), cut: false }
|
|
159
|
+
: { lines: questionRows(q.question, suffix, cells, cap >= POINTER_MIN_ROWS ? cap - 1 : cap), cut: true };
|
|
160
|
+
if (blockCache.size >= 256)
|
|
161
|
+
blockCache.clear();
|
|
162
|
+
blockCache.set(key, block);
|
|
163
|
+
return block;
|
|
164
|
+
}
|
|
165
|
+
const blockCache = new Map();
|
|
90
166
|
/**
|
|
91
167
|
* Context rows: every line keeps at least one row; wrapping fills the rest of
|
|
92
168
|
* the budget. Folded (`maxRows` below CONTEXT_MAX_ROWS, Task 4) the context
|
|
@@ -108,6 +184,8 @@ function contextRows(context, cells, maxRows = CONTEXT_MAX_ROWS) {
|
|
|
108
184
|
});
|
|
109
185
|
return out;
|
|
110
186
|
}
|
|
187
|
+
/** The rows question `i` may take at `layout`. */
|
|
188
|
+
const questionCap = (layout, i) => layout.questionRowsFor?.[i] ?? layout.questionRows;
|
|
111
189
|
/**
|
|
112
190
|
* Lay out one option at `inner` cells. The label wraps onto LABEL_MAX_ROWS
|
|
113
191
|
* under its own text; the (recommended) / (default) suffix stays whole on
|
|
@@ -151,6 +229,17 @@ export function optionRows(o, idx, q, inner, layout) {
|
|
|
151
229
|
}
|
|
152
230
|
/** Rows one option takes on the card. */
|
|
153
231
|
const optionHeight = (r) => r.label.length + r.description.length;
|
|
232
|
+
/**
|
|
233
|
+
* 1.1.53 R16 — the digit 1-9 a key chunk stands for, or 0. Remote desktops,
|
|
234
|
+
* VDI, ConPTY and key drivers can deliver a digit and Enter as one chunk
|
|
235
|
+
* ("2\r"): it counts as the digit alone, and the newline goes with it (never
|
|
236
|
+
* on to a following card). Any other multi-byte chunk ("22", an arrow plus a
|
|
237
|
+
* digit) is no digit.
|
|
238
|
+
*/
|
|
239
|
+
function digitKey(input) {
|
|
240
|
+
const m = /^([1-9])[\r\n]*$/.exec(input);
|
|
241
|
+
return m ? Number(m[1]) : 0;
|
|
242
|
+
}
|
|
154
243
|
export function toCardQuestions(request) {
|
|
155
244
|
if ('questions' in request) {
|
|
156
245
|
return request.questions.map((q) => {
|
|
@@ -212,29 +301,31 @@ const suffixText = (r) => (r.rec ? REC : '') + (r.def ? DEFAULT_SUFFIX : '');
|
|
|
212
301
|
const askInner = (width) => Math.max(1, width - 4);
|
|
213
302
|
/** The frame's top and bottom edges. */
|
|
214
303
|
const FRAME_ROWS = 2;
|
|
304
|
+
/** The rows of one question's screen when its question may take `cap` rows. */
|
|
305
|
+
function screenRows(q, chipRows, inner, layout, cap) {
|
|
306
|
+
const ctx = contextRows(q.context, inner, layout.contextRows).length;
|
|
307
|
+
const block = questionBlock(q, inner, cap);
|
|
308
|
+
const qRows = block.lines.length + (block.cut && cap >= POINTER_MIN_ROWS ? 1 : 0); // + the pointer row
|
|
309
|
+
let body = 2; // margin + the text input row
|
|
310
|
+
if (!q.freeText) {
|
|
311
|
+
// Every wrapped label and description row counts; with more options
|
|
312
|
+
// than the window shows, the tallest window of VISIBLE_OPTION_ROWS.
|
|
313
|
+
const heights = q.options.map((o, i) => optionHeight(optionRows(o, i, q, inner, layout)));
|
|
314
|
+
const span = Math.min(heights.length, VISIBLE_OPTION_ROWS);
|
|
315
|
+
let tallest = 0;
|
|
316
|
+
for (let start = 0; start + span <= heights.length; start++) {
|
|
317
|
+
tallest = Math.max(tallest, heights.slice(start, start + span).reduce((x, y) => x + y, 0));
|
|
318
|
+
}
|
|
319
|
+
const arrows = q.options.length > VISIBLE_OPTION_ROWS ? 2 : 0;
|
|
320
|
+
body = 1 + tallest + arrows + (q.otherRow ? 1 : 0);
|
|
321
|
+
}
|
|
322
|
+
return chipRows + qRows + ctx + body + 1 + FRAME_ROWS; // question + body + hint + frame
|
|
323
|
+
}
|
|
215
324
|
/** The card's rows at `layout` — worst case over every question screen, plus the Review screen. */
|
|
216
325
|
function askRowsFor(qs, inner, layout) {
|
|
217
326
|
const chipRows = qs.length > 1 ? 2 : 0; // tab row + its blank line
|
|
218
327
|
let worst = 0;
|
|
219
|
-
|
|
220
|
-
const ctx = contextRows(q.context, inner, layout.contextRows).length;
|
|
221
|
-
const suffix = q.multiSelect && inner - MULTI_SUFFIX.length >= 1 ? MULTI_SUFFIX : '';
|
|
222
|
-
const qRows = questionRows(q.question, suffix, inner, layout.questionRows).length;
|
|
223
|
-
let body = 2; // margin + the text input row
|
|
224
|
-
if (!q.freeText) {
|
|
225
|
-
// Every wrapped label and description row counts; with more options
|
|
226
|
-
// than the window shows, the tallest window of VISIBLE_OPTION_ROWS.
|
|
227
|
-
const heights = q.options.map((o, i) => optionHeight(optionRows(o, i, q, inner, layout)));
|
|
228
|
-
const span = Math.min(heights.length, VISIBLE_OPTION_ROWS);
|
|
229
|
-
let tallest = 0;
|
|
230
|
-
for (let start = 0; start + span <= heights.length; start++) {
|
|
231
|
-
tallest = Math.max(tallest, heights.slice(start, start + span).reduce((x, y) => x + y, 0));
|
|
232
|
-
}
|
|
233
|
-
const arrows = q.options.length > VISIBLE_OPTION_ROWS ? 2 : 0;
|
|
234
|
-
body = 1 + tallest + arrows + (q.otherRow ? 1 : 0);
|
|
235
|
-
}
|
|
236
|
-
worst = Math.max(worst, chipRows + qRows + ctx + body + 1 + FRAME_ROWS); // question + body + hint + frame
|
|
237
|
-
}
|
|
328
|
+
qs.forEach((q, i) => { worst = Math.max(worst, screenRows(q, chipRows, inner, layout, questionCap(layout, i))); });
|
|
238
329
|
if (qs.length > 1) {
|
|
239
330
|
// title + one row per answer + margin + choices + hint + frame
|
|
240
331
|
worst = Math.max(worst, chipRows + 1 + qs.length + 1 + REVIEW_CHOICES.length + 1 + FRAME_ROWS);
|
|
@@ -256,10 +347,12 @@ export function askCardLayout(request, showDescriptions, width = uiWidth(), maxR
|
|
|
256
347
|
contextRows: CONTEXT_MAX_ROWS,
|
|
257
348
|
questionRows: QUESTION_MAX_ROWS,
|
|
258
349
|
};
|
|
259
|
-
if (maxRows === undefined)
|
|
260
|
-
return layout;
|
|
261
350
|
const qs = toCardQuestions(request);
|
|
262
351
|
const inner = askInner(width);
|
|
352
|
+
// 1.1.53 Task 14 — no budget: every question shows whole.
|
|
353
|
+
if (maxRows === undefined) {
|
|
354
|
+
return { ...layout, questionRowsFor: qs.map((q) => Math.max(layout.questionRows, questionNeed(q, inner))) };
|
|
355
|
+
}
|
|
263
356
|
const folds = [
|
|
264
357
|
(l) => { l.labelRows = 1; },
|
|
265
358
|
(l) => { l.descriptionRows = Math.min(l.descriptionRows, 1); },
|
|
@@ -272,6 +365,21 @@ export function askCardLayout(request, showDescriptions, width = uiWidth(), maxR
|
|
|
272
365
|
break;
|
|
273
366
|
fold(layout);
|
|
274
367
|
}
|
|
368
|
+
// 1.1.53 Task 14 — then each question grows to the rows it needs while its
|
|
369
|
+
// screen still fits maxRows. What still does not fit stays cut (the AskCard
|
|
370
|
+
// prints it whole above). The question block is min(need, cap) rows tall
|
|
371
|
+
// (questionBlock), so the rest of the screen is measured once at the base
|
|
372
|
+
// and the cap follows directly (fix round 2, I-2 — no per-row re-wrap).
|
|
373
|
+
const chipRows = qs.length > 1 ? 2 : 0;
|
|
374
|
+
const base = layout.questionRows;
|
|
375
|
+
layout.questionRowsFor = qs.map((q) => {
|
|
376
|
+
const need = questionNeed(q, inner);
|
|
377
|
+
if (need <= base)
|
|
378
|
+
return base;
|
|
379
|
+
const rest = screenRows(q, chipRows, inner, layout, base) - base; // everything but the question
|
|
380
|
+
const room = maxRows - rest;
|
|
381
|
+
return need <= room ? need : Math.max(base, room);
|
|
382
|
+
});
|
|
275
383
|
return layout;
|
|
276
384
|
}
|
|
277
385
|
/**
|
|
@@ -320,6 +428,9 @@ export function AskCard({ request, width, showDescriptions, maxRows, onResolveV1
|
|
|
320
428
|
// while output streamed must not answer the card.
|
|
321
429
|
const mountGuard = questions.some((qq) => qq.keys !== undefined);
|
|
322
430
|
const mountedAt = useRef(Date.now());
|
|
431
|
+
// Task 20 — when the current screen appeared (reset on every focus change).
|
|
432
|
+
const screenAt = useRef(Date.now());
|
|
433
|
+
const pickGuarded = () => Date.now() - screenAt.current < pickGuardMs;
|
|
323
434
|
const done = useRef(false);
|
|
324
435
|
// Focus slots: 0..n-1 = questions; n = the Review step (2+ questions,
|
|
325
436
|
// reachable only once every question has an answer).
|
|
@@ -329,6 +440,16 @@ export function AskCard({ request, width, showDescriptions, maxRows, onResolveV1
|
|
|
329
440
|
const [drafts, setDrafts] = useState(() => questions.map(() => ''));
|
|
330
441
|
const [answers, setAnswers] = useState(() => questions.map(() => null));
|
|
331
442
|
const [reviewCursor, setReviewCursor] = useState(0);
|
|
443
|
+
const firstScreen = useRef(true);
|
|
444
|
+
useLayoutEffect(() => {
|
|
445
|
+
if (firstScreen.current) {
|
|
446
|
+
firstScreen.current = false;
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
screenAt.current = Date.now();
|
|
450
|
+
}, [focus]);
|
|
451
|
+
// 1.1.53 Task 14 — questions already printed whole to the scrollback.
|
|
452
|
+
const printedFull = useRef(new Set());
|
|
332
453
|
const isReview = n > 1 && focus === n;
|
|
333
454
|
const q = isReview ? undefined : questions[focus];
|
|
334
455
|
const cursor = q ? cursors[focus] : 0;
|
|
@@ -421,6 +542,8 @@ export function AskCard({ request, width, showDescriptions, maxRows, onResolveV1
|
|
|
421
542
|
answerAndAdvance(focus, opt.value);
|
|
422
543
|
};
|
|
423
544
|
const onTextSubmit = (v) => {
|
|
545
|
+
if (pickGuarded())
|
|
546
|
+
return; // Task 20 — a stray Enter does not submit the row
|
|
424
547
|
const trimmed = v.trim();
|
|
425
548
|
if (trimmed.length === 0) {
|
|
426
549
|
if (!isForm)
|
|
@@ -483,11 +606,15 @@ export function AskCard({ request, width, showDescriptions, maxRows, onResolveV1
|
|
|
483
606
|
setReviewCursor(1);
|
|
484
607
|
return;
|
|
485
608
|
}
|
|
486
|
-
|
|
609
|
+
// R16 — "1\r" / "2\n" in one chunk act like the bare digit.
|
|
610
|
+
const reviewDigit = digitKey(input);
|
|
611
|
+
if (pickGuarded() && (reviewDigit > 0 || key.return))
|
|
612
|
+
return; // Task 20
|
|
613
|
+
if (reviewDigit === 1 || (key.return && reviewCursor === 0)) {
|
|
487
614
|
submit(answers);
|
|
488
615
|
return;
|
|
489
616
|
}
|
|
490
|
-
if (
|
|
617
|
+
if (reviewDigit === 2 || (key.return && reviewCursor === 1)) {
|
|
491
618
|
cancel();
|
|
492
619
|
return;
|
|
493
620
|
}
|
|
@@ -510,11 +637,14 @@ export function AskCard({ request, width, showDescriptions, maxRows, onResolveV1
|
|
|
510
637
|
return;
|
|
511
638
|
// Ctrl+Y / Meta+Y are not "y": only a bare key answers.
|
|
512
639
|
const alias = key.ctrl || key.meta ? undefined : q.keys?.[input.toLowerCase()];
|
|
640
|
+
// Task 20 — Enter, digit picks and alias keys wait out the screen's window.
|
|
641
|
+
if (pickGuarded() && (alias !== undefined || digitKey(input) > 0 || key.return))
|
|
642
|
+
return;
|
|
513
643
|
if (alias !== undefined && q.options.some((o) => o.value === alias)) {
|
|
514
644
|
answerAndAdvance(focus, alias);
|
|
515
645
|
return;
|
|
516
646
|
}
|
|
517
|
-
const digit =
|
|
647
|
+
const digit = digitKey(input) - 1;
|
|
518
648
|
if (digit >= 0) {
|
|
519
649
|
if (digit >= rowCount)
|
|
520
650
|
return;
|
|
@@ -545,6 +675,19 @@ export function AskCard({ request, width, showDescriptions, maxRows, onResolveV1
|
|
|
545
675
|
const frameHeader = isForm ? request.frameHeader : undefined;
|
|
546
676
|
// The same fold as estimateAskCardRows, so the drawn card is the estimated card.
|
|
547
677
|
const layout = askCardLayout(request, showDescriptions, width, maxRows);
|
|
678
|
+
// 1.1.53 Task 14 — a question the card has to cut is printed whole, once,
|
|
679
|
+
// to the scrollback just above the card (where answered questions land),
|
|
680
|
+
// when it becomes the current question. A question that fits prints
|
|
681
|
+
// nothing. Classic / headless have no trace emitter: emitTrace is a no-op.
|
|
682
|
+
const block = q ? questionBlock(q, inner, questionCap(layout, focus)) : undefined;
|
|
683
|
+
const questionCut = block?.cut === true;
|
|
684
|
+
const showPointer = questionCut && questionCap(layout, focus) >= POINTER_MIN_ROWS;
|
|
685
|
+
useEffect(() => {
|
|
686
|
+
if (!q || !questionCut || printedFull.current.has(focus))
|
|
687
|
+
return;
|
|
688
|
+
printedFull.current.add(focus);
|
|
689
|
+
emitTrace(fullQuestionText(q.question, inner));
|
|
690
|
+
}, [q, questionCut, focus, inner]);
|
|
548
691
|
const chipRow = n > 1 ? (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { wrap: "truncate-end", children: fitSegments([
|
|
549
692
|
...questions.map((qq, i) => ({
|
|
550
693
|
text: `${i > 0 ? ' ' : ''}${answers[i] !== null ? g.answered : g.pending} ${fit(qq.header, inner)}`,
|
|
@@ -574,8 +717,7 @@ export function AskCard({ request, width, showDescriptions, maxRows, onResolveV1
|
|
|
574
717
|
const hintKind = q.freeText ? 'text' : onOther ? 'other' : q.multiSelect ? 'multi' : 'pick';
|
|
575
718
|
// The multi-select suffix is the card's only multi cue: it stays whole and
|
|
576
719
|
// the question is cut instead (the same rule as "(recommended)" below).
|
|
577
|
-
const
|
|
578
|
-
const questionLines = questionRows(q.question, suffix, inner, layout.questionRows);
|
|
720
|
+
const questionLines = block.lines;
|
|
579
721
|
const hint = askCardHint(hintKind, {
|
|
580
722
|
multiQuestion: n > 1,
|
|
581
723
|
count: rowCount,
|
|
@@ -583,7 +725,7 @@ export function AskCard({ request, width, showDescriptions, maxRows, onResolveV1
|
|
|
583
725
|
});
|
|
584
726
|
// Task 4 — the same frame as ConfirmCard: QUESTION tag (plus the optional
|
|
585
727
|
// header) on the top edge, the hint right under the options.
|
|
586
|
-
return (_jsxs(CardFrame, { tag: "QUESTION", header: frameHeader, width: width, children: [chipRow, questionLines.map((line, i) => (_jsx(Text, { bold: true, wrap: "truncate-end", children: line }, `q-${i}`))), contextLines.map((line, i) => (_jsx(Text, { ...dim, wrap: "truncate-end", children: line }, `ctx-${i}`))), q.freeText ? (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { ...roleProps('sap'), children: `${g.cursor} ` }), _jsx(TextInput, { value: drafts[focus], onChange: setDraft, onSubmit: onTextSubmit, maxDisplayWidth: inputWidth })] })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [winStart > 0 && _jsx(Text, { ...dim, wrap: "truncate-end", children: ` ↑ ${winStart} more` }), visible.map((o, vi) => {
|
|
728
|
+
return (_jsxs(CardFrame, { tag: "QUESTION", header: frameHeader, width: width, children: [chipRow, questionLines.map((line, i) => (_jsx(Text, { bold: true, wrap: "truncate-end", children: line }, `q-${i}`))), showPointer ? _jsx(Text, { ...dim, wrap: "truncate-end", children: fit(FULL_QUESTION_NOTE, inner) }) : null, contextLines.map((line, i) => (_jsx(Text, { ...dim, wrap: "truncate-end", children: line }, `ctx-${i}`))), q.freeText ? (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { ...roleProps('sap'), children: `${g.cursor} ` }), _jsx(TextInput, { value: drafts[focus], onChange: setDraft, onSubmit: onTextSubmit, maxDisplayWidth: inputWidth })] })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [winStart > 0 && _jsx(Text, { ...dim, wrap: "truncate-end", children: ` ↑ ${winStart} more` }), visible.map((o, vi) => {
|
|
587
729
|
const idx = winStart + vi;
|
|
588
730
|
const cur = !onOther && idx === cursor;
|
|
589
731
|
const tick = q.multiSelect ? `${ticks[focus].has(o.value) ? g.tickOn : g.tickOff} ` : '';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { cardBorderStyle } from '../ascii-chars.js';
|
|
3
4
|
import { roleProps } from '../role-props.js';
|
|
4
5
|
// The model names a colour; the bar takes the matching theme role, so it follows
|
|
5
6
|
// NO_COLOR and the light palette. White / unknown → the terminal's own text colour.
|
|
@@ -13,7 +14,7 @@ function barProps(c) {
|
|
|
13
14
|
export function BarChart({ data }) {
|
|
14
15
|
const max = Math.max(...data.map((d) => d.value), 1);
|
|
15
16
|
const barWidth = 30;
|
|
16
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle:
|
|
17
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: cardBorderStyle(), borderColor: roleProps('frame').color, paddingX: 1, children: [_jsx(Text, { ...roleProps('sap', { bold: true }), children: "Distribution" }), data.map((item) => {
|
|
17
18
|
const filled = Math.round((item.value / max) * barWidth);
|
|
18
19
|
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
19
20
|
return (_jsxs(Box, { children: [_jsxs(Text, { children: [item.label.padEnd(28), " "] }), _jsx(Text, { ...barProps(item.color), children: bar }), _jsxs(Text, { children: [" ", item.value] })] }, item.label));
|
|
@@ -192,7 +192,7 @@ export function CardFrame({ tag, header, footnote, width, children }) {
|
|
|
192
192
|
const bottom = edgeSegments([box.bl, box.br], box.h, footnote, width);
|
|
193
193
|
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: width, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...frame, children: top.lead }), _jsx(Text, { ...roleProps(TAG_ROLE[tag], { bold: true }), children: tagShown }), headerShown ? _jsx(Text, { ...roleProps('dim'), children: headerShown }) : null, _jsx(Text, { ...frame, children: top.tail })] }), _jsx(Box, { flexDirection: "column", width: width, paddingX: 1, borderStyle: inkBorder(box), borderTop: false, borderBottom: false, borderColor: frame.color, children: children }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...frame, children: bottom.lead }), _jsx(Text, { ...roleProps('dim'), children: bottom.label }), _jsx(Text, { ...frame, children: bottom.tail })] })] }));
|
|
194
194
|
}
|
|
195
|
-
export function ConfirmCard({ request, width, maxRows, onResolve, onInterruptTurn }) {
|
|
195
|
+
export function ConfirmCard({ request, width, maxRows, onResolve, onInterruptTurn, onEscape }) {
|
|
196
196
|
// Re-assert raw stdin on mount: an earlier inquirer prompt can leave Ink's
|
|
197
197
|
// raw-mode bookkeeping stale, and the card would then get no keys.
|
|
198
198
|
useReclaimRawStdin();
|
|
@@ -222,6 +222,7 @@ export function ConfirmCard({ request, width, maxRows, onResolve, onInterruptTur
|
|
|
222
222
|
if (Date.now() - mountedAt.current < CONFIRM_INPUT_GUARD_MS)
|
|
223
223
|
return;
|
|
224
224
|
if (key.escape) {
|
|
225
|
+
onEscape?.();
|
|
225
226
|
finish({ value: null, cancelled: true });
|
|
226
227
|
return;
|
|
227
228
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { cardBorderStyle } from '../ascii-chars.js';
|
|
3
4
|
import { roleProps } from '../role-props.js';
|
|
4
5
|
export function DepGraph({ data }) {
|
|
5
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle:
|
|
6
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: cardBorderStyle(), borderColor: roleProps('frame').color, paddingX: 1, children: [_jsxs(Text, { ...roleProps('sap', { bold: true }), children: ["Dependencies \u2014 ", data.root] }), data.children.map((c, i) => {
|
|
6
7
|
const isLast = i === data.children.length - 1;
|
|
7
8
|
const prefix = isLast ? '└─' : '├─';
|
|
8
9
|
return (_jsxs(Box, { children: [_jsxs(Text, { ...roleProps('dim'), children: [prefix, " "] }), _jsx(Text, { ...roleProps('warn'), children: c.name }), _jsxs(Text, { ...roleProps('dim'), children: [" (", c.type, ") "] }), _jsx(Text, { dimColor: true, children: c.relation })] }, c.name));
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { cardBorderStyle } from '../ascii-chars.js';
|
|
3
4
|
import { roleProps } from '../role-props.js';
|
|
4
5
|
export function DiffViewer({ data }) {
|
|
5
6
|
const lines = data.unified.split('\n');
|
|
6
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle:
|
|
7
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: cardBorderStyle(), borderColor: roleProps('frame').color, paddingX: 1, children: [_jsx(Text, { ...roleProps('dim', { bold: true }), children: "Diff" }), lines.map((line, idx) => {
|
|
7
8
|
const role = line.startsWith('+') ? 'ok' :
|
|
8
9
|
line.startsWith('-') ? 'bad' :
|
|
9
10
|
line.startsWith('@@') ? 'sap' : 'dim';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { cardBorderStyle } from '../ascii-chars.js';
|
|
3
4
|
import { roleProps } from '../role-props.js';
|
|
4
5
|
/**
|
|
5
6
|
* Ink-mode rendering of cspeach-widget:question. Uses a double-border box
|
|
@@ -8,5 +9,5 @@ import { roleProps } from '../role-props.js';
|
|
|
8
9
|
* and types their answer as the next REPL prompt.
|
|
9
10
|
*/
|
|
10
11
|
export function QuestionCard({ data }) {
|
|
11
|
-
return (_jsxs(Box, { borderStyle:
|
|
12
|
+
return (_jsxs(Box, { borderStyle: cardBorderStyle('double'), borderColor: roleProps('sap').color, flexDirection: "column", padding: 1, marginTop: 1, children: [_jsxs(Text, { bold: true, children: ["Question: ", data.id] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: data.question }) }), data.context ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Context: ", data.context] }) })) : null, (data.kind === 'choice' || data.kind === 'multi') && data.choices ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: data.kind === 'choice' ? 'Pick one:' : 'Pick any (comma-separate):' }), data.choices.map((c, i) => (_jsxs(Text, { children: ["[", i + 1, "] ", c.label] }, c.value)))] })) : null] }));
|
|
12
13
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
3
|
import { roleProps } from '../role-props.js';
|
|
4
|
-
import {
|
|
4
|
+
import { cardBorderStyle } from '../ascii-chars.js';
|
|
5
5
|
import { truncateToWidth } from '../../renderer/ui-width.js';
|
|
6
6
|
/** Spec §8 table, in order. */
|
|
7
7
|
export const SHORTCUT_ROWS = [
|
|
@@ -21,5 +21,5 @@ export function ShortcutsSheet({ width, onClose }) {
|
|
|
21
21
|
useInput((input, key) => { if (key.escape || (key.ctrl && input === 'c'))
|
|
22
22
|
onClose(); });
|
|
23
23
|
const inner = Math.max(8, width - 4);
|
|
24
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle:
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: cardBorderStyle(), borderColor: roleProps('frame').color, paddingX: 1, width: width, children: [_jsx(Text, { bold: true, children: "Shortcuts" }), SHORTCUT_ROWS.map(([key, action]) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { ...roleProps('sap'), children: key.padEnd(KEY_COL) }), truncateToWidth(action, Math.max(1, inner - KEY_COL))] }, key))), _jsx(Text, { ...roleProps('dim'), wrap: "truncate-end", children: truncateToWidth(SHORTCUTS_NOTE, inner) }), _jsx(Text, { ...roleProps('dim'), children: "Esc close" })] }));
|
|
25
25
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { cardBorderStyle } from '../ascii-chars.js';
|
|
3
4
|
import { roleProps } from '../role-props.js';
|
|
4
5
|
export function StackFrames({ data }) {
|
|
5
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle:
|
|
6
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: cardBorderStyle(), borderColor: roleProps('bad').color, paddingX: 1, children: [_jsx(Text, { ...roleProps('bad', { bold: true }), children: "Stack Frames" }), data.map((f) => (_jsxs(Box, { flexDirection: "column", marginTop: f.frame === 0 ? 0 : 1, children: [_jsx(Box, { children: _jsxs(Text, { ...roleProps(f.type === 'error' ? 'bad' : 'dim'), children: ['#' + String(f.frame).padStart(2, '0'), " ", f.program, ":", f.line] }) }), _jsx(Box, { marginLeft: 5, children: _jsx(Text, { ...(f.type === 'error' ? {} : roleProps('dim')), dimColor: f.type === 'context', children: f.code }) })] }, f.frame)))] }));
|
|
6
7
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 2026-10-23 read retry — one more try, after a short pause, when a GET read
|
|
3
|
+
* could not connect: a connect timeout, a DNS error, or a reset before any
|
|
4
|
+
* response. A brief DNS stall on a laptop (seen in the eval, likely Docker
|
|
5
|
+
* changing adapters) clears on the second try.
|
|
6
|
+
*
|
|
7
|
+
* GET only. Never wrap /v1/messages or any POST/write with this: a retried
|
|
8
|
+
* write could run twice. It does not touch the global dispatcher, agents or
|
|
9
|
+
* DNS order, so NODE_USE_ENV_PROXY keeps working on corporate VDIs.
|
|
10
|
+
*
|
|
11
|
+
* A copy of this rule lives in cspeach-proxy/src/skills/manifest.ts
|
|
12
|
+
* (fetchManifestResponse) — separate package, no shared code. Change both.
|
|
13
|
+
*/
|
|
14
|
+
/** The error codes worth one more try. Read from `.code` only, never the message. */
|
|
15
|
+
const RETRYABLE_CODES = new Set(['UND_ERR_CONNECT_TIMEOUT', 'ENOTFOUND', 'EAI_AGAIN', 'ECONNRESET']);
|
|
16
|
+
export const CONNECT_RETRY_DELAY_MS = 500;
|
|
17
|
+
/** True when `e` (or its cause chain — undici wraps it as TypeError('fetch failed', { cause })) carries a retryable code. */
|
|
18
|
+
export function isConnectRetryable(e) {
|
|
19
|
+
let cur = e;
|
|
20
|
+
for (let depth = 0; depth < 4 && cur && typeof cur === 'object'; depth++) {
|
|
21
|
+
const code = cur.code;
|
|
22
|
+
if (typeof code === 'string' && RETRYABLE_CODES.has(code))
|
|
23
|
+
return true;
|
|
24
|
+
cur = cur.cause;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
29
|
+
/**
|
|
30
|
+
* fetch with one retry on a connect failure, for GET only. `makeInit` is
|
|
31
|
+
* called once per attempt so each try gets a fresh timeout signal; when that
|
|
32
|
+
* per-try timer fires (TimeoutError) it counts as a connect failure. A caller
|
|
33
|
+
* abort never does. An HTTP
|
|
34
|
+
* error status is a response, returned as is. After the one retry fails, the
|
|
35
|
+
* ORIGINAL error is thrown, so callers' messages stay the same.
|
|
36
|
+
*/
|
|
37
|
+
export async function fetchWithConnectRetry(url, makeInit, opts = {}) {
|
|
38
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
39
|
+
const first = makeInit();
|
|
40
|
+
try {
|
|
41
|
+
return await doFetch(url, first);
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
const method = (first.method ?? 'GET').toUpperCase();
|
|
45
|
+
if (method !== 'GET')
|
|
46
|
+
throw e;
|
|
47
|
+
const reasonName = first.signal?.aborted ? first.signal.reason?.name : undefined;
|
|
48
|
+
// Our own per-try timer fired (a stalled connect/DNS, in effect a connect
|
|
49
|
+
// timeout; it races undici's own 10 s connect timeout). makeInit hands the
|
|
50
|
+
// retry a fresh timer.
|
|
51
|
+
const ownTimerFired = reasonName === 'TimeoutError' && e?.name === 'TimeoutError';
|
|
52
|
+
if (!isConnectRetryable(e) && !ownTimerFired)
|
|
53
|
+
throw e;
|
|
54
|
+
// The caller gave up (user cancel / session end): never a second try.
|
|
55
|
+
if (first.signal?.aborted && reasonName !== 'TimeoutError')
|
|
56
|
+
throw e;
|
|
57
|
+
await (opts.sleep ?? defaultSleep)(CONNECT_RETRY_DELAY_MS);
|
|
58
|
+
try {
|
|
59
|
+
return await doFetch(url, makeInit());
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw e;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cspeach/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.53",
|
|
4
4
|
"description": "CSPeach — AI-assisted ABAP development CLI. Direct SAP access, signed skills, safety gates.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"abap",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"@anthropic-ai/sdk": "^0.90.0",
|
|
55
55
|
"@azure/msal-node": "6.0.1",
|
|
56
56
|
"@cspeach/register-core": "0.4.2",
|
|
57
|
-
"@cspeach/sap-client": "0.1.
|
|
57
|
+
"@cspeach/sap-client": "0.1.31",
|
|
58
58
|
"@iarna/toml": "^2.2.5",
|
|
59
59
|
"@inkjs/ui": "^2.0.0",
|
|
60
60
|
"@inquirer/prompts": "^7.0.0",
|