@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/tools/sap-write.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* lock(type, name) → Promise<LockResult> line 1914
|
|
16
16
|
* unlock(type, name, lockHandle) → Promise<void> line 1949
|
|
17
17
|
* serviceBindingPublish(name) → Promise<{status,error?}> line 2658
|
|
18
|
+
* serviceBindingUnpublish(name) → Promise<{status,error?}>
|
|
18
19
|
* messageMaintain (STUB — not yet in sap-client)
|
|
19
20
|
* numberRangeIntervals (STUB — not yet in sap-client)
|
|
20
21
|
*
|
|
@@ -24,18 +25,65 @@ import crypto from 'node:crypto';
|
|
|
24
25
|
import { registerTool } from './index.js';
|
|
25
26
|
import { verifyAndSpendApprovalId } from '../approvals/jwt.js';
|
|
26
27
|
import { autoSnapshot } from './snapshot.js';
|
|
28
|
+
import { buildWriteChainLine } from '../renderer/verify-chain.js';
|
|
27
29
|
import { verifySyntax, verifyActive } from './verify.js';
|
|
28
30
|
import { appendPending, finalizeToolCall } from '../session/pending.js';
|
|
29
31
|
import { auditLog } from '@cspeach/sap-client';
|
|
30
32
|
import { ERR } from '../errors/codes.js';
|
|
31
33
|
import { interceptUpdateMethod, CancelledByUserError, } from './update-method-intercept.js';
|
|
34
|
+
import { resolveWriteTransport } from './transport-resolution.js';
|
|
32
35
|
import { presentSafetyConfirmation } from '../repl/safety-confirm.js';
|
|
33
36
|
import { isSafetyConfirmEnabled } from '../repl/safety-mode-state.js';
|
|
37
|
+
// ── Transcript visibility helpers (D16-display + tour slide-7, 2026-06-12) ──
|
|
38
|
+
//
|
|
39
|
+
// Rule 7 auto-snapshots always fired but the transcript never said so
|
|
40
|
+
// (battery D16-display). And the tour shows writes as
|
|
41
|
+
// "① snapshot ✓ ② written ✓ ③ syntax ✓ ④ activated ✓" while real
|
|
42
|
+
// transcripts showed an opaque tool row. Both lines below are HUMAN-stream
|
|
43
|
+
// only (ctx.chunkEmitter) — tool RESULT payloads the model sees are
|
|
44
|
+
// unchanged, and subagent ctxs (no chunkEmitter) stay silent.
|
|
45
|
+
/** Dim one-liner when the Rule 7 auto-snapshot actually captured source. */
|
|
46
|
+
function emitSnapshotNotice(ctx, snap, type, name) {
|
|
47
|
+
if (snap?.taken && snap.entry) {
|
|
48
|
+
ctx.chunkEmitter?.emit('info', `snapshot ✓ ${type} ${name} ${snap.entry.timestamp}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Distinct LOUD note when the write proceeds against a fresh/never-activated
|
|
52
|
+
* shell that has no committed active version — nothing to snapshot (Rule 7 is
|
|
53
|
+
* satisfied because there is no prior source to protect). Kept separate from
|
|
54
|
+
* the 'new_object' (genuine 404) path so the transcript is honest about which
|
|
55
|
+
* case it was — this is the RAP behaviour-pool ZBP first-write. */
|
|
56
|
+
function emitNewShellNote(ctx, type, name) {
|
|
57
|
+
ctx.chunkEmitter?.emit('warn', `${type} ${name}: new/empty shell — no active version, no prior source to snapshot; proceeding with first write`);
|
|
58
|
+
}
|
|
59
|
+
/** Map the snapshot-gate outcome onto the chain line's honest states.
|
|
60
|
+
* `undefined` covers the best-effort gates whose failure was swallowed.
|
|
61
|
+
* 'object_not_found' is a genuine ADT 404 only; any other read failure maps
|
|
62
|
+
* to 'source_read_failed' (+ short detail) — never to "(new object)".
|
|
63
|
+
* 'new_shell' (exists but no active version — nothing to save) reuses the
|
|
64
|
+
* 'new_object' chain glyph; the honest which-case distinction is carried by
|
|
65
|
+
* the LOUD emitNewShellNote line, not the compact chain. */
|
|
66
|
+
function snapshotChainState(snap) {
|
|
67
|
+
if (snap?.taken)
|
|
68
|
+
return { snapshot: 'taken' };
|
|
69
|
+
if (snap?.reason === 'object_not_found' || snap?.reason === 'new_shell') {
|
|
70
|
+
return { snapshot: 'new_object' };
|
|
71
|
+
}
|
|
72
|
+
if (snap?.reason === 'source_read_failed') {
|
|
73
|
+
return { snapshot: 'source_read_failed', snapshotDetail: snap.detail };
|
|
74
|
+
}
|
|
75
|
+
return { snapshot: 'best_effort_failed' };
|
|
76
|
+
}
|
|
77
|
+
/** Compact ①②③④ pipeline summary — derived from the real gate results. */
|
|
78
|
+
function emitWriteChain(ctx, input) {
|
|
79
|
+
ctx.chunkEmitter?.emit('chunk', buildWriteChainLine(input) + '\n');
|
|
80
|
+
}
|
|
34
81
|
registerTool({
|
|
35
82
|
name: 'sap_set_source',
|
|
36
83
|
description: 'Overwrite the full source of an ABAP object. Requires approval_id. '
|
|
37
84
|
+ 'Use for class-definition changes, new methods, or any change not confined to '
|
|
38
|
-
+ 'a single method body. For method-body-only edits use sap_update_method (Rule 7a).'
|
|
85
|
+
+ 'a single method body. For method-body-only edits use sap_update_method (Rule 7a). '
|
|
86
|
+
+ 'The transport auto-resolves to the owning request if the object is already locked.',
|
|
39
87
|
isMutating: true,
|
|
40
88
|
input_schema: {
|
|
41
89
|
type: 'object',
|
|
@@ -57,7 +105,7 @@ registerTool({
|
|
|
57
105
|
is_error: true,
|
|
58
106
|
};
|
|
59
107
|
}
|
|
60
|
-
const toolUseId = crypto.randomUUID();
|
|
108
|
+
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
61
109
|
const startedAt = Date.now();
|
|
62
110
|
// ── Rule 9 conflict warning ──────────────────────────────────────────────
|
|
63
111
|
// Emit a warning if the LLM passed an explicit transport that differs from
|
|
@@ -69,9 +117,16 @@ registerTool({
|
|
|
69
117
|
ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
|
|
70
118
|
}
|
|
71
119
|
}
|
|
72
|
-
// ──
|
|
120
|
+
// ── Rule 9 owning-transport check (D15/D20) ──────────────────────────────
|
|
121
|
+
// If the object is already locked in an open request, write under THAT
|
|
122
|
+
// request — any other transport is a guaranteed CTS lock conflict.
|
|
123
|
+
const resolved = await resolveWriteTransport(ctx, args.type, args.name, args.transport);
|
|
124
|
+
// ── Gate 2: Snapshot (Rule 7) — HARD gate ────────────────────────────────
|
|
125
|
+
// Only a genuine 404 (new object) may proceed snapshot-less. A read
|
|
126
|
+
// failure on a possibly-EXISTING object means no recovery copy → block.
|
|
127
|
+
let snapOutcome;
|
|
73
128
|
try {
|
|
74
|
-
await autoSnapshot(args.name, args.type, ctx.adt);
|
|
129
|
+
snapOutcome = await autoSnapshot(args.name, args.type, ctx.adt);
|
|
75
130
|
}
|
|
76
131
|
catch (err) {
|
|
77
132
|
return {
|
|
@@ -79,6 +134,22 @@ registerTool({
|
|
|
79
134
|
is_error: true,
|
|
80
135
|
};
|
|
81
136
|
}
|
|
137
|
+
if (snapOutcome?.reason === 'source_read_failed') {
|
|
138
|
+
return {
|
|
139
|
+
content: JSON.stringify({
|
|
140
|
+
error: ERR.SNAPSHOT_FAILED,
|
|
141
|
+
reason: 'source_read_failed',
|
|
142
|
+
detail: snapOutcome.detail,
|
|
143
|
+
recovery: 'Pre-write source read failed (non-404) and no committed active version could be confirmed — the object may exist with source we could not read, so no snapshot was possible. If this is a live connection/auth problem, fix it and retry. If this is a freshly-created shell that got edit-locked by an earlier aborted write, recover it directly: unlock it (or activate the empty shell, then delete it) and re-run the create+write. Do not bypass Rule 7 by writing over source you never read.',
|
|
144
|
+
}),
|
|
145
|
+
is_error: true,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// Fresh/never-activated shell (no active version → nothing to snapshot):
|
|
149
|
+
// proceed like a new object, but say so LOUDLY and distinctly.
|
|
150
|
+
if (snapOutcome?.reason === 'new_shell')
|
|
151
|
+
emitNewShellNote(ctx, args.type, args.name);
|
|
152
|
+
emitSnapshotNotice(ctx, snapOutcome, args.type, args.name);
|
|
82
153
|
// ── Gate 3: Safety confirmation (Rule 7) ─────────────────────────────────
|
|
83
154
|
if (isSafetyConfirmEnabled()) {
|
|
84
155
|
const r = await presentSafetyConfirmation({
|
|
@@ -87,7 +158,7 @@ registerTool({
|
|
|
87
158
|
object: { type: args.type, name: args.name },
|
|
88
159
|
what_will_happen: 'Overwrites full object source; snapshot is taken first.',
|
|
89
160
|
rollback_available: true,
|
|
90
|
-
transport:
|
|
161
|
+
transport: resolved.transport ?? ctx.currentTransport?.get() ?? '',
|
|
91
162
|
});
|
|
92
163
|
if (!r.confirmed) {
|
|
93
164
|
return {
|
|
@@ -114,7 +185,7 @@ registerTool({
|
|
|
114
185
|
// Regression evidence + diagnosis: memory/project_phase3_lite_regression.md.
|
|
115
186
|
let writeResult;
|
|
116
187
|
try {
|
|
117
|
-
writeResult = await ctx.adt.setSource(args.type, args.name, args.source,
|
|
188
|
+
writeResult = await ctx.adt.setSource(args.type, args.name, args.source, resolved.transport);
|
|
118
189
|
}
|
|
119
190
|
catch (err) {
|
|
120
191
|
const errStr = String(err);
|
|
@@ -140,12 +211,19 @@ registerTool({
|
|
|
140
211
|
// ── Gate 4: Verify — syntax check (Rule 10) ──────────────────────────────
|
|
141
212
|
const syntax = await verifySyntax(ctx.adt, args.name, args.type);
|
|
142
213
|
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ written: true, syntax }), false, Date.now() - startedAt);
|
|
214
|
+
// Tour slide-7 chain — derived from the gates that actually ran.
|
|
215
|
+
emitWriteChain(ctx, {
|
|
216
|
+
...snapshotChainState(snapOutcome),
|
|
217
|
+
written: true,
|
|
218
|
+
syntax: { ok: syntax.ok, errors: syntax.errors },
|
|
219
|
+
});
|
|
143
220
|
return {
|
|
144
221
|
content: JSON.stringify({
|
|
145
222
|
written: true,
|
|
146
223
|
syntax_ok: syntax.ok,
|
|
147
224
|
errors: syntax.errors,
|
|
148
225
|
warnings: syntax.warnings,
|
|
226
|
+
...(resolved.owning ? { transport_used: resolved.owning, transport_note: resolved.note } : {}),
|
|
149
227
|
next_step: syntax.ok
|
|
150
228
|
? 'activation_allowed'
|
|
151
229
|
: 'activation_blocked_until_syntax_errors_fixed',
|
|
@@ -166,7 +244,8 @@ registerTool({
|
|
|
166
244
|
description: 'Replace the body of a single method in an existing class. '
|
|
167
245
|
+ 'Preferred over sap_set_source for method-body-only changes (Rule 7a) — '
|
|
168
246
|
+ 'does NOT rewrite the class definition. '
|
|
169
|
-
+ 'method_source must be the lines between METHOD x. and ENDMETHOD. (no wrappers).'
|
|
247
|
+
+ 'method_source must be the lines between METHOD x. and ENDMETHOD. (no wrappers). '
|
|
248
|
+
+ 'The transport auto-resolves to the owning request if the class is already locked.',
|
|
170
249
|
isMutating: true,
|
|
171
250
|
input_schema: {
|
|
172
251
|
type: 'object',
|
|
@@ -188,7 +267,7 @@ registerTool({
|
|
|
188
267
|
is_error: true,
|
|
189
268
|
};
|
|
190
269
|
}
|
|
191
|
-
const toolUseId = crypto.randomUUID();
|
|
270
|
+
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
192
271
|
const startedAt = Date.now();
|
|
193
272
|
// ── Rule 9 conflict warning ──────────────────────────────────────────────
|
|
194
273
|
// Emit a warning if the LLM passed an explicit transport that differs from
|
|
@@ -200,9 +279,15 @@ registerTool({
|
|
|
200
279
|
ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
|
|
201
280
|
}
|
|
202
281
|
}
|
|
203
|
-
// ──
|
|
282
|
+
// ── Rule 9 owning-transport check (D15/D20) ──────────────────────────────
|
|
283
|
+
const resolved = await resolveWriteTransport(ctx, 'CLAS', args.class_name, args.transport);
|
|
284
|
+
// ── Gate 2: Snapshot (Rule 7) — HARD gate ────────────────────────────────
|
|
285
|
+
// sap_update_method always targets an EXISTING class, so a read failure
|
|
286
|
+
// (non-404) means no recovery copy → block. (A genuine 404 falls through
|
|
287
|
+
// here but the preview gate below fails on the same missing class.)
|
|
288
|
+
let snapOutcome;
|
|
204
289
|
try {
|
|
205
|
-
await autoSnapshot(args.class_name, 'CLAS', ctx.adt);
|
|
290
|
+
snapOutcome = await autoSnapshot(args.class_name, 'CLAS', ctx.adt);
|
|
206
291
|
}
|
|
207
292
|
catch (err) {
|
|
208
293
|
return {
|
|
@@ -210,6 +295,22 @@ registerTool({
|
|
|
210
295
|
is_error: true,
|
|
211
296
|
};
|
|
212
297
|
}
|
|
298
|
+
if (snapOutcome?.reason === 'source_read_failed') {
|
|
299
|
+
return {
|
|
300
|
+
content: JSON.stringify({
|
|
301
|
+
error: ERR.SNAPSHOT_FAILED,
|
|
302
|
+
reason: 'source_read_failed',
|
|
303
|
+
detail: snapOutcome.detail,
|
|
304
|
+
recovery: 'Pre-write source read failed (non-404) and no committed active version could be confirmed — the class may exist with source we could not read, so no snapshot was possible. If this is a live connection/auth problem, fix it and retry. If this is a freshly-created class shell that got edit-locked by an earlier aborted write, recover it directly: unlock it (or activate the empty shell, then delete it) and re-run the create+write. Do not bypass Rule 7 by writing over source you never read.',
|
|
305
|
+
}),
|
|
306
|
+
is_error: true,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
// Fresh/never-activated class shell (no active version → nothing to
|
|
310
|
+
// snapshot): proceed like a new object, but say so LOUDLY and distinctly.
|
|
311
|
+
if (snapOutcome?.reason === 'new_shell')
|
|
312
|
+
emitNewShellNote(ctx, 'CLAS', args.class_name);
|
|
313
|
+
emitSnapshotNotice(ctx, snapOutcome, 'CLAS', args.class_name);
|
|
213
314
|
// ── Gate 3: Diff preview & confirmation (Rule 7a) ────────────────────────
|
|
214
315
|
if (!ctx.previewHook) {
|
|
215
316
|
return {
|
|
@@ -227,7 +328,7 @@ registerTool({
|
|
|
227
328
|
className: args.class_name,
|
|
228
329
|
methodName: args.method_name,
|
|
229
330
|
methodSource: args.method_source,
|
|
230
|
-
transport:
|
|
331
|
+
transport: resolved.transport ?? ctx.currentTransport?.get() ?? '',
|
|
231
332
|
};
|
|
232
333
|
try {
|
|
233
334
|
await interceptUpdateMethod(minimal, call, ctx.previewHook);
|
|
@@ -263,7 +364,7 @@ registerTool({
|
|
|
263
364
|
// silently overwriting the inactive-only declarations.
|
|
264
365
|
let updateResult;
|
|
265
366
|
try {
|
|
266
|
-
updateResult = await ctx.adt.updateMethod(args.class_name, args.method_name, args.method_source,
|
|
367
|
+
updateResult = await ctx.adt.updateMethod(args.class_name, args.method_name, args.method_source, resolved.transport);
|
|
267
368
|
}
|
|
268
369
|
catch (err) {
|
|
269
370
|
const errStr = String(err);
|
|
@@ -302,12 +403,19 @@ registerTool({
|
|
|
302
403
|
// ── Gate 4: Verify — syntax check (Rule 10) ──────────────────────────────
|
|
303
404
|
const syntax = await verifySyntax(ctx.adt, args.class_name, 'CLAS');
|
|
304
405
|
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ updated: true, syntax }), false, Date.now() - startedAt);
|
|
406
|
+
// Tour slide-7 chain — derived from the gates that actually ran.
|
|
407
|
+
emitWriteChain(ctx, {
|
|
408
|
+
...snapshotChainState(snapOutcome),
|
|
409
|
+
written: true,
|
|
410
|
+
syntax: { ok: syntax.ok, errors: syntax.errors },
|
|
411
|
+
});
|
|
305
412
|
return {
|
|
306
413
|
content: JSON.stringify({
|
|
307
414
|
updated: true,
|
|
308
415
|
syntax_ok: syntax.ok,
|
|
309
416
|
errors: syntax.errors,
|
|
310
417
|
warnings: syntax.warnings,
|
|
418
|
+
...(resolved.owning ? { transport_used: resolved.owning, transport_note: resolved.note } : {}),
|
|
311
419
|
next_step: syntax.ok
|
|
312
420
|
? 'activation_allowed'
|
|
313
421
|
: 'activation_blocked_until_syntax_errors_fixed',
|
|
@@ -324,7 +432,8 @@ registerTool({
|
|
|
324
432
|
name: 'sap_delete_object',
|
|
325
433
|
description: 'Delete an ABAP object. Requires approval_id. '
|
|
326
434
|
+ 'A snapshot is taken automatically before deletion. '
|
|
327
|
-
+ 'Post-check: confirms the object no longer exists.'
|
|
435
|
+
+ 'Post-check: confirms the object no longer exists. '
|
|
436
|
+
+ 'The transport auto-resolves to the owning request if the object is already locked.',
|
|
328
437
|
isMutating: true,
|
|
329
438
|
input_schema: {
|
|
330
439
|
type: 'object',
|
|
@@ -345,7 +454,7 @@ registerTool({
|
|
|
345
454
|
is_error: true,
|
|
346
455
|
};
|
|
347
456
|
}
|
|
348
|
-
const toolUseId = crypto.randomUUID();
|
|
457
|
+
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
349
458
|
const startedAt = Date.now();
|
|
350
459
|
// ── Rule 9 conflict warning ──────────────────────────────────────────────
|
|
351
460
|
// Emit a warning if the LLM passed an explicit transport that differs from
|
|
@@ -357,13 +466,17 @@ registerTool({
|
|
|
357
466
|
ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
|
|
358
467
|
}
|
|
359
468
|
}
|
|
469
|
+
// ── Rule 9 owning-transport check (D15/D20) ──────────────────────────────
|
|
470
|
+
const resolved = await resolveWriteTransport(ctx, args.type, args.name, args.transport);
|
|
360
471
|
// ── Gate 2: Snapshot (Rule 7) — best-effort, object may not have source ──
|
|
472
|
+
let snapOutcome;
|
|
361
473
|
try {
|
|
362
|
-
await autoSnapshot(args.name, args.type, ctx.adt);
|
|
474
|
+
snapOutcome = await autoSnapshot(args.name, args.type, ctx.adt);
|
|
363
475
|
}
|
|
364
476
|
catch {
|
|
365
477
|
// Silently ignore — object may be a shell with no readable source.
|
|
366
478
|
}
|
|
479
|
+
emitSnapshotNotice(ctx, snapOutcome, args.type, args.name);
|
|
367
480
|
// ── Gate 3: Safety confirmation (Rule 7 — delete is irreversible-ish) ────
|
|
368
481
|
if (isSafetyConfirmEnabled()) {
|
|
369
482
|
const r = await presentSafetyConfirmation({
|
|
@@ -372,7 +485,7 @@ registerTool({
|
|
|
372
485
|
object: { type: args.type, name: args.name },
|
|
373
486
|
what_will_happen: 'Deletes object from SAP; snapshot is taken first; recovery via sap_snapshot_read + sap_set_source (LLM composes the rollback).',
|
|
374
487
|
rollback_available: true,
|
|
375
|
-
transport:
|
|
488
|
+
transport: resolved.transport ?? ctx.currentTransport?.get() ?? '',
|
|
376
489
|
});
|
|
377
490
|
if (!r.confirmed) {
|
|
378
491
|
return {
|
|
@@ -394,7 +507,7 @@ registerTool({
|
|
|
394
507
|
// 4xx. Must check status / error on the return value.
|
|
395
508
|
let deleteResult;
|
|
396
509
|
try {
|
|
397
|
-
deleteResult = await ctx.adt.deleteObject(args.type, args.name,
|
|
510
|
+
deleteResult = await ctx.adt.deleteObject(args.type, args.name, resolved.transport);
|
|
398
511
|
}
|
|
399
512
|
catch (err) {
|
|
400
513
|
const errStr = String(err);
|
|
@@ -423,7 +536,11 @@ registerTool({
|
|
|
423
536
|
.catch(() => false);
|
|
424
537
|
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ deleted: !stillExists }), false, Date.now() - startedAt);
|
|
425
538
|
return {
|
|
426
|
-
content: JSON.stringify({
|
|
539
|
+
content: JSON.stringify({
|
|
540
|
+
deleted: !stillExists,
|
|
541
|
+
still_exists: stillExists,
|
|
542
|
+
...(resolved.owning ? { transport_used: resolved.owning, transport_note: resolved.note } : {}),
|
|
543
|
+
}),
|
|
427
544
|
};
|
|
428
545
|
},
|
|
429
546
|
});
|
|
@@ -448,6 +565,11 @@ registerTool({
|
|
|
448
565
|
description: { type: 'string', description: 'Short description' },
|
|
449
566
|
transport: { type: 'string', description: 'Target transport (optional — required for workbench packages)' },
|
|
450
567
|
approval_id: { type: 'string', description: 'JWT from request_approval' },
|
|
568
|
+
// SRVB-only: a service binding must name the activated service definition it
|
|
569
|
+
// binds, plus the protocol/version. Required for type=SRVB; ignored otherwise.
|
|
570
|
+
serviceDefinition: { type: 'string', description: 'SRVB ONLY — REQUIRED when type=SRVB: name of the activated service definition (SRVD) to bind, e.g. ZUI_MYSERVICE.' },
|
|
571
|
+
bindingType: { type: 'string', description: 'SRVB ONLY (optional): binding protocol. Default ODATA.' },
|
|
572
|
+
bindingVersion: { type: 'string', description: 'SRVB ONLY (optional): binding version. Default V4 (OData V4 UI). Use V2 for OData V2.' },
|
|
451
573
|
},
|
|
452
574
|
required: ['name', 'type', 'package', 'description', 'approval_id'],
|
|
453
575
|
},
|
|
@@ -460,7 +582,7 @@ registerTool({
|
|
|
460
582
|
is_error: true,
|
|
461
583
|
};
|
|
462
584
|
}
|
|
463
|
-
const toolUseId = crypto.randomUUID();
|
|
585
|
+
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
464
586
|
const startedAt = Date.now();
|
|
465
587
|
// No snapshot — new object has no prior source.
|
|
466
588
|
// ── Rule 9 conflict warning + 3-tier transport fallback ──────────────────
|
|
@@ -486,7 +608,11 @@ registerTool({
|
|
|
486
608
|
// createObject(objtype, name, packageName, description, transport?, extras?)
|
|
487
609
|
let result;
|
|
488
610
|
try {
|
|
489
|
-
result = await ctx.adt.createObject(args.type, args.name, args.package, args.description, effectiveTransport
|
|
611
|
+
result = await ctx.adt.createObject(args.type, args.name, args.package, args.description, effectiveTransport, {
|
|
612
|
+
serviceDefinition: args.serviceDefinition,
|
|
613
|
+
bindingType: args.bindingType,
|
|
614
|
+
bindingVersion: args.bindingVersion,
|
|
615
|
+
});
|
|
490
616
|
await auditLog('createObject', args.type, args.name, 'success', Date.now() - startedAt);
|
|
491
617
|
}
|
|
492
618
|
catch (err) {
|
|
@@ -516,6 +642,386 @@ registerTool({
|
|
|
516
642
|
};
|
|
517
643
|
},
|
|
518
644
|
});
|
|
645
|
+
// ── sap_set_class_include ─────────────────────────────────────────────────────
|
|
646
|
+
//
|
|
647
|
+
// Signature: setClassInclude(className, includeType, source, transport?)
|
|
648
|
+
// → Promise<ClassIncludeResult>
|
|
649
|
+
// Writes a class local-include source (CCDEF/CCIMP/CCMAC/test classes). This is
|
|
650
|
+
// how RAP behaviour-pool handlers (which live in CCIMP / locals_imp) get landed.
|
|
651
|
+
// AdtClient locks the CLASS object, tries a ladder of PUT URL/content-type
|
|
652
|
+
// variants (first 2xx wins), and always unlocks. Snapshot is best-effort:
|
|
653
|
+
// the include source may not be independently readable, so a failed snapshot
|
|
654
|
+
// must not block the write (consistent with sap_delete_object).
|
|
655
|
+
registerTool({
|
|
656
|
+
name: 'sap_set_class_include',
|
|
657
|
+
description: 'Write a class local-include source (CCDEF locals_def, CCIMP locals_imp, '
|
|
658
|
+
+ 'CCMAC macros, or test classes). Required for RAP behaviour-pool handlers, '
|
|
659
|
+
+ 'which live in the CCIMP (locals_imp) include. Requires approval_id. '
|
|
660
|
+
+ 'After writing, activate the class via sap_activate. '
|
|
661
|
+
+ 'The transport auto-resolves to the owning request if the class is already locked.',
|
|
662
|
+
isMutating: true,
|
|
663
|
+
input_schema: {
|
|
664
|
+
type: 'object',
|
|
665
|
+
properties: {
|
|
666
|
+
className: { type: 'string', description: 'ABAP class name (e.g. ZBP_FOO)' },
|
|
667
|
+
includeType: {
|
|
668
|
+
type: 'string',
|
|
669
|
+
enum: ['locals_def', 'locals_imp', 'macros', 'testclasses'],
|
|
670
|
+
description: 'Which local include to write (locals_imp = CCIMP, where RAP handlers live)',
|
|
671
|
+
},
|
|
672
|
+
source: { type: 'string', description: 'Include source body' },
|
|
673
|
+
transport: { type: 'string', description: 'Transport request number (optional)' },
|
|
674
|
+
approval_id: { type: 'string', description: 'JWT from request_approval' },
|
|
675
|
+
},
|
|
676
|
+
required: ['className', 'includeType', 'source', 'approval_id'],
|
|
677
|
+
},
|
|
678
|
+
handler: async (args, ctx) => {
|
|
679
|
+
// ── Gate 1: Approval ────────────────────────────────────────────────────
|
|
680
|
+
const verify = await verifyAndSpendApprovalId(args.approval_id, args.className, 'modify');
|
|
681
|
+
if (!verify.ok) {
|
|
682
|
+
return {
|
|
683
|
+
content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: verify.reason }),
|
|
684
|
+
is_error: true,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
688
|
+
const startedAt = Date.now();
|
|
689
|
+
// ── Rule 9 conflict warning + 3-tier transport fallback ──────────────────
|
|
690
|
+
{
|
|
691
|
+
const sessionTransport = ctx.currentTransport?.get() ?? null;
|
|
692
|
+
if (args.transport && sessionTransport && args.transport !== sessionTransport) {
|
|
693
|
+
ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const effectiveTransport = args.transport ?? ctx.currentTransport?.get() ?? undefined;
|
|
697
|
+
// ── Rule 9 owning-transport check (D15/D20) ──────────────────────────────
|
|
698
|
+
// The CCIMP piece (LIMU CINC) is exactly the kind of object that sits
|
|
699
|
+
// locked in a request from earlier work — battery S1's 409 root cause.
|
|
700
|
+
const resolved = await resolveWriteTransport(ctx, 'CLAS', args.className, effectiveTransport);
|
|
701
|
+
// ── Gate 2: Snapshot (Rule 7) — best-effort, include may not be readable ─
|
|
702
|
+
let snapOutcome;
|
|
703
|
+
try {
|
|
704
|
+
snapOutcome = await autoSnapshot(args.className, 'CLAS', ctx.adt);
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
// Silently ignore — include source may not be independently readable.
|
|
708
|
+
}
|
|
709
|
+
emitSnapshotNotice(ctx, snapOutcome, 'CLAS', args.className);
|
|
710
|
+
// ── WAL: record pending entry ────────────────────────────────────────────
|
|
711
|
+
await appendPending(ctx.session, {
|
|
712
|
+
tool_use_id: toolUseId,
|
|
713
|
+
tool: 'sap_set_class_include',
|
|
714
|
+
args: { className: args.className, includeType: args.includeType },
|
|
715
|
+
started_at: new Date().toISOString(),
|
|
716
|
+
});
|
|
717
|
+
// ── Write ────────────────────────────────────────────────────────────────
|
|
718
|
+
// setClassInclude THROWS only when it cannot acquire the class lock;
|
|
719
|
+
// for the all-variants-failed case it RETURNS { written:false, ... }.
|
|
720
|
+
// Both paths must surface as an error envelope.
|
|
721
|
+
let result;
|
|
722
|
+
try {
|
|
723
|
+
result = await ctx.adt.setClassInclude(args.className, args.includeType, args.source, resolved.transport);
|
|
724
|
+
}
|
|
725
|
+
catch (err) {
|
|
726
|
+
const errStr = String(err);
|
|
727
|
+
await finalizeToolCall(ctx.session, toolUseId, errStr, true, Date.now() - startedAt);
|
|
728
|
+
return {
|
|
729
|
+
content: JSON.stringify({ error: 'class_include_write_failed', detail: errStr }),
|
|
730
|
+
is_error: true,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
if (!result.written) {
|
|
734
|
+
// D14: `result.error` now carries SAP's T100 diagnosis verbatim when a
|
|
735
|
+
// variant hit a CTS/lock conflict — pass it through unaltered, plus the
|
|
736
|
+
// structured owning transport so the model can retry correctly instead
|
|
737
|
+
// of declaring the change manual-only.
|
|
738
|
+
const detail = result.error ?? `All PUT variants failed (last HTTP ${result.httpStatus})`;
|
|
739
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
|
|
740
|
+
return {
|
|
741
|
+
content: JSON.stringify({
|
|
742
|
+
error: 'class_include_write_failed',
|
|
743
|
+
detail,
|
|
744
|
+
http_status: result.httpStatus,
|
|
745
|
+
...(result.lockedInTransport ? {
|
|
746
|
+
locked_in_transport: result.lockedInTransport,
|
|
747
|
+
recovery: `Retry this exact write with transport=${result.lockedInTransport} — that request already owns the object.`,
|
|
748
|
+
} : {}),
|
|
749
|
+
attempts: result.attempts,
|
|
750
|
+
}),
|
|
751
|
+
is_error: true,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
await auditLog('setClassInclude', 'CLAS', args.className, 'success', Date.now() - startedAt, `include=${args.includeType}`);
|
|
755
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ written: true, variant: result.variantUsed }), false, Date.now() - startedAt);
|
|
756
|
+
// Tour slide-7 chain. Honest states: includes have no standalone syntax
|
|
757
|
+
// gate (checked at activation), so ③ renders as deferred, never faked.
|
|
758
|
+
emitWriteChain(ctx, {
|
|
759
|
+
...snapshotChainState(snapOutcome),
|
|
760
|
+
written: true,
|
|
761
|
+
syntax: 'not_checked',
|
|
762
|
+
});
|
|
763
|
+
return {
|
|
764
|
+
content: JSON.stringify({
|
|
765
|
+
written: true,
|
|
766
|
+
variant: result.variantUsed,
|
|
767
|
+
...(resolved.owning ? { transport_used: resolved.owning, transport_note: resolved.note } : {}),
|
|
768
|
+
next_step: 'activate_the_class',
|
|
769
|
+
}),
|
|
770
|
+
};
|
|
771
|
+
},
|
|
772
|
+
});
|
|
773
|
+
// ── sap_create_domain ────────────────────────────────────────────────────────
|
|
774
|
+
//
|
|
775
|
+
// Signature: createDomain(spec, transport?) → Promise<DdicCreateResult>
|
|
776
|
+
// AdtClient creates → activates → verifies in one call (Rule 10 internal).
|
|
777
|
+
// No snapshot needed: new object has no prior source.
|
|
778
|
+
registerTool({
|
|
779
|
+
name: 'sap_create_domain',
|
|
780
|
+
description: 'Create a DDIC domain (DOMA), then activate and verify it. '
|
|
781
|
+
+ 'Requires approval_id. No snapshot needed (new object). '
|
|
782
|
+
+ 'Use the domain in a data element via sap_create_data_element.',
|
|
783
|
+
isMutating: true,
|
|
784
|
+
input_schema: {
|
|
785
|
+
type: 'object',
|
|
786
|
+
properties: {
|
|
787
|
+
name: { type: 'string', description: 'Domain name (Z*/Y*)' },
|
|
788
|
+
description: { type: 'string', description: 'Short description' },
|
|
789
|
+
package: { type: 'string', description: 'Development package (Z*/Y* or $TMP)' },
|
|
790
|
+
dataType: { type: 'string', description: 'ABAP data type (CHAR/NUMC/DEC/CURR/QUAN/INT1/INT2/INT4/DATS/TIMS/…)' },
|
|
791
|
+
length: { type: 'number', description: 'Domain length (number of characters/digits)' },
|
|
792
|
+
decimals: { type: 'number', description: 'Number of decimal places (DEC/CURR/QUAN)' },
|
|
793
|
+
outputLength: { type: 'number', description: 'Output length (optional)' },
|
|
794
|
+
lowercase: { type: 'boolean', description: 'Allow lowercase (optional)' },
|
|
795
|
+
signExists: { type: 'boolean', description: 'Value can be negative (optional)' },
|
|
796
|
+
valueTable: { type: 'string', description: 'Check / value table (optional)' },
|
|
797
|
+
fixedValues: {
|
|
798
|
+
type: 'array',
|
|
799
|
+
description: 'Fixed value list (optional)',
|
|
800
|
+
items: {
|
|
801
|
+
type: 'object',
|
|
802
|
+
properties: {
|
|
803
|
+
value: { type: 'string', description: 'Fixed value' },
|
|
804
|
+
text: { type: 'string', description: 'Description text' },
|
|
805
|
+
},
|
|
806
|
+
required: ['value', 'text'],
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
transport: { type: 'string', description: 'Target transport (optional)' },
|
|
810
|
+
approval_id: { type: 'string', description: 'JWT from request_approval' },
|
|
811
|
+
},
|
|
812
|
+
required: ['name', 'description', 'package', 'dataType', 'length', 'approval_id'],
|
|
813
|
+
},
|
|
814
|
+
handler: async (args, ctx) => {
|
|
815
|
+
// ── Gate 1: Approval ────────────────────────────────────────────────────
|
|
816
|
+
const verify = await verifyAndSpendApprovalId(args.approval_id, args.name, 'create');
|
|
817
|
+
if (!verify.ok) {
|
|
818
|
+
return {
|
|
819
|
+
content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: verify.reason }),
|
|
820
|
+
is_error: true,
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
824
|
+
const startedAt = Date.now();
|
|
825
|
+
// No snapshot — new object has no prior source.
|
|
826
|
+
// ── Rule 9 conflict warning + 3-tier transport fallback ──────────────────
|
|
827
|
+
{
|
|
828
|
+
const sessionTransport = ctx.currentTransport?.get() ?? null;
|
|
829
|
+
if (args.transport && sessionTransport && args.transport !== sessionTransport) {
|
|
830
|
+
ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
const effectiveTransport = args.transport ?? ctx.currentTransport?.get() ?? undefined;
|
|
834
|
+
// ── WAL: record pending entry ────────────────────────────────────────────
|
|
835
|
+
await appendPending(ctx.session, {
|
|
836
|
+
tool_use_id: toolUseId,
|
|
837
|
+
tool: 'sap_create_domain',
|
|
838
|
+
args: { name: args.name, type: 'DOMA', package: args.package },
|
|
839
|
+
started_at: new Date().toISOString(),
|
|
840
|
+
});
|
|
841
|
+
// ── Build spec (omit undefined optionals) ────────────────────────────────
|
|
842
|
+
const spec = {
|
|
843
|
+
name: args.name,
|
|
844
|
+
description: args.description,
|
|
845
|
+
package: args.package,
|
|
846
|
+
dataType: args.dataType,
|
|
847
|
+
length: args.length,
|
|
848
|
+
};
|
|
849
|
+
if (args.decimals !== undefined)
|
|
850
|
+
spec.decimals = args.decimals;
|
|
851
|
+
if (args.outputLength !== undefined)
|
|
852
|
+
spec.outputLength = args.outputLength;
|
|
853
|
+
if (args.lowercase !== undefined)
|
|
854
|
+
spec.lowercase = args.lowercase;
|
|
855
|
+
if (args.signExists !== undefined)
|
|
856
|
+
spec.signExists = args.signExists;
|
|
857
|
+
if (args.valueTable !== undefined)
|
|
858
|
+
spec.valueTable = args.valueTable;
|
|
859
|
+
if (args.fixedValues !== undefined)
|
|
860
|
+
spec.fixedValues = args.fixedValues;
|
|
861
|
+
// ── Create → activate → verify (AdtClient does all three) ────────────────
|
|
862
|
+
let result;
|
|
863
|
+
try {
|
|
864
|
+
result = await ctx.adt.createDomain(spec, effectiveTransport);
|
|
865
|
+
await auditLog('createObject', 'DOMA/DD', args.name, 'success', Date.now() - startedAt);
|
|
866
|
+
}
|
|
867
|
+
catch (err) {
|
|
868
|
+
const errStr = String(err);
|
|
869
|
+
await finalizeToolCall(ctx.session, toolUseId, errStr, true, Date.now() - startedAt);
|
|
870
|
+
return {
|
|
871
|
+
content: JSON.stringify({ error: 'create_failed', detail: errStr }),
|
|
872
|
+
is_error: true,
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
if (!result.created) {
|
|
876
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
|
|
877
|
+
return {
|
|
878
|
+
content: JSON.stringify({ error: 'create_failed', detail: result.error }),
|
|
879
|
+
is_error: true,
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
if (!result.active) {
|
|
883
|
+
// Rule 10: created but did not activate — surface as an error.
|
|
884
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
|
|
885
|
+
return {
|
|
886
|
+
content: JSON.stringify({ error: 'activation_failed', object: result.name, detail: result.error }),
|
|
887
|
+
is_error: true,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), false, Date.now() - startedAt);
|
|
891
|
+
return {
|
|
892
|
+
content: JSON.stringify({
|
|
893
|
+
created: true,
|
|
894
|
+
active: true,
|
|
895
|
+
object: result.name,
|
|
896
|
+
type: result.type,
|
|
897
|
+
next_step: 'use_in_table_field',
|
|
898
|
+
}),
|
|
899
|
+
};
|
|
900
|
+
},
|
|
901
|
+
});
|
|
902
|
+
// ── sap_create_data_element ──────────────────────────────────────────────────
|
|
903
|
+
//
|
|
904
|
+
// Signature: createDataElement(spec, transport?) → Promise<DdicCreateResult>
|
|
905
|
+
// AdtClient creates → activates → verifies in one call (Rule 10 internal).
|
|
906
|
+
// No snapshot needed: new object has no prior source.
|
|
907
|
+
registerTool({
|
|
908
|
+
name: 'sap_create_data_element',
|
|
909
|
+
description: 'Create a DDIC data element (DTEL) bound to an existing domain, then '
|
|
910
|
+
+ 'activate and verify it. Requires approval_id. No snapshot needed (new object). '
|
|
911
|
+
+ 'Use the data element as a table/structure field type.',
|
|
912
|
+
isMutating: true,
|
|
913
|
+
input_schema: {
|
|
914
|
+
type: 'object',
|
|
915
|
+
properties: {
|
|
916
|
+
name: { type: 'string', description: 'Data element name (Z*/Y*)' },
|
|
917
|
+
description: { type: 'string', description: 'Short description' },
|
|
918
|
+
package: { type: 'string', description: 'Development package (Z*/Y* or $TMP)' },
|
|
919
|
+
domainName: { type: 'string', description: 'Referenced domain name' },
|
|
920
|
+
dataType: { type: 'string', description: "Referenced domain's data type (CHAR/NUMC/CURR/…)" },
|
|
921
|
+
length: { type: 'number', description: 'Domain length' },
|
|
922
|
+
decimals: { type: 'number', description: 'Decimal places (optional)' },
|
|
923
|
+
labels: {
|
|
924
|
+
type: 'object',
|
|
925
|
+
description: 'Field labels',
|
|
926
|
+
properties: {
|
|
927
|
+
short: { type: 'string', description: 'Short label' },
|
|
928
|
+
medium: { type: 'string', description: 'Medium label' },
|
|
929
|
+
long: { type: 'string', description: 'Long label' },
|
|
930
|
+
heading: { type: 'string', description: 'Heading label' },
|
|
931
|
+
},
|
|
932
|
+
required: ['short', 'medium', 'long', 'heading'],
|
|
933
|
+
},
|
|
934
|
+
searchHelp: { type: 'string', description: 'Attached search help name (optional)' },
|
|
935
|
+
searchHelpParameter: { type: 'string', description: 'Search help parameter (optional)' },
|
|
936
|
+
transport: { type: 'string', description: 'Target transport (optional)' },
|
|
937
|
+
approval_id: { type: 'string', description: 'JWT from request_approval' },
|
|
938
|
+
},
|
|
939
|
+
required: ['name', 'description', 'package', 'domainName', 'dataType', 'length', 'labels', 'approval_id'],
|
|
940
|
+
},
|
|
941
|
+
handler: async (args, ctx) => {
|
|
942
|
+
// ── Gate 1: Approval ────────────────────────────────────────────────────
|
|
943
|
+
const verify = await verifyAndSpendApprovalId(args.approval_id, args.name, 'create');
|
|
944
|
+
if (!verify.ok) {
|
|
945
|
+
return {
|
|
946
|
+
content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: verify.reason }),
|
|
947
|
+
is_error: true,
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
951
|
+
const startedAt = Date.now();
|
|
952
|
+
// No snapshot — new object has no prior source.
|
|
953
|
+
// ── Rule 9 conflict warning + 3-tier transport fallback ──────────────────
|
|
954
|
+
{
|
|
955
|
+
const sessionTransport = ctx.currentTransport?.get() ?? null;
|
|
956
|
+
if (args.transport && sessionTransport && args.transport !== sessionTransport) {
|
|
957
|
+
ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
const effectiveTransport = args.transport ?? ctx.currentTransport?.get() ?? undefined;
|
|
961
|
+
// ── WAL: record pending entry ────────────────────────────────────────────
|
|
962
|
+
await appendPending(ctx.session, {
|
|
963
|
+
tool_use_id: toolUseId,
|
|
964
|
+
tool: 'sap_create_data_element',
|
|
965
|
+
args: { name: args.name, type: 'DTEL', package: args.package },
|
|
966
|
+
started_at: new Date().toISOString(),
|
|
967
|
+
});
|
|
968
|
+
// ── Build spec (omit undefined optionals) ────────────────────────────────
|
|
969
|
+
const spec = {
|
|
970
|
+
name: args.name,
|
|
971
|
+
description: args.description,
|
|
972
|
+
package: args.package,
|
|
973
|
+
domainName: args.domainName,
|
|
974
|
+
dataType: args.dataType,
|
|
975
|
+
length: args.length,
|
|
976
|
+
labels: args.labels,
|
|
977
|
+
};
|
|
978
|
+
if (args.decimals !== undefined)
|
|
979
|
+
spec.decimals = args.decimals;
|
|
980
|
+
if (args.searchHelp !== undefined)
|
|
981
|
+
spec.searchHelp = args.searchHelp;
|
|
982
|
+
if (args.searchHelpParameter !== undefined)
|
|
983
|
+
spec.searchHelpParameter = args.searchHelpParameter;
|
|
984
|
+
// ── Create → activate → verify (AdtClient does all three) ────────────────
|
|
985
|
+
let result;
|
|
986
|
+
try {
|
|
987
|
+
result = await ctx.adt.createDataElement(spec, effectiveTransport);
|
|
988
|
+
await auditLog('createObject', 'DTEL/DE', args.name, 'success', Date.now() - startedAt);
|
|
989
|
+
}
|
|
990
|
+
catch (err) {
|
|
991
|
+
const errStr = String(err);
|
|
992
|
+
await finalizeToolCall(ctx.session, toolUseId, errStr, true, Date.now() - startedAt);
|
|
993
|
+
return {
|
|
994
|
+
content: JSON.stringify({ error: 'create_failed', detail: errStr }),
|
|
995
|
+
is_error: true,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
if (!result.created) {
|
|
999
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
|
|
1000
|
+
return {
|
|
1001
|
+
content: JSON.stringify({ error: 'create_failed', detail: result.error }),
|
|
1002
|
+
is_error: true,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
if (!result.active) {
|
|
1006
|
+
// Rule 10: created but did not activate — surface as an error.
|
|
1007
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
|
|
1008
|
+
return {
|
|
1009
|
+
content: JSON.stringify({ error: 'activation_failed', object: result.name, detail: result.error }),
|
|
1010
|
+
is_error: true,
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), false, Date.now() - startedAt);
|
|
1014
|
+
return {
|
|
1015
|
+
content: JSON.stringify({
|
|
1016
|
+
created: true,
|
|
1017
|
+
active: true,
|
|
1018
|
+
object: result.name,
|
|
1019
|
+
type: result.type,
|
|
1020
|
+
next_step: 'use_in_table_field',
|
|
1021
|
+
}),
|
|
1022
|
+
};
|
|
1023
|
+
},
|
|
1024
|
+
});
|
|
519
1025
|
// ── sap_activate ─────────────────────────────────────────────────────────────
|
|
520
1026
|
//
|
|
521
1027
|
// Signature: activate(type, name) → Promise<ActivationResult>
|
|
@@ -565,7 +1071,7 @@ registerTool({
|
|
|
565
1071
|
is_error: true,
|
|
566
1072
|
};
|
|
567
1073
|
}
|
|
568
|
-
const toolUseId = crypto.randomUUID();
|
|
1074
|
+
const toolUseId = ctx.toolUseId ?? crypto.randomUUID();
|
|
569
1075
|
const startedAt = Date.now();
|
|
570
1076
|
// ── WAL: record pending entry ────────────────────────────────────────────
|
|
571
1077
|
await appendPending(ctx.session, {
|
|
@@ -700,6 +1206,29 @@ registerTool({
|
|
|
700
1206
|
return { content: JSON.stringify(result) };
|
|
701
1207
|
},
|
|
702
1208
|
});
|
|
1209
|
+
// ── sap_service_binding_unpublish ─────────────────────────────────────────────
|
|
1210
|
+
//
|
|
1211
|
+
// Unpublish a service binding (inverse of publish). REQUIRED before deleting a
|
|
1212
|
+
// published binding — SAP returns HTTP 412 "… is published" otherwise (Bug #6).
|
|
1213
|
+
// Signature: serviceBindingUnpublish(name) → Promise<{ status: number; error?: string }>
|
|
1214
|
+
// NOTE: the unpublishjobs endpoint is inferred-not-yet-live-verified (VPN down).
|
|
1215
|
+
registerTool({
|
|
1216
|
+
name: 'sap_service_binding_unpublish',
|
|
1217
|
+
description: 'Unpublish an ADT service binding so its OData endpoint is removed. '
|
|
1218
|
+
+ 'Required before deleting a published binding (SAP returns HTTP 412 "is published" otherwise).',
|
|
1219
|
+
isMutating: true,
|
|
1220
|
+
input_schema: {
|
|
1221
|
+
type: 'object',
|
|
1222
|
+
properties: {
|
|
1223
|
+
name: { type: 'string', description: 'Service binding name (e.g. ZSALES_UI_V4)' },
|
|
1224
|
+
},
|
|
1225
|
+
required: ['name'],
|
|
1226
|
+
},
|
|
1227
|
+
handler: async (args, ctx) => {
|
|
1228
|
+
const result = await ctx.adt.serviceBindingUnpublish(args.name);
|
|
1229
|
+
return { content: JSON.stringify(result) };
|
|
1230
|
+
},
|
|
1231
|
+
});
|
|
703
1232
|
// ── sap_message_maintain (STUB) ───────────────────────────────────────────────
|
|
704
1233
|
//
|
|
705
1234
|
// STUB — MSAG message-class maintenance requires XML message-class-format
|