@cspeach/cli 0.9.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agent/intent-system-prompt.js +1 -1
- package/dist/agent/loop.js +228 -26
- package/dist/agent/providers/license-gate.js +44 -0
- package/dist/agent/skill-checkpoint.js +1 -1
- package/dist/agent/tool-dispatch.js +15 -0
- package/dist/approvals/canonical.js +91 -0
- package/dist/approvals/jwt.js +39 -2
- package/dist/approvals/op-labels.js +124 -0
- package/dist/approvals/render.js +42 -36
- package/dist/auth/org-anthropic-key.js +25 -0
- package/dist/classifier/client.js +18 -3
- package/dist/cli.js +15 -0
- package/dist/commands/compact.js +28 -2
- package/dist/commands/config-set.js +284 -0
- package/dist/commands/config-show.js +20 -0
- package/dist/commands/export-audit.js +43 -0
- package/dist/commands/help.js +5 -0
- package/dist/commands/login.js +31 -14
- package/dist/commands/plan-audit-evidence.js +266 -0
- package/dist/commands/plan-audit.js +692 -0
- package/dist/commands/plan-chain.js +671 -0
- package/dist/commands/plan-continue.js +179 -0
- package/dist/commands/plan-gate.js +154 -0
- package/dist/commands/plan-model-tier.js +83 -0
- package/dist/commands/plan-resume.js +728 -46
- package/dist/config/loader.js +223 -5
- package/dist/config/model-defaults.js +14 -0
- package/dist/cost/pricing.js +27 -1
- package/dist/doctor/checks/_http-probe.js +1 -0
- package/dist/doctor/checks/cert.js +14 -3
- package/dist/doctor/checks/sap.js +30 -8
- package/dist/doctor/checks/system-roles.js +41 -0
- package/dist/doctor/checks/zcspeach.js +19 -4
- package/dist/doctor/run.js +2 -0
- package/dist/models/resolve.js +61 -0
- package/dist/models/server-config.js +155 -0
- package/dist/one-shot.js +76 -6
- package/dist/projects/answer-blockers.js +137 -0
- package/dist/projects/extract-cca.js +111 -17
- package/dist/projects/extract-modernize.js +4 -2
- package/dist/projects/extract-plan.js +184 -37
- package/dist/projects/extract-spec-gap.js +34 -7
- package/dist/projects/extract-test-coverage.js +4 -2
- package/dist/projects/extract-upgrade.js +116 -23
- package/dist/projects/handover-md.js +195 -0
- package/dist/projects/index.js +5 -2
- package/dist/projects/merge-cca.js +292 -0
- package/dist/projects/merge-upgrade.js +173 -0
- package/dist/projects/migration.js +103 -1
- package/dist/projects/output-paths.js +27 -0
- package/dist/projects/plan-run.js +285 -27
- package/dist/projects/plan-schema.js +136 -3
- package/dist/projects/promote-command.js +25 -2
- package/dist/projects/promote.js +128 -0
- package/dist/projects/run-lease.js +157 -0
- package/dist/projects/save-command.js +259 -21
- package/dist/projects/status.js +3 -1
- package/dist/projects/validate.js +1 -1
- package/dist/projects/workspace.js +164 -20
- package/dist/renderer/notices.js +64 -0
- package/dist/renderer/progress-chatter.js +8 -0
- package/dist/renderer/status-footer.js +22 -12
- package/dist/renderer/thinking-heartbeat.js +64 -8
- package/dist/renderer/todo-block.js +51 -0
- package/dist/renderer/tool-widget.js +55 -4
- package/dist/renderer/tty.js +43 -4
- package/dist/renderer/verify-chain.js +77 -0
- package/dist/repl/at-picker.js +60 -7
- package/dist/repl/bracketed-paste.js +28 -19
- package/dist/repl/builtin-commands.js +42 -0
- package/dist/repl/current-transport.js +10 -0
- package/dist/repl/early-line-buffer.js +68 -0
- package/dist/repl/history.js +86 -0
- package/dist/repl/ink-stdin-guard.js +64 -0
- package/dist/repl/inquirer-guard.js +70 -5
- package/dist/repl/mode-ceiling.js +16 -0
- package/dist/repl/mode-cycle.js +104 -0
- package/dist/repl/numbered-menu.js +131 -0
- package/dist/repl/post-turn-status.js +26 -6
- package/dist/repl/rule8-detector.js +17 -2
- package/dist/repl/safety-confirm.js +111 -2
- package/dist/repl/safety-mode-state.js +19 -3
- package/dist/repl/slash-completer.js +5 -0
- package/dist/repl/slash-picker.js +10 -15
- package/dist/repl.js +1232 -95
- package/dist/rewind/candidates.js +194 -0
- package/dist/rewind/cli.js +137 -0
- package/dist/rewind/format.js +27 -0
- package/dist/rewind/restore.js +245 -0
- package/dist/router/classifier.js +150 -6
- package/dist/sap/capability-matrix.js +20 -0
- package/dist/sap/capability-matrix.json +11236 -0
- package/dist/sap/capability.js +146 -0
- package/dist/sap/connection-manager.js +19 -1
- package/dist/sap/onboarding.js +42 -4
- package/dist/session/audit-export.js +459 -0
- package/dist/session/context-report.js +163 -0
- package/dist/session/pending.js +27 -0
- package/dist/session/recap.js +160 -0
- package/dist/skill-catalog.js +51 -40
- package/dist/skills/bundled-skills.js +272 -1
- package/dist/skills/promotion-dispatch.js +23 -0
- package/dist/tools/_command-shared.js +36 -12
- package/dist/tools/_filesystem-shared.js +139 -4
- package/dist/tools/_flag.js +25 -0
- package/dist/tools/approval.js +177 -26
- package/dist/tools/ask-question.js +400 -7
- package/dist/tools/capability/tool.js +74 -0
- package/dist/tools/dispatch-skill.js +22 -1
- package/dist/tools/extend-model/anchored-insert.js +1414 -0
- package/dist/tools/extend-model/tool.js +340 -0
- package/dist/tools/filesystem/extract-document.js +57 -0
- package/dist/tools/filesystem/file-edit.js +12 -2
- package/dist/tools/filesystem/file-read.js +2 -2
- package/dist/tools/filesystem/file-write.js +11 -2
- package/dist/tools/filesystem/glob.js +11 -0
- package/dist/tools/filesystem/grep.js +10 -0
- package/dist/tools/filesystem/read-document.js +107 -0
- package/dist/tools/fiori/apply.js +50 -0
- package/dist/tools/fiori/bin.js +3 -0
- package/dist/tools/fiori/catalog/index.js +27 -0
- package/dist/tools/fiori/catalog/value-help.js +230 -0
- package/dist/tools/fiori/catalog/viz-chart.js +177 -0
- package/dist/tools/fiori/cli.js +71 -0
- package/dist/tools/fiori/deploy-config.js +73 -0
- package/dist/tools/fiori/fe-extend.js +76 -0
- package/dist/tools/fiori/fe-scaffold.js +71 -0
- package/dist/tools/fiori/floorplan-map.js +19 -0
- package/dist/tools/fiori/i18n.js +39 -0
- package/dist/tools/fiori/manifest.js +70 -0
- package/dist/tools/fiori/render.js +77 -0
- package/dist/tools/fiori/samples/data/index.json +13602 -0
- package/dist/tools/fiori/samples/data/sources.generated.js +808 -0
- package/dist/tools/fiori/samples/loader.js +248 -0
- package/dist/tools/fiori/samples/search.js +63 -0
- package/dist/tools/fiori/samples/types.js +2 -0
- package/dist/tools/fiori/scaffold.js +39 -0
- package/dist/tools/fiori/smoke/assertions.js +74 -0
- package/dist/tools/fiori/smoke/browser.js +52 -0
- package/dist/tools/fiori/smoke/driver.js +89 -0
- package/dist/tools/fiori/smoke/freestyle-spec.js +317 -0
- package/dist/tools/fiori/smoke/run-smoke.js +149 -0
- package/dist/tools/fiori/tools.js +681 -0
- package/dist/tools/fiori/types.js +1 -0
- package/dist/tools/local-build.js +86 -0
- package/dist/tools/local-files.js +31 -0
- package/dist/tools/project/_merge-shared.js +68 -0
- package/dist/tools/project/cca_merge.js +164 -0
- package/dist/tools/project/playbook_get.js +1 -1
- package/dist/tools/project/upgrade_merge_progress.js +206 -0
- package/dist/tools/sap-read.js +132 -20
- package/dist/tools/sap-write.js +550 -21
- package/dist/tools/shell/shell_exec.js +41 -6
- package/dist/tools/snapshot.js +63 -14
- package/dist/tools/subagent/agent_run.js +27 -3
- package/dist/tools/subagent/background_run.js +17 -1
- package/dist/tools/todo.js +144 -0
- package/dist/tools/transport-resolution.js +86 -0
- package/dist/tools/transport.js +224 -5
- package/dist/tools/write-mode.js +4 -0
- package/dist/ui/app.js +378 -21
- package/dist/ui/approval-modal.js +49 -16
- package/dist/ui/ask-question-emitter.js +14 -0
- package/dist/ui/body.js +13 -0
- package/dist/ui/context-grid.js +108 -0
- package/dist/ui/footer.js +120 -27
- package/dist/ui/header.js +7 -0
- package/dist/ui/line-resolution.js +35 -8
- package/dist/ui/rewind-emitter.js +10 -0
- package/dist/ui/rewind-panel.js +81 -0
- package/dist/ui/sap-state-store.js +1 -0
- package/dist/ui/session-timeline.js +1 -0
- package/dist/ui/status-line.js +43 -0
- package/dist/ui/text-input.js +214 -0
- package/dist/ui/todo-emitter.js +25 -0
- package/dist/ui/todo-panel.js +64 -0
- package/dist/ui/turn-status-emitter.js +50 -4
- package/dist/ui/turn-status.js +18 -3
- package/dist/ui/widgets/ask-form.js +242 -0
- package/dist/ui/widgets/ask-question-modal.js +21 -8
- package/package.json +22 -3
- package/bench/README.md +0 -78
- package/bench/prompts/abap-document-cds.md +0 -44
- package/bench/prompts/abap-explain-bdef-handler.md +0 -57
- package/bench/prompts/abap-test-method.md +0 -42
- package/bench/results/abap-document-cds/claude-haiku-4-5.md +0 -189
- package/bench/results/abap-document-cds/claude-opus-4-7.md +0 -120
- package/bench/results/abap-document-cds/claude-sonnet-4-6.md +0 -151
- package/bench/results/abap-explain-bdef-handler/claude-haiku-4-5.md +0 -112
- package/bench/results/abap-explain-bdef-handler/claude-opus-4-7.md +0 -101
- package/bench/results/abap-explain-bdef-handler/claude-sonnet-4-6.md +0 -101
- package/bench/results/abap-test-method/claude-haiku-4-5.md +0 -186
- package/bench/results/abap-test-method/claude-opus-4-7.md +0 -193
- package/bench/results/abap-test-method/claude-sonnet-4-6.md +0 -234
package/dist/approvals/jwt.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { SignJWT, jwtVerify } from 'jose';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
|
+
import { auditLog } from '@cspeach/sap-client';
|
|
4
|
+
import { canonicalApprovalObject, approvalObjectMatches } from './canonical.js';
|
|
3
5
|
// Session-scoped HMAC key — generated per CLI start, never persisted.
|
|
4
6
|
const sessionKey = crypto.randomBytes(32);
|
|
5
7
|
const usedNonces = new Set();
|
|
@@ -43,6 +45,24 @@ export async function mintApprovalId(payload) {
|
|
|
43
45
|
.setExpirationTime('30m')
|
|
44
46
|
.sign(sessionKey);
|
|
45
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* THE minting path for request_approval changes (Task A3 — D17/D21/D28).
|
|
50
|
+
*
|
|
51
|
+
* The object string stored in the JWT is canonicalized by the same helper the
|
|
52
|
+
* spend-side comparison in verifyAndSpendApprovalId uses, so mint and spend
|
|
53
|
+
* can never disagree on decoration ("Transport: …", "(testclasses include)"),
|
|
54
|
+
* case, or whitespace. request_approval and any future minting site MUST go
|
|
55
|
+
* through this function instead of calling mintApprovalId with a free-text
|
|
56
|
+
* object string.
|
|
57
|
+
*/
|
|
58
|
+
export async function mintChangeApproval(change, sessionId) {
|
|
59
|
+
return mintApprovalId({
|
|
60
|
+
object: canonicalApprovalObject(change.object),
|
|
61
|
+
type: change.type,
|
|
62
|
+
op: change.op,
|
|
63
|
+
session_id: sessionId,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
46
66
|
export async function verifyAndSpendApprovalId(jwt, expectedObject, expectedOp) {
|
|
47
67
|
// 2026-05-15 (bug 7): the previous catch lumped EVERY thrown failure
|
|
48
68
|
// into `'invalid_signature'`, including expirations (which used to be
|
|
@@ -75,8 +95,19 @@ export async function verifyAndSpendApprovalId(jwt, expectedObject, expectedOp)
|
|
|
75
95
|
// taxonomy short. Mismatch / spent-nonce paths NEVER reach this catch.
|
|
76
96
|
return { ok: false, reason: 'invalid_signature' };
|
|
77
97
|
}
|
|
78
|
-
|
|
98
|
+
// Canonical comparison (Task A3 — D17/D21/D28): both sides are normalized
|
|
99
|
+
// by the same helper that mintChangeApproval used at mint time, and a
|
|
100
|
+
// comma-joined batch approval matches each of its member objects. The spend
|
|
101
|
+
// side canonicalizes as well because (a) `expectedObject` is always the raw
|
|
102
|
+
// structured tool argument, never pre-canonicalized, and (b) it is
|
|
103
|
+
// defense-in-depth against any future mint site that bypasses
|
|
104
|
+
// mintChangeApproval and stores a raw string. (It is NOT back-compat for
|
|
105
|
+
// old JWTs: the HMAC key is per-process and never persisted, so no
|
|
106
|
+
// pre-canonicalization token can ever reach this verifier.)
|
|
107
|
+
const matchKind = approvalObjectMatches(payload.object, expectedObject);
|
|
108
|
+
if (!matchKind) {
|
|
79
109
|
return { ok: false, reason: 'object_mismatch' };
|
|
110
|
+
}
|
|
80
111
|
// Op-group coverage: a create approval also authorizes the modify+activate
|
|
81
112
|
// that complete the create; a modify approval authorizes its activate.
|
|
82
113
|
const covered = OP_COVERAGE[payload.op];
|
|
@@ -90,5 +121,11 @@ export async function verifyAndSpendApprovalId(jwt, expectedObject, expectedOp)
|
|
|
90
121
|
return { ok: false, reason: 'nonce_spent' };
|
|
91
122
|
usedNonces.add(payload.nonce);
|
|
92
123
|
}
|
|
93
|
-
|
|
124
|
+
// Auditability: a lenient (non-byte-equal) match is an authorization
|
|
125
|
+
// decision worth a trace — record WHICH fold let the spend through, in the
|
|
126
|
+
// same audit log the write tools use.
|
|
127
|
+
if (matchKind !== 'exact') {
|
|
128
|
+
await auditLog('approvalSpend', payload.type, expectedObject, 'success', 0, `matchKind=${matchKind} minted="${payload.object}"`);
|
|
129
|
+
}
|
|
130
|
+
return { ok: true, payload, matchKind };
|
|
94
131
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable operation + object labels for the approval surfaces.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the cryptic op-code glyph (`A`/`M`/`D`/`>`/`^`) the live review
|
|
5
|
+
* found dishonest and information-poor. Every approval box, plan gate, and
|
|
6
|
+
* classic prompt now speaks a verb ("Create table ZMNT_NOTE") instead of a
|
|
7
|
+
* one-letter code where a word belongs.
|
|
8
|
+
*/
|
|
9
|
+
const VERBS = {
|
|
10
|
+
create: 'Create',
|
|
11
|
+
modify: 'Change',
|
|
12
|
+
delete: 'Delete',
|
|
13
|
+
activate: 'Activate',
|
|
14
|
+
release: 'Release',
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* SAP ADT type codes → plain words. The `type` field is free text supplied by
|
|
18
|
+
* the model, so an unknown/decorated value (e.g. "testclasses include") falls
|
|
19
|
+
* through unchanged rather than being mangled.
|
|
20
|
+
*/
|
|
21
|
+
const TYPE_WORDS = {
|
|
22
|
+
TABL: 'table',
|
|
23
|
+
CLAS: 'class',
|
|
24
|
+
INTF: 'interface',
|
|
25
|
+
PROG: 'program',
|
|
26
|
+
FUGR: 'function group',
|
|
27
|
+
DDLS: 'CDS view',
|
|
28
|
+
DDLX: 'metadata extension',
|
|
29
|
+
DCLS: 'access control',
|
|
30
|
+
BDEF: 'behavior definition',
|
|
31
|
+
SRVD: 'service definition',
|
|
32
|
+
SRVB: 'service binding',
|
|
33
|
+
DTEL: 'data element',
|
|
34
|
+
DOMA: 'domain',
|
|
35
|
+
STRU: 'structure',
|
|
36
|
+
TTYP: 'table type',
|
|
37
|
+
MSAG: 'message class',
|
|
38
|
+
DEVC: 'package',
|
|
39
|
+
ENHO: 'enhancement',
|
|
40
|
+
XSLT: 'transformation',
|
|
41
|
+
SHLP: 'search help',
|
|
42
|
+
};
|
|
43
|
+
export function humanVerb(op) {
|
|
44
|
+
return VERBS[op] ?? op.charAt(0).toUpperCase() + op.slice(1);
|
|
45
|
+
}
|
|
46
|
+
export function humanType(type) {
|
|
47
|
+
if (!type)
|
|
48
|
+
return '';
|
|
49
|
+
const key = type.trim().toUpperCase();
|
|
50
|
+
if (TYPE_WORDS[key])
|
|
51
|
+
return TYPE_WORDS[key];
|
|
52
|
+
// Already a word (has a lowercase letter or a space)? use it verbatim.
|
|
53
|
+
if (/[a-z ]/.test(type))
|
|
54
|
+
return type;
|
|
55
|
+
return type.toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
/** e.g. "Create table ZMNT_NOTE", "Change class ZCL_X", "Delete table ZOLD". */
|
|
58
|
+
export function verbLine(change) {
|
|
59
|
+
return [humanVerb(change.op), humanType(change.type), change.object]
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.join(' ');
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The transport to DISPLAY on the approval surface for one change — mirrors
|
|
65
|
+
* how the write tools actually resolve it (traced in sap-write.ts, 2026-07-05
|
|
66
|
+
* review):
|
|
67
|
+
* - create-family (sap_create_object / set_class_include / create_domain /
|
|
68
|
+
* create_data_element): `args.transport ?? session current` — so the
|
|
69
|
+
* session fallback is honest to show.
|
|
70
|
+
* - modify/delete (sap_set_source / sap_update_method / sap_delete_object):
|
|
71
|
+
* resolveWriteTransport(args only, + owning-TR override) — NO session
|
|
72
|
+
* fallback in the actual write, so we only show a transport the request
|
|
73
|
+
* explicitly carries.
|
|
74
|
+
* - activate/release: the tools take no transport arg at all — never show
|
|
75
|
+
* a Transport segment.
|
|
76
|
+
* When nothing is resolvable, return undefined and the caller OMITS the
|
|
77
|
+
* segment — no "new (session)" promise (a $TMP write never creates one).
|
|
78
|
+
*/
|
|
79
|
+
export function displayTransport(op, explicit, session) {
|
|
80
|
+
switch (op) {
|
|
81
|
+
case 'create':
|
|
82
|
+
return explicit ?? session ?? undefined;
|
|
83
|
+
case 'modify':
|
|
84
|
+
case 'delete':
|
|
85
|
+
return explicit;
|
|
86
|
+
default: // activate / release
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* A create/delete "diff" is really a source dump (all-adds, or plain source).
|
|
92
|
+
* Strip unified-diff scaffolding (hunk + file headers) and the leading +/-
|
|
93
|
+
* marker so View source shows clean source — no +/- colouring that would
|
|
94
|
+
* imply a before/after that does not exist for a create. Shared by the Ink
|
|
95
|
+
* modal's [v] view and the classic prompt's View source branch.
|
|
96
|
+
*/
|
|
97
|
+
export function toSourceLines(content) {
|
|
98
|
+
return content
|
|
99
|
+
.split('\n')
|
|
100
|
+
.filter((l) => !l.startsWith('@@') && !l.startsWith('+++') && !l.startsWith('---'))
|
|
101
|
+
.map((l) => l.replace(/^[+-] ?/, ''));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The review key that MATCHES the operation — honesty over a blanket "Show
|
|
105
|
+
* diff":
|
|
106
|
+
* - modify → [d] Show diff (there IS a before/after to diff)
|
|
107
|
+
* - create → [v] View source, only when the new source is carried
|
|
108
|
+
* - delete → [v] View current source, only when the source is carried
|
|
109
|
+
* - activate/release → nothing to review (no source in the payload)
|
|
110
|
+
*/
|
|
111
|
+
export function viewAffordance(change) {
|
|
112
|
+
switch (change.op) {
|
|
113
|
+
case 'modify':
|
|
114
|
+
// Existing behaviour: [d] always offered; the renderer warns when the
|
|
115
|
+
// model supplied a description rather than a real unified diff.
|
|
116
|
+
return { key: 'd', label: 'Show diff' };
|
|
117
|
+
case 'create':
|
|
118
|
+
return change.diff ? { key: 'v', label: 'View source' } : null;
|
|
119
|
+
case 'delete':
|
|
120
|
+
return change.diff ? { key: 'v', label: 'View current source' } : null;
|
|
121
|
+
default:
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
package/dist/approvals/render.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { select, input } from '@inquirer/prompts';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import { verbLine, viewAffordance, toSourceLines } from './op-labels.js';
|
|
3
4
|
import { shouldUseInk } from '../renderer/tty.js';
|
|
4
5
|
import { approvalEmitter } from '../ui/approval-emitter.js';
|
|
5
6
|
import { askQuestionEmitter } from '../ui/ask-question-emitter.js';
|
|
@@ -7,26 +8,6 @@ import { withInquirer } from '../repl/inquirer-guard.js';
|
|
|
7
8
|
import { inquirerTheme } from '../repl/inquirer-theme.js';
|
|
8
9
|
import { PEACH } from '../renderer/banners.js';
|
|
9
10
|
const peach = (s) => chalk.hex(PEACH)(s);
|
|
10
|
-
/**
|
|
11
|
-
* Op-row glyph. 2026-05-01: dropped the per-op color spread (yellow M,
|
|
12
|
-
* green A, blue >, magenta ^). Using 5 different hues for routine plan
|
|
13
|
-
* rows trains the user to ignore color as semantic signal — by the time
|
|
14
|
-
* an actual delete shows up, red has already appeared on three other
|
|
15
|
-
* lines and lost its punch. Now: only `D` (delete) gets red, because
|
|
16
|
-
* delete is the only op that's destructive enough to deserve a hot
|
|
17
|
-
* accent. Everything else renders dim — the row layout already tells
|
|
18
|
-
* the user it's a change row.
|
|
19
|
-
*/
|
|
20
|
-
function opIcon(op) {
|
|
21
|
-
switch (op) {
|
|
22
|
-
case 'create': return chalk.dim('A');
|
|
23
|
-
case 'modify': return chalk.dim('M');
|
|
24
|
-
case 'delete': return chalk.red('D');
|
|
25
|
-
case 'activate': return chalk.dim('>');
|
|
26
|
-
case 'release': return chalk.dim('^');
|
|
27
|
-
default: return chalk.dim('?');
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
11
|
/**
|
|
31
12
|
* Risk tag. 2026-05-01: yellow used to fire on `medium` too, which
|
|
32
13
|
* meant most plans had a yellow tag — defeating yellow's purpose as
|
|
@@ -119,12 +100,14 @@ export async function renderPlanGate(summary, changes, transport, effectiveRisk)
|
|
|
119
100
|
if (effectiveRisk.raisedBy.length > 0) {
|
|
120
101
|
ctx.push(`(raised from low: ${effectiveRisk.raisedBy.join(', ')})`);
|
|
121
102
|
}
|
|
122
|
-
|
|
123
|
-
|
|
103
|
+
// Numbered, operation-aware verb rows ("1. Create table ZMNT_NOTE") —
|
|
104
|
+
// no cryptic op-code glyph. The Ink modal renders `context` itself, so
|
|
105
|
+
// no chalk here.
|
|
106
|
+
visibleChanges.forEach((c, i) => ctx.push(` ${i + 1}. ${verbLine(c)}`));
|
|
124
107
|
const result = await askQuestionEmitter.request({
|
|
125
108
|
id: 'plan-gate',
|
|
126
109
|
question: 'Apply this plan?',
|
|
127
|
-
// Strip chalk ANSI from
|
|
110
|
+
// Strip chalk ANSI from riskTag — the Ink modal renders `context`
|
|
128
111
|
// itself (dimColor), so embedded escape codes would show as raw text.
|
|
129
112
|
context: ctx.join('\n').replace(/\x1b\[[0-9;]*m/g, ''),
|
|
130
113
|
kind: 'choice',
|
|
@@ -149,9 +132,9 @@ export async function renderPlanGate(summary, changes, transport, effectiveRisk)
|
|
|
149
132
|
if (effectiveRisk.raisedBy.length > 0) {
|
|
150
133
|
console.log(` ${chalk.dim(`(raised from low: ${effectiveRisk.raisedBy.join(', ')})`)}`);
|
|
151
134
|
}
|
|
152
|
-
|
|
153
|
-
console.log(` ${
|
|
154
|
-
}
|
|
135
|
+
visibleChanges.forEach((c, i) => {
|
|
136
|
+
console.log(` ${chalk.dim(`${i + 1}.`)} ${verbLine(c)}`);
|
|
137
|
+
});
|
|
155
138
|
console.log('');
|
|
156
139
|
const choice = await withInquirer(() => select({
|
|
157
140
|
message: 'Apply?',
|
|
@@ -173,17 +156,20 @@ export async function renderPerChangeApproval(change, effectiveRisk, transport)
|
|
|
173
156
|
// fires standalone (single-change request_approval) AND risk is non-low.
|
|
174
157
|
void effectiveRisk;
|
|
175
158
|
void transport;
|
|
176
|
-
|
|
159
|
+
// Verb-first label ("Create table ZMNT_NOTE — apply?") — no op-code glyph.
|
|
160
|
+
const objLabel = verbLine(change);
|
|
161
|
+
// Op-aware review choice: [d] Show diff for modify, [v] View source for
|
|
162
|
+
// create/delete-with-source, omitted for activate/release.
|
|
163
|
+
const aff = viewAffordance(change);
|
|
164
|
+
const choices = [{ value: 'apply', name: 'Apply' }];
|
|
165
|
+
if (aff)
|
|
166
|
+
choices.push({ value: 'diff', name: aff.label });
|
|
167
|
+
choices.push({ value: 'skip', name: 'Skip (tell model why)' }, { value: 'cancel', name: 'Cancel turn' });
|
|
177
168
|
const choice = await withInquirer(() => select({
|
|
178
169
|
message: `${objLabel} — apply?`,
|
|
179
170
|
default: 'apply',
|
|
180
171
|
theme: inquirerTheme,
|
|
181
|
-
choices
|
|
182
|
-
{ value: 'apply', name: 'Apply' },
|
|
183
|
-
{ value: 'diff', name: 'Show diff' },
|
|
184
|
-
{ value: 'skip', name: 'Skip (tell model why)' },
|
|
185
|
-
{ value: 'cancel', name: 'Cancel turn' },
|
|
186
|
-
],
|
|
172
|
+
choices,
|
|
187
173
|
}));
|
|
188
174
|
if (choice === 'apply')
|
|
189
175
|
return { approved: true };
|
|
@@ -193,9 +179,29 @@ export async function renderPerChangeApproval(change, effectiveRisk, transport)
|
|
|
193
179
|
const reason = await withInquirer(() => input({ message: 'Why skip?', theme: inquirerTheme }));
|
|
194
180
|
return { approved: false, reason };
|
|
195
181
|
}
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
182
|
+
// Review choice.
|
|
183
|
+
//
|
|
184
|
+
// I3 (2026-07-05 review): a 'v' affordance (create/delete View source) is
|
|
185
|
+
// NOT a diff — the payload is plain/all-adds source. Routing it through the
|
|
186
|
+
// diff printer fired the "⚠ Model did not provide a unified diff" warning
|
|
187
|
+
// under a View source label. Print numbered plain source instead — never
|
|
188
|
+
// the diff path or its warning. (The affordance is only offered when
|
|
189
|
+
// change.diff is present, so the fallback line is belt-and-braces.)
|
|
190
|
+
if (aff?.key === 'v') {
|
|
191
|
+
if (change.diff) {
|
|
192
|
+
const numbered = toSourceLines(change.diff)
|
|
193
|
+
.map((line, i) => `${chalk.dim(String(i + 1).padStart(4) + ' ')}${line}`)
|
|
194
|
+
.join('\n');
|
|
195
|
+
console.log('\n' + numbered + '\n');
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
console.log(chalk.dim('(No source provided by the model)'));
|
|
199
|
+
}
|
|
200
|
+
return renderPerChangeApproval(change, effectiveRisk, transport);
|
|
201
|
+
}
|
|
202
|
+
// 'd' (modify) — print colorized + lines (green) / - lines (red) / hunk
|
|
203
|
+
// headers (peach) so the user can scan the change at a glance instead of
|
|
204
|
+
// squinting at flat-gray text.
|
|
199
205
|
//
|
|
200
206
|
// 2026-05-01: detect "model sent a description, not a diff" — when the
|
|
201
207
|
// payload has no +/-/@ lines and no `--- /+++ ` headers, colorizeDiff
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch the organisation's Anthropic API key from the CSPeach proxy.
|
|
3
|
+
*
|
|
4
|
+
* Returns the key string on success, or null on any failure.
|
|
5
|
+
* Non-fatal by design — callers must fall back to the manual prompt when null
|
|
6
|
+
* is returned. The key is NEVER logged.
|
|
7
|
+
*/
|
|
8
|
+
export async function fetchOrgAnthropicKey(proxyUrl, bearer, deps) {
|
|
9
|
+
const fetchFn = deps?.fetchImpl ?? fetch;
|
|
10
|
+
const url = `${proxyUrl.replace(/\/$/, '')}/v1/me/anthropic-key`;
|
|
11
|
+
try {
|
|
12
|
+
const r = await fetchFn(url, {
|
|
13
|
+
headers: { Authorization: `Bearer ${bearer}` },
|
|
14
|
+
});
|
|
15
|
+
if (!r.ok)
|
|
16
|
+
return null;
|
|
17
|
+
const body = await r.json();
|
|
18
|
+
if (typeof body.key === 'string' && body.key.length > 0)
|
|
19
|
+
return body.key;
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -8,8 +8,10 @@ const RULES = [
|
|
|
8
8
|
{ skill: 'abap-document', patterns: [/developer\s+documentation|document\s+this\s+(class|program|object)/i] },
|
|
9
9
|
// abap-incident MUST precede dump rules — pipeline framing beats keyword-only matches
|
|
10
10
|
{ skill: 'abap-incident', patterns: [/incident|resolve.*dump.*to.*fix|pipeline.*(short\s+dump|st22|dump)|(dump|st22).*to.*transport|formal\s+(incident|dump)\s+report|end.to.end.*dump/i] },
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// abap-dump (2026-06-12) — merger of abap-dump-analyze + abap-dump-triage.
|
|
12
|
+
// Triage-vs-fix is an intent decision INSIDE the skill now, so both
|
|
13
|
+
// pattern sets route here.
|
|
14
|
+
{ skill: 'abap-dump', patterns: [/short\s+dump|runtime\s+error|\bst22\b|dump\s+(analys|triage|forensic)|structured\s+investigation.*dump/i] },
|
|
13
15
|
{ skill: 'abap-eml', patterns: [/\beml\b|entity\s+manipulation|behavior\s+handler/i] },
|
|
14
16
|
{ skill: 'abap-enhance', patterns: [/\bbadi\b|enhancement\s+spot|implicit\s+enhancement|customer\s+exit|user\s+exit|(hook\s+into|extend)\s+(sap|standard|me\d+|migo|miro|va\d+|vf\d+|vl\d+|fb\d+)/i] },
|
|
15
17
|
{ skill: 'abap-estimate', patterns: [/effort\s+estimat|ticket\s+estimat|size\s+this\s+(work|task|ticket|story)|how\s+(many|long).*(hours|days).*(build|develop|implement)/i] },
|
|
@@ -20,11 +22,24 @@ const RULES = [
|
|
|
20
22
|
// abap-modernize MUST precede abap-rap so "rebuild as RAP" / "convert to RAP" stays
|
|
21
23
|
// on modernize (rebuild intent) instead of falling to abap-rap's bare \brap\b match.
|
|
22
24
|
{ skill: 'abap-modernize', patterns: [/modernise|modernize|classic.*to.*modern|convert.*to\s+(rap|fiori|odata)|rebuild.*as\s+(rap|fiori)/i] },
|
|
25
|
+
// abap-fiori-build (D12, 2026-06-12) MUST follow abap-modernize ("convert
|
|
26
|
+
// this WRITE report to Fiori" = rebuild intent) and precede abap-rap so
|
|
27
|
+
// "fiori app for my RAP/OData service" lands on the UI skill, not the
|
|
28
|
+
// backend scaffolder.
|
|
29
|
+
{ skill: 'abap-fiori-build', patterns: [/\bfiori\b|\b(sap)?ui5\b|freestyle\s+app|\bui\s+app\b|front.?end\s+(for|on)\s/i] },
|
|
23
30
|
// abap-rap MUST precede abap-generate so RAP-stack prompts route to rap, not the
|
|
24
31
|
// generic generator (cluster I, 2026-05-03). Also captures "scaffold a CDS view +
|
|
25
32
|
// BDEF + service binding" type prompts which collide with abap-generate.
|
|
26
33
|
{ skill: 'abap-rap', patterns: [/\brap\b|business\s+object(?!s?\s+for)|bdef|service\s+binding|srvb|scaffold.*(cds|odata\s+v4|bdef|behavior)|odata\s+v4\s+service/i] },
|
|
27
|
-
|
|
34
|
+
// Daily-edit phrasings (D12, 2026-06-12): "add a validation to ZCL_X",
|
|
35
|
+
// "change/fix ZFOO" are abap-generate work — the old rules only matched
|
|
36
|
+
// explicit "generate"/"create" verbs, so daily edits fell through and the
|
|
37
|
+
// coaching picker offered enhance/refactor/test instead.
|
|
38
|
+
{ skill: 'abap-generate', patterns: [
|
|
39
|
+
/\bgenerate\b|create\s+(a\s+)?(new\s+)?(class|program|report|include)|build.*from\s+scratch/i,
|
|
40
|
+
/\badd\s+(a\s+|an\s+)?[\w\s-]{1,40}\bto\s+z[_a-z0-9]+/i,
|
|
41
|
+
/\b(change|fix|update|modify|adjust)\s+(the\s+)?z[_a-z0-9]+\b/i,
|
|
42
|
+
] },
|
|
28
43
|
{ skill: 'abap-impact', patterns: [/impact\s+analysis|where.*used|what.*affected\s+by/i] },
|
|
29
44
|
// abap-upgrade-* must precede abap-migrate
|
|
30
45
|
{ skill: 'abap-upgrade-verify', patterns: [/upgrade\s+verify|remediation\s+report|verify\s+upgrade/i] },
|
package/dist/cli.js
CHANGED
|
@@ -62,6 +62,20 @@ export async function main(argv) {
|
|
|
62
62
|
}
|
|
63
63
|
return 0;
|
|
64
64
|
}
|
|
65
|
+
if (args[0] === 'config' && args[1] === 'unset') {
|
|
66
|
+
const { runConfigUnset, ConfigSetError } = await import('./commands/config-set.js');
|
|
67
|
+
try {
|
|
68
|
+
await runConfigUnset(args.slice(2));
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
if (e instanceof ConfigSetError) {
|
|
72
|
+
console.error(chalk.red(`✗ ${e.message}`));
|
|
73
|
+
process.exit(e.exitCode);
|
|
74
|
+
}
|
|
75
|
+
throw e;
|
|
76
|
+
}
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
65
79
|
if (args[0] === 'config' && args[1] === 'show') {
|
|
66
80
|
const { runConfigShow } = await import('./commands/config-show.js');
|
|
67
81
|
await runConfigShow(args.slice(2));
|
|
@@ -134,6 +148,7 @@ Usage:
|
|
|
134
148
|
cspeach config add-sap Add a SAP system interactively
|
|
135
149
|
cspeach configure Alias for \`cspeach config add-sap\`
|
|
136
150
|
cspeach config set <key> <value> Update a config key
|
|
151
|
+
cspeach config unset <key> Remove a config key (revert to default)
|
|
137
152
|
cspeach config show [key] Show config (full or single key)
|
|
138
153
|
cspeach doctor Diagnose install + connectivity
|
|
139
154
|
cspeach login Sign in via device-code flow
|
package/dist/commands/compact.js
CHANGED
|
@@ -161,6 +161,24 @@ export async function backupBeforeCompact(session) {
|
|
|
161
161
|
await fs.writeFile(filePath, JSON.stringify(session, null, 2), 'utf-8');
|
|
162
162
|
return filePath;
|
|
163
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* UX Wave 2 / Task 4 — render session.todos as a plain-text,
|
|
166
|
+
* status-prefixed block for the compacted context. Returns '' when there
|
|
167
|
+
* is no list (undefined or empty) so callers can append unconditionally.
|
|
168
|
+
*
|
|
169
|
+
* Format (explicit status words, not glyphs — the reader is the NEXT
|
|
170
|
+
* turn's model, and `[in_progress]` is unambiguous where `[>]` isn't):
|
|
171
|
+
*
|
|
172
|
+
* Current task list:
|
|
173
|
+
* - [completed] Create table ZORDER
|
|
174
|
+
* - [in_progress] Write behavior definition
|
|
175
|
+
* - [pending] Publish service binding
|
|
176
|
+
*/
|
|
177
|
+
export function renderTodoSection(todos) {
|
|
178
|
+
if (!todos || todos.length === 0)
|
|
179
|
+
return '';
|
|
180
|
+
return ['', 'Current task list:', ...todos.map((t) => `- [${t.status}] ${t.text}`)].join('\n');
|
|
181
|
+
}
|
|
164
182
|
/**
|
|
165
183
|
* The compacted "synthesised earlier history" message that replaces the
|
|
166
184
|
* summarised slice in session.messages. Role=user because (a) Anthropic
|
|
@@ -168,9 +186,16 @@ export async function backupBeforeCompact(session) {
|
|
|
168
186
|
* makes it visually distinct from the model's prior assistant outputs, and
|
|
169
187
|
* (c) the explicit "context compacted by /compact" framing tells the next
|
|
170
188
|
* turn's model exactly what it's looking at.
|
|
189
|
+
*
|
|
190
|
+
* UX Wave 2 / Task 4: when `todos` is provided (runCompact passes
|
|
191
|
+
* session.todos), a `Current task list:` section is appended after the
|
|
192
|
+
* summary so the model retains its plan across /compact — the summarised
|
|
193
|
+
* turns are where the list was built, and Haiku isn't trusted to
|
|
194
|
+
* reproduce it verbatim.
|
|
171
195
|
*/
|
|
172
|
-
export function buildSummaryMessage(summary, droppedTurns) {
|
|
196
|
+
export function buildSummaryMessage(summary, droppedTurns, todos) {
|
|
173
197
|
const ts = new Date().toISOString();
|
|
198
|
+
const todoSection = renderTodoSection(todos);
|
|
174
199
|
return {
|
|
175
200
|
role: 'user',
|
|
176
201
|
content: [
|
|
@@ -180,6 +205,7 @@ export function buildSummaryMessage(summary, droppedTurns) {
|
|
|
180
205
|
`to ~/.cspeach/sessions/${'<session-id>'}.pre-compact-<timestamp>.json]`,
|
|
181
206
|
'',
|
|
182
207
|
summary,
|
|
208
|
+
...(todoSection ? [todoSection] : []),
|
|
183
209
|
].join('\n'),
|
|
184
210
|
};
|
|
185
211
|
}
|
|
@@ -228,7 +254,7 @@ export async function runCompact(opts) {
|
|
|
228
254
|
// which it will be unless the session is very long. (For very long sessions
|
|
229
255
|
// we accept losing the original first prompt because the summary should
|
|
230
256
|
// mention what the goal was.)
|
|
231
|
-
const summaryMsg = buildSummaryMessage(summary, plan.toSummarise.length);
|
|
257
|
+
const summaryMsg = buildSummaryMessage(summary, plan.toSummarise.length, session.todos);
|
|
232
258
|
session.messages = [summaryMsg, ...plan.toKeep];
|
|
233
259
|
await saveSession(session);
|
|
234
260
|
return {
|