@cspeach/cli 0.6.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/LICENSE +8 -0
- package/README.md +108 -0
- package/dist/agent/anthropic-provider.js +59 -0
- package/dist/agent/llm-provider.js +1 -0
- package/dist/agent/loop.js +709 -0
- package/dist/agent/maybe-build-project-context.js +126 -0
- package/dist/agent/providers/ai-hub-provider.js +58 -0
- package/dist/agent/providers/byok-provider.js +53 -0
- package/dist/agent/providers/factory.js +13 -0
- package/dist/agent/providers/local-provider.js +125 -0
- package/dist/agent/repair-partial.js +31 -0
- package/dist/agent/retry-key.js +58 -0
- package/dist/agent/retry.js +17 -0
- package/dist/agent/sap-connection-adapter.js +82 -0
- package/dist/agent/skill-checkpoint.js +119 -0
- package/dist/agent/tool-dispatch.js +47 -0
- package/dist/agent/turn-assistant-text.js +49 -0
- package/dist/agent/turn-error-ux.js +126 -0
- package/dist/agent/turn-stream.js +79 -0
- package/dist/agent/turn-watchdog.js +71 -0
- package/dist/approvals/advisory-prompt.js +40 -0
- package/dist/approvals/advisory-render.js +38 -0
- package/dist/approvals/approval-prompt.js +100 -0
- package/dist/approvals/jwt.js +33 -0
- package/dist/approvals/render.js +211 -0
- package/dist/approvals/risk-floor.js +26 -0
- package/dist/auth/api-key.js +40 -0
- package/dist/auth/auth-file.js +59 -0
- package/dist/auth/device.js +8 -0
- package/dist/auth/me.js +19 -0
- package/dist/classifier/client.js +58 -0
- package/dist/cli-args.js +38 -0
- package/dist/cli.js +148 -0
- package/dist/commands/config-set.js +245 -0
- package/dist/commands/config-show.js +159 -0
- package/dist/commands/help.js +93 -0
- package/dist/commands/login.js +122 -0
- package/dist/commands/logout.js +17 -0
- package/dist/commands/project-context-impact.js +215 -0
- package/dist/commands/reroute.js +60 -0
- package/dist/commands/spec-gap-status.js +52 -0
- package/dist/commands/whoami.js +35 -0
- package/dist/config/loader.js +67 -0
- package/dist/config/paths.js +20 -0
- package/dist/doctor/checks/_http-probe.js +56 -0
- package/dist/doctor/checks/auth.js +15 -0
- package/dist/doctor/checks/cert.js +24 -0
- package/dist/doctor/checks/forge-rules.js +102 -0
- package/dist/doctor/checks/keychain-fallback.js +14 -0
- package/dist/doctor/checks/keychain.js +23 -0
- package/dist/doctor/checks/llm-mode.js +27 -0
- package/dist/doctor/checks/proxy.js +13 -0
- package/dist/doctor/checks/sap.js +33 -0
- package/dist/doctor/checks/skill.js +24 -0
- package/dist/doctor/checks/write-mode.js +34 -0
- package/dist/doctor/checks/zcspeach.js +76 -0
- package/dist/doctor/run.js +46 -0
- package/dist/errors/codes.js +12 -0
- package/dist/index.js +6 -0
- package/dist/lock-contention.js +22 -0
- package/dist/one-shot.js +104 -0
- package/dist/project-context/conventions.js +309 -0
- package/dist/project-context/detect.js +250 -0
- package/dist/project-context/domain/abap-cloud.js +26 -0
- package/dist/project-context/domain/abapgit.js +177 -0
- package/dist/project-context/domain/cap.js +164 -0
- package/dist/project-context/domain/fiori.js +326 -0
- package/dist/project-context/git.js +115 -0
- package/dist/project-context/index-files.js +235 -0
- package/dist/project-context/index.js +117 -0
- package/dist/project-context/render.js +308 -0
- package/dist/project-context/types.js +14 -0
- package/dist/projects/build.js +20 -0
- package/dist/projects/canonicalize.js +39 -0
- package/dist/projects/email-template.js +54 -0
- package/dist/projects/extract-cca.js +139 -0
- package/dist/projects/extract-design.js +107 -0
- package/dist/projects/extract-estimate.js +93 -0
- package/dist/projects/extract-modernize.js +130 -0
- package/dist/projects/extract-spec-gap.js +101 -0
- package/dist/projects/extract-test-coverage.js +137 -0
- package/dist/projects/extract-upgrade.js +230 -0
- package/dist/projects/filename.js +18 -0
- package/dist/projects/index.js +8 -0
- package/dist/projects/migration.js +111 -0
- package/dist/projects/promote-command.js +96 -0
- package/dist/projects/promote.js +107 -0
- package/dist/projects/save-command.js +124 -0
- package/dist/projects/save.js +21 -0
- package/dist/projects/status.js +170 -0
- package/dist/projects/types.js +1 -0
- package/dist/projects/validate.js +146 -0
- package/dist/projects/workspace.js +478 -0
- package/dist/renderer/abap-inline.js +121 -0
- package/dist/renderer/banners.js +39 -0
- package/dist/renderer/highlighters/abap.js +126 -0
- package/dist/renderer/highlighters/bdef.js +81 -0
- package/dist/renderer/highlighters/cds.js +91 -0
- package/dist/renderer/markdown.js +291 -0
- package/dist/renderer/pipeline.js +201 -0
- package/dist/renderer/progress-chatter.js +237 -0
- package/dist/renderer/question-normalizer.js +306 -0
- package/dist/renderer/severity.js +61 -0
- package/dist/renderer/status-footer.js +50 -0
- package/dist/renderer/syntax.js +58 -0
- package/dist/renderer/tables.js +55 -0
- package/dist/renderer/thinking-heartbeat.js +70 -0
- package/dist/renderer/tool-widget.js +199 -0
- package/dist/renderer/tty.js +66 -0
- package/dist/renderer/widget-extractor.js +87 -0
- package/dist/renderer/widget-fallback.js +78 -0
- package/dist/renderer/widget-schemas.js +43 -0
- package/dist/repl/at-completer.js +64 -0
- package/dist/repl/at-picker.js +122 -0
- package/dist/repl/bracketed-paste.js +284 -0
- package/dist/repl/current-transport.js +46 -0
- package/dist/repl/diff-display.js +41 -0
- package/dist/repl/file-picker.js +219 -0
- package/dist/repl/inquirer-guard.js +130 -0
- package/dist/repl/inquirer-theme.js +41 -0
- package/dist/repl/rule8-detector.js +99 -0
- package/dist/repl/safety-confirm.js +106 -0
- package/dist/repl/safety-mode-state.js +36 -0
- package/dist/repl/slash-completer.js +59 -0
- package/dist/repl/slash-picker.js +124 -0
- package/dist/repl/update-method-preview-hook.js +45 -0
- package/dist/repl.js +1383 -0
- package/dist/router/classifier.js +38 -0
- package/dist/router/intent-extractor.js +140 -0
- package/dist/router/routing-decision.js +19 -0
- package/dist/sap/connection-manager.js +52 -0
- package/dist/sap/onboarding.js +178 -0
- package/dist/sap/system-info.js +515 -0
- package/dist/session/awaiting-answer.js +73 -0
- package/dist/session/gc.js +28 -0
- package/dist/session/pending.js +37 -0
- package/dist/session/resume.js +77 -0
- package/dist/session/schema.js +20 -0
- package/dist/session/store.js +147 -0
- package/dist/session/time-ago.js +41 -0
- package/dist/skill-catalog.js +222 -0
- package/dist/skills/bundled-skills.js +1 -0
- package/dist/skills/canonical.js +12 -0
- package/dist/skills/manifest-client.js +93 -0
- package/dist/skills/promotion-dispatch.js +24 -0
- package/dist/skills/signing-public-key.js +4 -0
- package/dist/skills/source-bundled.js +20 -0
- package/dist/skills/source-managed.js +26 -0
- package/dist/skills/source-manifest.js +26 -0
- package/dist/tools/_command-shared.js +110 -0
- package/dist/tools/_filesystem-shared.js +81 -0
- package/dist/tools/_flag.js +39 -0
- package/dist/tools/approval.js +228 -0
- package/dist/tools/ask-question.js +205 -0
- package/dist/tools/dispatch-skill.js +81 -0
- package/dist/tools/filesystem/file-edit.js +140 -0
- package/dist/tools/filesystem/file-read.js +89 -0
- package/dist/tools/filesystem/file-write.js +128 -0
- package/dist/tools/filesystem/glob.js +177 -0
- package/dist/tools/filesystem/grep.js +163 -0
- package/dist/tools/index.js +32 -0
- package/dist/tools/project/convention_get.js +91 -0
- package/dist/tools/project/playbook_get.js +132 -0
- package/dist/tools/project/project_context_get.js +101 -0
- package/dist/tools/sap-read.js +454 -0
- package/dist/tools/sap-write.js +746 -0
- package/dist/tools/shell/shell_exec.js +209 -0
- package/dist/tools/snapshot.js +107 -0
- package/dist/tools/subagent/_background-shared.js +133 -0
- package/dist/tools/subagent/agent_run.js +186 -0
- package/dist/tools/subagent/background_run.js +143 -0
- package/dist/tools/subagent/monitor_emit.js +65 -0
- package/dist/tools/subagent/schedule_create.js +131 -0
- package/dist/tools/transport.js +233 -0
- package/dist/tools/update-method-intercept.js +119 -0
- package/dist/tools/verify.js +39 -0
- package/dist/tools/web/_web-shared.js +251 -0
- package/dist/tools/web/web_fetch.js +257 -0
- package/dist/tools/web/web_search.js +195 -0
- package/dist/tools/write-mode.js +22 -0
- package/dist/ui/app.js +95 -0
- package/dist/ui/approval-emitter.js +10 -0
- package/dist/ui/approval-modal.js +53 -0
- package/dist/ui/ascii-chars.js +6 -0
- package/dist/ui/body.js +102 -0
- package/dist/ui/coaching-picker-classic.js +36 -0
- package/dist/ui/coaching-picker-emitter.js +27 -0
- package/dist/ui/command-palette.js +34 -0
- package/dist/ui/error-emitter.js +21 -0
- package/dist/ui/footer.js +103 -0
- package/dist/ui/header.js +17 -0
- package/dist/ui/ink-classifier-route.js +19 -0
- package/dist/ui/login-banner.js +72 -0
- package/dist/ui/rich-error-box.js +9 -0
- package/dist/ui/sap-state-store.js +65 -0
- package/dist/ui/session-timeline.js +31 -0
- package/dist/ui/sidebar.js +10 -0
- package/dist/ui/skill-picker.js +50 -0
- package/dist/ui/status-row.js +12 -0
- package/dist/ui/widget-control.js +4 -0
- package/dist/ui/widgets/bar-chart.js +15 -0
- package/dist/ui/widgets/coaching-picker.js +41 -0
- package/dist/ui/widgets/component-registry.js +12 -0
- package/dist/ui/widgets/dep-graph.js +9 -0
- package/dist/ui/widgets/diff-viewer.js +11 -0
- package/dist/ui/widgets/question-card.js +11 -0
- package/dist/ui/widgets/stack-frames.js +5 -0
- package/dist/upgrade-check.js +28 -0
- package/dist/upgrade.js +13 -0
- package/package.json +83 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write tools with the complete gate pipeline:
|
|
3
|
+
*
|
|
4
|
+
* approval_id verify → snapshot gate → WAL pending → adt.<method> →
|
|
5
|
+
* audit log → verify gate (syntax check / activation check) → WAL finalize
|
|
6
|
+
*
|
|
7
|
+
* Confirmed AdtClient signatures (sap-client/src/adt-client.ts):
|
|
8
|
+
* setSource(type, name, source, transport?) → Promise<WriteResult> line 1983
|
|
9
|
+
* updateMethod(className, methodName, methodBody, transport?)
|
|
10
|
+
* → Promise<WriteResult> line 2159
|
|
11
|
+
* createObject(objtype, name, packageName, description, transport?, extras?)
|
|
12
|
+
* → Promise<CreateResult> line 2245
|
|
13
|
+
* deleteObject(type, name, transport?) → Promise<{status,error?}> line 2335
|
|
14
|
+
* activate(type, name) → Promise<ActivationResult> line 2382
|
|
15
|
+
* lock(type, name) → Promise<LockResult> line 1914
|
|
16
|
+
* unlock(type, name, lockHandle) → Promise<void> line 1949
|
|
17
|
+
* serviceBindingPublish(name) → Promise<{status,error?}> line 2658
|
|
18
|
+
* messageMaintain (STUB — not yet in sap-client)
|
|
19
|
+
* numberRangeIntervals (STUB — not yet in sap-client)
|
|
20
|
+
*
|
|
21
|
+
* auditLog aliased from safety/audit-log.ts#log via sap-client/src/index.ts.
|
|
22
|
+
*/
|
|
23
|
+
import crypto from 'node:crypto';
|
|
24
|
+
import { registerTool } from './index.js';
|
|
25
|
+
import { verifyAndSpendApprovalId } from '../approvals/jwt.js';
|
|
26
|
+
import { autoSnapshot } from './snapshot.js';
|
|
27
|
+
import { verifySyntax, verifyActive } from './verify.js';
|
|
28
|
+
import { appendPending, finalizeToolCall } from '../session/pending.js';
|
|
29
|
+
import { auditLog } from '@cspeach/sap-client';
|
|
30
|
+
import { ERR } from '../errors/codes.js';
|
|
31
|
+
import { interceptUpdateMethod, CancelledByUserError, } from './update-method-intercept.js';
|
|
32
|
+
import { presentSafetyConfirmation } from '../repl/safety-confirm.js';
|
|
33
|
+
import { isSafetyConfirmEnabled } from '../repl/safety-mode-state.js';
|
|
34
|
+
registerTool({
|
|
35
|
+
name: 'sap_set_source',
|
|
36
|
+
description: 'Overwrite the full source of an ABAP object. Requires approval_id. '
|
|
37
|
+
+ '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).',
|
|
39
|
+
isMutating: true,
|
|
40
|
+
input_schema: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
name: { type: 'string', description: 'ABAP object name' },
|
|
44
|
+
type: { type: 'string', description: 'ABAP object type (PROG, CLAS, etc.)' },
|
|
45
|
+
source: { type: 'string', description: 'Full new source (replaces existing)' },
|
|
46
|
+
transport: { type: 'string', description: 'Transport request number (optional)' },
|
|
47
|
+
approval_id: { type: 'string', description: 'JWT from request_approval' },
|
|
48
|
+
},
|
|
49
|
+
required: ['name', 'type', 'source', 'approval_id'],
|
|
50
|
+
},
|
|
51
|
+
handler: async (args, ctx) => {
|
|
52
|
+
// ── Gate 1: Approval ────────────────────────────────────────────────────
|
|
53
|
+
const verify = await verifyAndSpendApprovalId(args.approval_id, args.name, 'modify');
|
|
54
|
+
if (!verify.ok) {
|
|
55
|
+
return {
|
|
56
|
+
content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: verify.reason }),
|
|
57
|
+
is_error: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const toolUseId = crypto.randomUUID();
|
|
61
|
+
const startedAt = Date.now();
|
|
62
|
+
// ── Rule 9 conflict warning ──────────────────────────────────────────────
|
|
63
|
+
// Emit a warning if the LLM passed an explicit transport that differs from
|
|
64
|
+
// the session current-transport. Prefer LLM intent (explicit caller); this
|
|
65
|
+
// is informational only and does NOT block the operation.
|
|
66
|
+
{
|
|
67
|
+
const sessionTransport = ctx.currentTransport?.get() ?? null;
|
|
68
|
+
if (args.transport && sessionTransport && args.transport !== sessionTransport) {
|
|
69
|
+
ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// ── Gate 2: Snapshot (Rule 7) ────────────────────────────────────────────
|
|
73
|
+
try {
|
|
74
|
+
await autoSnapshot(args.name, args.type, ctx.adt);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
return {
|
|
78
|
+
content: JSON.stringify({ error: ERR.SNAPSHOT_FAILED, detail: String(err) }),
|
|
79
|
+
is_error: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// ── Gate 3: Safety confirmation (Rule 7) ─────────────────────────────────
|
|
83
|
+
if (isSafetyConfirmEnabled()) {
|
|
84
|
+
const r = await presentSafetyConfirmation({
|
|
85
|
+
rule: 'RULE_7_SNAPSHOT_BEFORE_WRITE',
|
|
86
|
+
op: 'set_source',
|
|
87
|
+
object: { type: args.type, name: args.name },
|
|
88
|
+
what_will_happen: 'Overwrites full object source; snapshot is taken first.',
|
|
89
|
+
rollback_available: true,
|
|
90
|
+
transport: args.transport ?? ctx.currentTransport?.get() ?? '',
|
|
91
|
+
});
|
|
92
|
+
if (!r.confirmed) {
|
|
93
|
+
return {
|
|
94
|
+
content: JSON.stringify({ error: 'cancelled_by_user', reason: r.reason }),
|
|
95
|
+
is_error: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// ── WAL: record pending entry ────────────────────────────────────────────
|
|
100
|
+
await appendPending(ctx.session, {
|
|
101
|
+
tool_use_id: toolUseId,
|
|
102
|
+
tool: 'sap_set_source',
|
|
103
|
+
args: { name: args.name, type: args.type },
|
|
104
|
+
started_at: new Date().toISOString(),
|
|
105
|
+
});
|
|
106
|
+
// ── Write ────────────────────────────────────────────────────────────────
|
|
107
|
+
// setSource(type, name, source, transport?) — transport is optional.
|
|
108
|
+
//
|
|
109
|
+
// CRITICAL: AdtClient.setSource only THROWS for catastrophic errors (network
|
|
110
|
+
// down, all retries exhausted). For HTTP 4xx responses (e.g., the second PUT
|
|
111
|
+
// after 423-recovery still failing) it RETURNS a WriteResult with
|
|
112
|
+
// writeStatus >= 400 and `error: '...'`. The handler MUST check the return
|
|
113
|
+
// value — without it, we silently report success on a failed write.
|
|
114
|
+
// Regression evidence + diagnosis: memory/project_phase3_lite_regression.md.
|
|
115
|
+
let writeResult;
|
|
116
|
+
try {
|
|
117
|
+
writeResult = await ctx.adt.setSource(args.type, args.name, args.source, args.transport);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const errStr = String(err);
|
|
121
|
+
await finalizeToolCall(ctx.session, toolUseId, errStr, true, Date.now() - startedAt);
|
|
122
|
+
return {
|
|
123
|
+
content: JSON.stringify({ error: 'write_failed', detail: errStr }),
|
|
124
|
+
is_error: true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (writeResult.error || (writeResult.writeStatus !== undefined && writeResult.writeStatus >= 400)) {
|
|
128
|
+
const detail = writeResult.error ?? `Write returned HTTP ${writeResult.writeStatus} but did not throw`;
|
|
129
|
+
await finalizeToolCall(ctx.session, toolUseId, detail, true, Date.now() - startedAt);
|
|
130
|
+
return {
|
|
131
|
+
content: JSON.stringify({
|
|
132
|
+
error: 'write_failed',
|
|
133
|
+
detail,
|
|
134
|
+
write_status: writeResult.writeStatus,
|
|
135
|
+
}),
|
|
136
|
+
is_error: true,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
await auditLog('setSource', args.type, args.name, 'success', Date.now() - startedAt);
|
|
140
|
+
// ── Gate 4: Verify — syntax check (Rule 10) ──────────────────────────────
|
|
141
|
+
const syntax = await verifySyntax(ctx.adt, args.name, args.type);
|
|
142
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ written: true, syntax }), false, Date.now() - startedAt);
|
|
143
|
+
return {
|
|
144
|
+
content: JSON.stringify({
|
|
145
|
+
written: true,
|
|
146
|
+
syntax_ok: syntax.ok,
|
|
147
|
+
errors: syntax.errors,
|
|
148
|
+
warnings: syntax.warnings,
|
|
149
|
+
next_step: syntax.ok
|
|
150
|
+
? 'activation_allowed'
|
|
151
|
+
: 'activation_blocked_until_syntax_errors_fixed',
|
|
152
|
+
}),
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
// ── sap_update_method ────────────────────────────────────────────────────────
|
|
157
|
+
//
|
|
158
|
+
// Rule 7a: method-body-only edits. Reads current source, replaces the named
|
|
159
|
+
// method block in-memory, then delegates to setSource internally via
|
|
160
|
+
// AdtClient.updateMethod. Does NOT rewrite the class definition.
|
|
161
|
+
//
|
|
162
|
+
// Signature: updateMethod(className, methodName, methodBody, transport?)
|
|
163
|
+
// methodBody — the lines BETWEEN METHOD x. and ENDMETHOD. (no wrappers)
|
|
164
|
+
registerTool({
|
|
165
|
+
name: 'sap_update_method',
|
|
166
|
+
description: 'Replace the body of a single method in an existing class. '
|
|
167
|
+
+ 'Preferred over sap_set_source for method-body-only changes (Rule 7a) — '
|
|
168
|
+
+ 'does NOT rewrite the class definition. '
|
|
169
|
+
+ 'method_source must be the lines between METHOD x. and ENDMETHOD. (no wrappers).',
|
|
170
|
+
isMutating: true,
|
|
171
|
+
input_schema: {
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties: {
|
|
174
|
+
class_name: { type: 'string', description: 'ABAP class name (e.g. ZCL_MY_CLASS)' },
|
|
175
|
+
method_name: { type: 'string', description: 'Method name (e.g. DO_WORK or IF_FOO~BAR)' },
|
|
176
|
+
method_source: { type: 'string', description: 'Lines between METHOD x. and ENDMETHOD. (no wrapper lines)' },
|
|
177
|
+
transport: { type: 'string', description: 'Transport request number (optional)' },
|
|
178
|
+
approval_id: { type: 'string', description: 'JWT from request_approval' },
|
|
179
|
+
},
|
|
180
|
+
required: ['class_name', 'method_name', 'method_source', 'approval_id'],
|
|
181
|
+
},
|
|
182
|
+
handler: async (args, ctx) => {
|
|
183
|
+
// ── Gate 1: Approval ────────────────────────────────────────────────────
|
|
184
|
+
const verify = await verifyAndSpendApprovalId(args.approval_id, args.class_name, 'modify');
|
|
185
|
+
if (!verify.ok) {
|
|
186
|
+
return {
|
|
187
|
+
content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: verify.reason }),
|
|
188
|
+
is_error: true,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const toolUseId = crypto.randomUUID();
|
|
192
|
+
const startedAt = Date.now();
|
|
193
|
+
// ── Rule 9 conflict warning ──────────────────────────────────────────────
|
|
194
|
+
// Emit a warning if the LLM passed an explicit transport that differs from
|
|
195
|
+
// the session current-transport. Prefer LLM intent (explicit caller); this
|
|
196
|
+
// is informational only and does NOT block the operation.
|
|
197
|
+
{
|
|
198
|
+
const sessionTransport = ctx.currentTransport?.get() ?? null;
|
|
199
|
+
if (args.transport && sessionTransport && args.transport !== sessionTransport) {
|
|
200
|
+
ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// ── Gate 2: Snapshot (Rule 7) ────────────────────────────────────────────
|
|
204
|
+
try {
|
|
205
|
+
await autoSnapshot(args.class_name, 'CLAS', ctx.adt);
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
return {
|
|
209
|
+
content: JSON.stringify({ error: ERR.SNAPSHOT_FAILED, detail: String(err) }),
|
|
210
|
+
is_error: true,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
// ── Gate 3: Diff preview & confirmation (Rule 7a) ────────────────────────
|
|
214
|
+
if (!ctx.previewHook) {
|
|
215
|
+
return {
|
|
216
|
+
content: JSON.stringify({
|
|
217
|
+
error: 'preview_hook_unavailable',
|
|
218
|
+
detail: 'cannot confirm method body changes in this execution context',
|
|
219
|
+
}),
|
|
220
|
+
is_error: true,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const minimal = {
|
|
224
|
+
getSource: ({ type, name }) => ctx.adt.getSource(type, name),
|
|
225
|
+
};
|
|
226
|
+
const call = {
|
|
227
|
+
className: args.class_name,
|
|
228
|
+
methodName: args.method_name,
|
|
229
|
+
methodSource: args.method_source,
|
|
230
|
+
transport: args.transport ?? ctx.currentTransport?.get() ?? '',
|
|
231
|
+
};
|
|
232
|
+
try {
|
|
233
|
+
await interceptUpdateMethod(minimal, call, ctx.previewHook);
|
|
234
|
+
}
|
|
235
|
+
catch (err) {
|
|
236
|
+
if (err instanceof CancelledByUserError) {
|
|
237
|
+
return {
|
|
238
|
+
content: JSON.stringify({ error: 'cancelled_by_user', detail: err.message }),
|
|
239
|
+
is_error: true,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
content: JSON.stringify({ error: 'preview_failed', detail: String(err) }),
|
|
244
|
+
is_error: true,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
// ── WAL: record pending entry ────────────────────────────────────────────
|
|
248
|
+
await appendPending(ctx.session, {
|
|
249
|
+
tool_use_id: toolUseId,
|
|
250
|
+
tool: 'sap_update_method',
|
|
251
|
+
args: { class_name: args.class_name, method_name: args.method_name },
|
|
252
|
+
started_at: new Date().toISOString(),
|
|
253
|
+
});
|
|
254
|
+
// ── Write ────────────────────────────────────────────────────────────────
|
|
255
|
+
// updateMethod(className, methodName, methodBody, transport?). Same return
|
|
256
|
+
// semantics as setSource — RETURNS a WriteResult on HTTP-level failure
|
|
257
|
+
// rather than throwing. Must check writeStatus / error on the return value.
|
|
258
|
+
let updateResult;
|
|
259
|
+
try {
|
|
260
|
+
updateResult = await ctx.adt.updateMethod(args.class_name, args.method_name, args.method_source, args.transport);
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
const errStr = String(err);
|
|
264
|
+
await finalizeToolCall(ctx.session, toolUseId, errStr, true, Date.now() - startedAt);
|
|
265
|
+
return {
|
|
266
|
+
content: JSON.stringify({ error: 'update_failed', detail: errStr }),
|
|
267
|
+
is_error: true,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (updateResult.error || (updateResult.writeStatus !== undefined && updateResult.writeStatus >= 400)) {
|
|
271
|
+
const detail = updateResult.error ?? `Update returned HTTP ${updateResult.writeStatus} but did not throw`;
|
|
272
|
+
await finalizeToolCall(ctx.session, toolUseId, detail, true, Date.now() - startedAt);
|
|
273
|
+
return {
|
|
274
|
+
content: JSON.stringify({
|
|
275
|
+
error: 'update_failed',
|
|
276
|
+
detail,
|
|
277
|
+
write_status: updateResult.writeStatus,
|
|
278
|
+
}),
|
|
279
|
+
is_error: true,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
await auditLog('updateMethod', 'CLAS', args.class_name, 'success', Date.now() - startedAt, `method=${args.method_name}`);
|
|
283
|
+
// ── Gate 4: Verify — syntax check (Rule 10) ──────────────────────────────
|
|
284
|
+
const syntax = await verifySyntax(ctx.adt, args.class_name, 'CLAS');
|
|
285
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ updated: true, syntax }), false, Date.now() - startedAt);
|
|
286
|
+
return {
|
|
287
|
+
content: JSON.stringify({
|
|
288
|
+
updated: true,
|
|
289
|
+
syntax_ok: syntax.ok,
|
|
290
|
+
errors: syntax.errors,
|
|
291
|
+
warnings: syntax.warnings,
|
|
292
|
+
next_step: syntax.ok
|
|
293
|
+
? 'activation_allowed'
|
|
294
|
+
: 'activation_blocked_until_syntax_errors_fixed',
|
|
295
|
+
}),
|
|
296
|
+
};
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
// ── sap_delete_object ────────────────────────────────────────────────────────
|
|
300
|
+
//
|
|
301
|
+
// Signature: deleteObject(type, name, transport?) → { status: number; error?: string }
|
|
302
|
+
// AdtClient auto-snapshots before delete (Rule 7 built-in), but we also call
|
|
303
|
+
// autoSnapshot here for belt-and-suspenders consistency with the CLI gate model.
|
|
304
|
+
registerTool({
|
|
305
|
+
name: 'sap_delete_object',
|
|
306
|
+
description: 'Delete an ABAP object. Requires approval_id. '
|
|
307
|
+
+ 'A snapshot is taken automatically before deletion. '
|
|
308
|
+
+ 'Post-check: confirms the object no longer exists.',
|
|
309
|
+
isMutating: true,
|
|
310
|
+
input_schema: {
|
|
311
|
+
type: 'object',
|
|
312
|
+
properties: {
|
|
313
|
+
name: { type: 'string', description: 'ABAP object name' },
|
|
314
|
+
type: { type: 'string', description: 'ABAP object type (PROG, CLAS, etc.)' },
|
|
315
|
+
transport: { type: 'string', description: 'Transport request number (optional)' },
|
|
316
|
+
approval_id: { type: 'string', description: 'JWT from request_approval' },
|
|
317
|
+
},
|
|
318
|
+
required: ['name', 'type', 'approval_id'],
|
|
319
|
+
},
|
|
320
|
+
handler: async (args, ctx) => {
|
|
321
|
+
// ── Gate 1: Approval ────────────────────────────────────────────────────
|
|
322
|
+
const verify = await verifyAndSpendApprovalId(args.approval_id, args.name, 'delete');
|
|
323
|
+
if (!verify.ok) {
|
|
324
|
+
return {
|
|
325
|
+
content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: verify.reason }),
|
|
326
|
+
is_error: true,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const toolUseId = crypto.randomUUID();
|
|
330
|
+
const startedAt = Date.now();
|
|
331
|
+
// ── Rule 9 conflict warning ──────────────────────────────────────────────
|
|
332
|
+
// Emit a warning if the LLM passed an explicit transport that differs from
|
|
333
|
+
// the session current-transport. Prefer LLM intent (explicit caller); this
|
|
334
|
+
// is informational only and does NOT block the operation.
|
|
335
|
+
{
|
|
336
|
+
const sessionTransport = ctx.currentTransport?.get() ?? null;
|
|
337
|
+
if (args.transport && sessionTransport && args.transport !== sessionTransport) {
|
|
338
|
+
ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// ── Gate 2: Snapshot (Rule 7) — best-effort, object may not have source ──
|
|
342
|
+
try {
|
|
343
|
+
await autoSnapshot(args.name, args.type, ctx.adt);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// Silently ignore — object may be a shell with no readable source.
|
|
347
|
+
}
|
|
348
|
+
// ── Gate 3: Safety confirmation (Rule 7 — delete is irreversible-ish) ────
|
|
349
|
+
if (isSafetyConfirmEnabled()) {
|
|
350
|
+
const r = await presentSafetyConfirmation({
|
|
351
|
+
rule: 'RULE_7_SNAPSHOT_BEFORE_WRITE',
|
|
352
|
+
op: 'delete_object',
|
|
353
|
+
object: { type: args.type, name: args.name },
|
|
354
|
+
what_will_happen: 'Deletes object from SAP; snapshot is taken first; recovery via sap_snapshot_read + sap_set_source (LLM composes the rollback).',
|
|
355
|
+
rollback_available: true,
|
|
356
|
+
transport: args.transport ?? ctx.currentTransport?.get() ?? '',
|
|
357
|
+
});
|
|
358
|
+
if (!r.confirmed) {
|
|
359
|
+
return {
|
|
360
|
+
content: JSON.stringify({ error: 'cancelled_by_user', reason: r.reason }),
|
|
361
|
+
is_error: true,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// ── WAL: record pending entry ────────────────────────────────────────────
|
|
366
|
+
await appendPending(ctx.session, {
|
|
367
|
+
tool_use_id: toolUseId,
|
|
368
|
+
tool: 'sap_delete_object',
|
|
369
|
+
args: { name: args.name, type: args.type },
|
|
370
|
+
started_at: new Date().toISOString(),
|
|
371
|
+
});
|
|
372
|
+
// ── Delete ───────────────────────────────────────────────────────────────
|
|
373
|
+
// deleteObject(type, name, transport?) → { status: number; error?: string }.
|
|
374
|
+
// Same return semantics as setSource — RETURNS rather than throws on HTTP
|
|
375
|
+
// 4xx. Must check status / error on the return value.
|
|
376
|
+
let deleteResult;
|
|
377
|
+
try {
|
|
378
|
+
deleteResult = await ctx.adt.deleteObject(args.type, args.name, args.transport);
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
const errStr = String(err);
|
|
382
|
+
await finalizeToolCall(ctx.session, toolUseId, errStr, true, Date.now() - startedAt);
|
|
383
|
+
return {
|
|
384
|
+
content: JSON.stringify({ error: 'delete_failed', detail: errStr }),
|
|
385
|
+
is_error: true,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
if (deleteResult.error || (deleteResult.status !== undefined && deleteResult.status >= 400)) {
|
|
389
|
+
const detail = deleteResult.error ?? `Delete returned HTTP ${deleteResult.status} but did not throw`;
|
|
390
|
+
await finalizeToolCall(ctx.session, toolUseId, detail, true, Date.now() - startedAt);
|
|
391
|
+
return {
|
|
392
|
+
content: JSON.stringify({
|
|
393
|
+
error: 'delete_failed',
|
|
394
|
+
detail,
|
|
395
|
+
status: deleteResult.status,
|
|
396
|
+
}),
|
|
397
|
+
is_error: true,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
await auditLog('deleteObject', args.type, args.name, 'success', Date.now() - startedAt);
|
|
401
|
+
// ── Post-check: object should no longer exist ────────────────────────────
|
|
402
|
+
const stillExists = await ctx.adt.objectStructure(args.type, args.name)
|
|
403
|
+
.then(() => true)
|
|
404
|
+
.catch(() => false);
|
|
405
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ deleted: !stillExists }), false, Date.now() - startedAt);
|
|
406
|
+
return {
|
|
407
|
+
content: JSON.stringify({ deleted: !stillExists, still_exists: stillExists }),
|
|
408
|
+
};
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
// ── sap_create_object ────────────────────────────────────────────────────────
|
|
412
|
+
//
|
|
413
|
+
// Signature: createObject(objtype, name, packageName, description, transport?, extras?)
|
|
414
|
+
// → Promise<CreateResult>
|
|
415
|
+
// NOTE: arg order differs from the plan snippet — packageName comes before description.
|
|
416
|
+
// No snapshot needed: new object has no prior source.
|
|
417
|
+
registerTool({
|
|
418
|
+
name: 'sap_create_object',
|
|
419
|
+
description: 'Create a new ABAP object shell. '
|
|
420
|
+
+ 'Requires approval_id. No snapshot needed (new object). '
|
|
421
|
+
+ 'Write source separately via sap_set_source, then call sap_activate.',
|
|
422
|
+
isMutating: true,
|
|
423
|
+
input_schema: {
|
|
424
|
+
type: 'object',
|
|
425
|
+
properties: {
|
|
426
|
+
name: { type: 'string', description: 'ABAP object name' },
|
|
427
|
+
type: { type: 'string', description: 'ABAP object type (PROG, CLAS, INTF, DDLS, etc.)' },
|
|
428
|
+
package: { type: 'string', description: 'Development package (Z*/Y*)' },
|
|
429
|
+
description: { type: 'string', description: 'Short description' },
|
|
430
|
+
transport: { type: 'string', description: 'Target transport (optional — required for workbench packages)' },
|
|
431
|
+
approval_id: { type: 'string', description: 'JWT from request_approval' },
|
|
432
|
+
},
|
|
433
|
+
required: ['name', 'type', 'package', 'description', 'approval_id'],
|
|
434
|
+
},
|
|
435
|
+
handler: async (args, ctx) => {
|
|
436
|
+
// ── Gate 1: Approval ────────────────────────────────────────────────────
|
|
437
|
+
const verify = await verifyAndSpendApprovalId(args.approval_id, args.name, 'create');
|
|
438
|
+
if (!verify.ok) {
|
|
439
|
+
return {
|
|
440
|
+
content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: verify.reason }),
|
|
441
|
+
is_error: true,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
const toolUseId = crypto.randomUUID();
|
|
445
|
+
const startedAt = Date.now();
|
|
446
|
+
// No snapshot — new object has no prior source.
|
|
447
|
+
// ── Rule 9 conflict warning + 3-tier transport fallback ──────────────────
|
|
448
|
+
// Emit a warning if the LLM passed an explicit transport that differs from
|
|
449
|
+
// the session current-transport. Prefer LLM intent (explicit caller); this
|
|
450
|
+
// is informational only and does NOT block the operation.
|
|
451
|
+
{
|
|
452
|
+
const sessionTransport = ctx.currentTransport?.get() ?? null;
|
|
453
|
+
if (args.transport && sessionTransport && args.transport !== sessionTransport) {
|
|
454
|
+
ctx.chunkEmitter?.emit('warn', `LLM passed transport=${args.transport} but session current-transport=${sessionTransport}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
// 3-tier fallback: explicit arg → session current-transport → undefined
|
|
458
|
+
const effectiveTransport = args.transport ?? ctx.currentTransport?.get() ?? undefined;
|
|
459
|
+
// ── WAL: record pending entry ────────────────────────────────────────────
|
|
460
|
+
await appendPending(ctx.session, {
|
|
461
|
+
tool_use_id: toolUseId,
|
|
462
|
+
tool: 'sap_create_object',
|
|
463
|
+
args: { name: args.name, type: args.type, package: args.package },
|
|
464
|
+
started_at: new Date().toISOString(),
|
|
465
|
+
});
|
|
466
|
+
// ── Create ───────────────────────────────────────────────────────────────
|
|
467
|
+
// createObject(objtype, name, packageName, description, transport?, extras?)
|
|
468
|
+
let result;
|
|
469
|
+
try {
|
|
470
|
+
result = await ctx.adt.createObject(args.type, args.name, args.package, args.description, effectiveTransport);
|
|
471
|
+
await auditLog('createObject', args.type, args.name, 'success', Date.now() - startedAt);
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
const errStr = String(err);
|
|
475
|
+
await finalizeToolCall(ctx.session, toolUseId, errStr, true, Date.now() - startedAt);
|
|
476
|
+
return {
|
|
477
|
+
content: JSON.stringify({ error: 'create_failed', detail: errStr }),
|
|
478
|
+
is_error: true,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
if (result.status !== 'created') {
|
|
482
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), true, Date.now() - startedAt);
|
|
483
|
+
return {
|
|
484
|
+
content: JSON.stringify({ error: 'create_failed', detail: result.error }),
|
|
485
|
+
is_error: true,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify(result), false, Date.now() - startedAt);
|
|
489
|
+
return {
|
|
490
|
+
content: JSON.stringify({
|
|
491
|
+
created: true,
|
|
492
|
+
object: result.objectName,
|
|
493
|
+
type: result.objectType,
|
|
494
|
+
package: result.packageName,
|
|
495
|
+
next_step: 'write_source_via_sap_set_source_then_activate',
|
|
496
|
+
}),
|
|
497
|
+
};
|
|
498
|
+
},
|
|
499
|
+
});
|
|
500
|
+
// ── sap_activate ─────────────────────────────────────────────────────────────
|
|
501
|
+
//
|
|
502
|
+
// Signature: activate(type, name) → Promise<ActivationResult>
|
|
503
|
+
// ActivationResult { success, hasErrors, hasWarnings, messages[] }
|
|
504
|
+
//
|
|
505
|
+
// AdtClient activates one object per call. This tool accepts a batch (array)
|
|
506
|
+
// and calls activate() per object, collecting results. Post-check via
|
|
507
|
+
// verifyActive() inspects the inactive-objects list.
|
|
508
|
+
registerTool({
|
|
509
|
+
name: 'sap_activate',
|
|
510
|
+
description: 'Activate one or more ABAP objects. Required after sap_set_source / '
|
|
511
|
+
+ 'sap_update_method / sap_create_object (Rule 10). '
|
|
512
|
+
+ 'Post-check: objects must no longer appear in the inactive-objects list.',
|
|
513
|
+
isMutating: true,
|
|
514
|
+
input_schema: {
|
|
515
|
+
type: 'object',
|
|
516
|
+
properties: {
|
|
517
|
+
objects: {
|
|
518
|
+
type: 'array',
|
|
519
|
+
description: 'List of objects to activate',
|
|
520
|
+
items: {
|
|
521
|
+
type: 'object',
|
|
522
|
+
properties: {
|
|
523
|
+
name: { type: 'string' },
|
|
524
|
+
type: { type: 'string' },
|
|
525
|
+
},
|
|
526
|
+
required: ['name', 'type'],
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
approval_id: {
|
|
530
|
+
type: 'string',
|
|
531
|
+
description: 'JWT from request_approval — one approval covers the whole batch',
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
required: ['objects', 'approval_id'],
|
|
535
|
+
},
|
|
536
|
+
handler: async (args, ctx) => {
|
|
537
|
+
if (!Array.isArray(args.objects) || args.objects.length === 0) {
|
|
538
|
+
return { content: JSON.stringify({ activated: [], note: 'empty_activation_batch' }) };
|
|
539
|
+
}
|
|
540
|
+
// ── Gate 1: Approval (verify against primary object) ────────────────────
|
|
541
|
+
const primary = args.objects[0];
|
|
542
|
+
const verify = await verifyAndSpendApprovalId(args.approval_id, primary.name, 'activate');
|
|
543
|
+
if (!verify.ok) {
|
|
544
|
+
return {
|
|
545
|
+
content: JSON.stringify({ error: ERR.APPROVAL_INVALID, reason: verify.reason }),
|
|
546
|
+
is_error: true,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
const toolUseId = crypto.randomUUID();
|
|
550
|
+
const startedAt = Date.now();
|
|
551
|
+
// ── WAL: record pending entry ────────────────────────────────────────────
|
|
552
|
+
await appendPending(ctx.session, {
|
|
553
|
+
tool_use_id: toolUseId,
|
|
554
|
+
tool: 'sap_activate',
|
|
555
|
+
args: { objects: args.objects },
|
|
556
|
+
started_at: new Date().toISOString(),
|
|
557
|
+
});
|
|
558
|
+
// ── Activate each object, collect per-object results ─────────────────────
|
|
559
|
+
// activate(type, name) — one call per object; no batch API in AdtClient.
|
|
560
|
+
const activationResults = [];
|
|
561
|
+
let anyError = false;
|
|
562
|
+
for (const obj of args.objects) {
|
|
563
|
+
try {
|
|
564
|
+
const res = await ctx.adt.activate(obj.type, obj.name);
|
|
565
|
+
await auditLog('activate', obj.type, obj.name, res.success ? 'success' : 'error', Date.now() - startedAt);
|
|
566
|
+
activationResults.push({ name: obj.name, type: obj.type, success: res.success, messages: res.messages });
|
|
567
|
+
if (!res.success)
|
|
568
|
+
anyError = true;
|
|
569
|
+
}
|
|
570
|
+
catch (err) {
|
|
571
|
+
activationResults.push({ name: obj.name, type: obj.type, success: false, messages: [String(err)] });
|
|
572
|
+
anyError = true;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// ── Post-check: verify none of the objects remain inactive ───────────────
|
|
576
|
+
// 2026-05-01: verifyActive now returns { active, verifyError? }. If the
|
|
577
|
+
// inactive-objects probe itself failed, we MUST surface that as
|
|
578
|
+
// "unverified" — not silently treat it as "activated". This was the
|
|
579
|
+
// root cause of the "told user activate succeeded when program was
|
|
580
|
+
// still inactive with syntax errors" incident.
|
|
581
|
+
const stillInactive = [];
|
|
582
|
+
const unverifiable = [];
|
|
583
|
+
for (const obj of args.objects) {
|
|
584
|
+
const v = await verifyActive(ctx.adt, obj.name, obj.type);
|
|
585
|
+
if (v.verifyError) {
|
|
586
|
+
unverifiable.push({ object: `${obj.type}/${obj.name}`, reason: v.verifyError });
|
|
587
|
+
}
|
|
588
|
+
else if (!v.active) {
|
|
589
|
+
stillInactive.push(`${obj.type}/${obj.name}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const overallSuccess = !anyError && stillInactive.length === 0 && unverifiable.length === 0;
|
|
593
|
+
await finalizeToolCall(ctx.session, toolUseId, JSON.stringify({ overallSuccess, activationResults, stillInactive, unverifiable }), anyError, Date.now() - startedAt);
|
|
594
|
+
let note;
|
|
595
|
+
if (stillInactive.length > 0) {
|
|
596
|
+
note = 'Some objects remain inactive — check activation_results for error messages';
|
|
597
|
+
}
|
|
598
|
+
else if (unverifiable.length > 0) {
|
|
599
|
+
note = 'Activation reported success but verification failed — DO NOT claim activation succeeded. ' +
|
|
600
|
+
'Re-run sap_inactive_objects manually to confirm state. See `unverifiable` for the underlying error.';
|
|
601
|
+
}
|
|
602
|
+
// 2026-05-01: when activation fails, return `is_error: true` so the
|
|
603
|
+
// dispatch layer (tool-dispatch.ts) does NOT credit this call as a
|
|
604
|
+
// successful write op. Without this, Rule 8 counts a failed activate
|
|
605
|
+
// as if it had run, inflating the "batch in progress" count and
|
|
606
|
+
// confusing the user about how many real writes have happened.
|
|
607
|
+
return {
|
|
608
|
+
content: JSON.stringify({
|
|
609
|
+
success: overallSuccess,
|
|
610
|
+
activation_results: activationResults,
|
|
611
|
+
still_inactive: stillInactive,
|
|
612
|
+
unverifiable,
|
|
613
|
+
note,
|
|
614
|
+
}),
|
|
615
|
+
is_error: !overallSuccess,
|
|
616
|
+
};
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
// ── sap_lock ──────────────────────────────────────────────────────────────────
|
|
620
|
+
//
|
|
621
|
+
// Lightweight lock primitive — does NOT go through the approval / WAL / verify
|
|
622
|
+
// pipeline. Used to acquire an edit lock on an object before a manual PUT.
|
|
623
|
+
// Signature: lock(type, name) → Promise<LockResult>
|
|
624
|
+
// LockResult { lockHandle: string; status: 'locked' | 'lock_failed' }
|
|
625
|
+
registerTool({
|
|
626
|
+
name: 'sap_lock',
|
|
627
|
+
description: 'RARELY needed. Returns a lock handle but other write tools cannot consume it — sap_set_source / sap_update_method / sap_delete_object each acquire their OWN lock internally. Calling sap_lock as a recovery step after a write failure CREATES A STALE LOCK that blocks the next write attempt; do not do that. Only call this when you need to inspect lock state or hold a lock for a coordinated multi-step ABAP workbench operation outside of CSPeach.',
|
|
628
|
+
isMutating: true,
|
|
629
|
+
input_schema: {
|
|
630
|
+
type: 'object',
|
|
631
|
+
properties: {
|
|
632
|
+
type: { type: 'string', description: 'ABAP object type (PROG, CLAS, etc.)' },
|
|
633
|
+
name: { type: 'string', description: 'ABAP object name' },
|
|
634
|
+
},
|
|
635
|
+
required: ['type', 'name'],
|
|
636
|
+
},
|
|
637
|
+
handler: async (args, ctx) => {
|
|
638
|
+
const result = await ctx.adt.lock(args.type, args.name);
|
|
639
|
+
return { content: JSON.stringify(result) };
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
// ── sap_unlock ────────────────────────────────────────────────────────────────
|
|
643
|
+
//
|
|
644
|
+
// Release an ADT edit lock previously acquired with sap_lock.
|
|
645
|
+
// Signature: unlock(type, name, lockHandle) → Promise<void>
|
|
646
|
+
registerTool({
|
|
647
|
+
name: 'sap_unlock',
|
|
648
|
+
description: 'Release an ADT edit lock on an ABAP object using the lockHandle from sap_lock.',
|
|
649
|
+
isMutating: true,
|
|
650
|
+
input_schema: {
|
|
651
|
+
type: 'object',
|
|
652
|
+
properties: {
|
|
653
|
+
type: { type: 'string', description: 'ABAP object type (PROG, CLAS, etc.)' },
|
|
654
|
+
name: { type: 'string', description: 'ABAP object name' },
|
|
655
|
+
lockHandle: { type: 'string', description: 'Lock handle returned by sap_lock' },
|
|
656
|
+
},
|
|
657
|
+
required: ['type', 'name', 'lockHandle'],
|
|
658
|
+
},
|
|
659
|
+
handler: async (args, ctx) => {
|
|
660
|
+
await ctx.adt.unlock(args.type, args.name, args.lockHandle);
|
|
661
|
+
return { content: JSON.stringify({ status: 'ok' }) };
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
// ── sap_service_binding_publish ───────────────────────────────────────────────
|
|
665
|
+
//
|
|
666
|
+
// Publish a service binding so it is reachable via OData.
|
|
667
|
+
// Signature: serviceBindingPublish(name) → Promise<{ status: number; error?: string }>
|
|
668
|
+
registerTool({
|
|
669
|
+
name: 'sap_service_binding_publish',
|
|
670
|
+
description: 'Publish an ADT service binding so its OData endpoint becomes active and reachable.',
|
|
671
|
+
isMutating: true,
|
|
672
|
+
input_schema: {
|
|
673
|
+
type: 'object',
|
|
674
|
+
properties: {
|
|
675
|
+
name: { type: 'string', description: 'Service binding name (e.g. ZSALES_UI_V4)' },
|
|
676
|
+
},
|
|
677
|
+
required: ['name'],
|
|
678
|
+
},
|
|
679
|
+
handler: async (args, ctx) => {
|
|
680
|
+
const result = await ctx.adt.serviceBindingPublish(args.name);
|
|
681
|
+
return { content: JSON.stringify(result) };
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
// ── sap_message_maintain (STUB) ───────────────────────────────────────────────
|
|
685
|
+
//
|
|
686
|
+
// STUB — MSAG message-class maintenance requires XML message-class-format
|
|
687
|
+
// parsing not yet implemented in @cspeach/sap-client.
|
|
688
|
+
registerTool({
|
|
689
|
+
name: 'sap_message_maintain',
|
|
690
|
+
description: 'STUB — not currently implemented; do not call. '
|
|
691
|
+
+ 'Maintaining MSAG message-class entries via ADT requires XML message-class-format '
|
|
692
|
+
+ 'parsing not yet implemented in @cspeach/sap-client.',
|
|
693
|
+
isMutating: true,
|
|
694
|
+
input_schema: {
|
|
695
|
+
type: 'object',
|
|
696
|
+
properties: {
|
|
697
|
+
messageClass: { type: 'string', description: 'Message class name (e.g. ZMSAG_FOO)' },
|
|
698
|
+
messageNumber: { type: 'string', description: 'Three-digit message number (e.g. 001)' },
|
|
699
|
+
messageText: { type: 'string', description: 'Message text (up to 73 characters)' },
|
|
700
|
+
messageType: { type: 'string', description: 'Message type: I, W, E, A, X, S' },
|
|
701
|
+
},
|
|
702
|
+
required: ['messageClass', 'messageNumber', 'messageText'],
|
|
703
|
+
},
|
|
704
|
+
handler: async (_args, _ctx) => {
|
|
705
|
+
return {
|
|
706
|
+
content: JSON.stringify({
|
|
707
|
+
error: 'not_implemented',
|
|
708
|
+
detail: 'MSAG message maintenance via ADT requires XML message-class-format parsing not yet implemented in @cspeach/sap-client',
|
|
709
|
+
}),
|
|
710
|
+
is_error: true,
|
|
711
|
+
};
|
|
712
|
+
},
|
|
713
|
+
});
|
|
714
|
+
// ── sap_number_range_intervals (STUB) ─────────────────────────────────────────
|
|
715
|
+
//
|
|
716
|
+
// STUB — NROB interval write requires NR-object XML format not yet implemented
|
|
717
|
+
// in @cspeach/sap-client.
|
|
718
|
+
registerTool({
|
|
719
|
+
name: 'sap_number_range_intervals',
|
|
720
|
+
description: 'STUB — not currently implemented; do not call. '
|
|
721
|
+
+ 'Writing NROB number-range intervals via ADT requires NR-object XML format '
|
|
722
|
+
+ 'not yet implemented in @cspeach/sap-client.',
|
|
723
|
+
isMutating: true,
|
|
724
|
+
input_schema: {
|
|
725
|
+
type: 'object',
|
|
726
|
+
properties: {
|
|
727
|
+
nrObject: { type: 'string', description: 'Number range object name' },
|
|
728
|
+
subobject: { type: 'string', description: 'Subobject (leave blank for main object)' },
|
|
729
|
+
intervals: {
|
|
730
|
+
type: 'array',
|
|
731
|
+
description: 'Array of interval definitions',
|
|
732
|
+
items: { type: 'object' },
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
required: ['nrObject'],
|
|
736
|
+
},
|
|
737
|
+
handler: async (_args, _ctx) => {
|
|
738
|
+
return {
|
|
739
|
+
content: JSON.stringify({
|
|
740
|
+
error: 'not_implemented',
|
|
741
|
+
detail: 'NROB interval write via ADT requires NR-object XML format not yet implemented in @cspeach/sap-client',
|
|
742
|
+
}),
|
|
743
|
+
is_error: true,
|
|
744
|
+
};
|
|
745
|
+
},
|
|
746
|
+
});
|