sap-ai-dev-toolkit 0.7.5 → 0.7.9
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 +30 -23
- package/package.json +1 -1
- package/scripts/postinstall.mjs +41 -9
- package/scripts/windows-negotiate-helper.ps1 +182 -0
- package/src/harness-mcp-config.mjs +122 -0
- package/src/harnesses.mjs +29 -1
- package/src/install-user-assets.mjs +45 -5
- package/src/launcher.mjs +47 -4
- package/src/local-sap-gui.mjs +217 -0
- package/src/mcp-config.mjs +45 -5
- package/src/mcp-proxy.mjs +1 -0
- package/src/setup.mjs +125 -26
- package/src/windows-sso-adt-proxy.mjs +224 -0
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
17
|
<img src="https://img.shields.io/badge/SAP-Business%20Application%20Studio-0A6ED1?style=flat-square&logo=sap&logoColor=white" alt="SAP Business Application Studio">
|
|
18
|
-
<img src="https://img.shields.io/badge/AI%20agents-Copilot%20%7C%20Claude%20%7C%20Codex%20%7C%20Cursor%20%7C%20Gemini%20%7C%20opencode-000000?style=flat-square" alt="Works with GitHub Copilot, Claude Code, OpenAI Codex, Cursor, Gemini CLI, and
|
|
18
|
+
<img src="https://img.shields.io/badge/AI%20agents-Copilot%20%7C%20Claude%20%7C%20Codex%20%7C%20Cursor%20%7C%20Gemini%20%7C%20opencode%20%7C%20Pi-000000?style=flat-square" alt="Works with GitHub Copilot, Claude Code, OpenAI Codex, Cursor, Gemini CLI, opencode, and Pi Coding Agent">
|
|
19
19
|
<img src="https://img.shields.io/badge/MCP-enabled-7B61FF?style=flat-square" alt="MCP enabled">
|
|
20
20
|
<img src="https://img.shields.io/badge/SAP%20ADT-connected-0A6ED1?style=flat-square&logo=sap&logoColor=white" alt="SAP ADT connected">
|
|
21
21
|
<img src="https://img.shields.io/badge/ABAP-CDS%20%7C%20RAP-EA4AAA?style=flat-square" alt="ABAP CDS RAP">
|
|
@@ -52,13 +52,13 @@ To also add optional full-stack SAP companion MCP servers for Fiori, UI5, CAP, a
|
|
|
52
52
|
sap-ai-dev --setup --tools
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
Then connect a destination in SAP Business Application Studio:
|
|
55
|
+
Then connect a destination in SAP Business Application Studio, or run the same setup locally to discover SAP GUI systems and create VS Code MCP entries:
|
|
56
56
|
|
|
57
57
|
1. Open the Command Palette.
|
|
58
58
|
2. Run **MCP: List Servers**.
|
|
59
|
-
3. Start the server named after your selected
|
|
60
|
-
4. In your AI assistant's chat, choose the best-fit bundled agent from the agent picker (GitHub Copilot, Claude Code, and opencode install the agents): **SAP Solution Architect**, **ABAP Developer**, **ABAP Runtime Debugger**, **RAP Service Developer**, or **HANA Cloud/HDI Specialist**.
|
|
61
|
-
5. In the Chat tools picker, enable the server for that
|
|
59
|
+
3. Start the server named after your selected destination (the lowercase slug, for example `demo-abap` for destination `DEMO_ABAP`).
|
|
60
|
+
4. In your AI assistant's chat, choose the best-fit bundled agent from the agent picker (GitHub Copilot, Claude Code, and opencode install the agents) or invoke the matching Pi prompt template (`/sap-solution-architect`, `/abap-developer`, `/abap-runtime-debugger`, `/rap-service-developer`, or `/hana-cloud-hdi-specialist`): **SAP Solution Architect**, **ABAP Developer**, **ABAP Runtime Debugger**, **RAP Service Developer**, or **HANA Cloud/HDI Specialist**.
|
|
61
|
+
5. In the Chat tools picker, enable the server for that destination.
|
|
62
62
|
6. Ask the agent to inspect, build, test, or verify something in your SAP landscape.
|
|
63
63
|
|
|
64
64
|
Use the attached destination-prefixed tools directly in chat. Do not launch `sap-ai-dev` or handcraft MCP JSON-RPC in a terminal to discover or call them.
|
|
@@ -67,7 +67,7 @@ Use the attached destination-prefixed tools directly in chat. Do not launch `sap
|
|
|
67
67
|
|
|
68
68
|
## 🤖 Available agents and skills
|
|
69
69
|
|
|
70
|
-
**Agents:** Five user-invocable custom agents are included. Select the best fit from the agent picker in your harness's chat (GitHub Copilot, Claude Code, or opencode), as shown in Quick start.
|
|
70
|
+
**Agents:** Five user-invocable custom agents are included. Select the best fit from the agent picker in your harness's chat (GitHub Copilot, Claude Code, or opencode), or use the matching Pi prompt command, as shown in Quick start.
|
|
71
71
|
|
|
72
72
|
| | Agent | Best for |
|
|
73
73
|
| --- | --- | --- |
|
|
@@ -79,7 +79,7 @@ Use the attached destination-prefixed tools directly in chat. Do not launch `sap
|
|
|
79
79
|
|
|
80
80
|
**Included skills:** `abap-development` · `abap-testing-quality` · `cds-development` · `rap-development` · `abap-debugging` · `abap-runtime-analysis` · `rap-service-delivery` · `sap-standard-api-analysis` · `clean-core-extensibility` · `sap-sdlc-orchestration` · `sap-transport-release` · `hana-cloud-inspection` · `hana-cloud-native-development` · `hana-cloud-validation`
|
|
81
81
|
|
|
82
|
-
Agents and skills install for the harnesses you pick during global installation: a multi-select offers GitHub Copilot and Claude Code pre-checked, plus OpenAI Codex, Cursor, Gemini CLI, and
|
|
82
|
+
Agents and skills install for the harnesses you pick during global installation: a multi-select offers GitHub Copilot and Claude Code pre-checked, plus OpenAI Codex, Cursor, Gemini CLI, opencode, and Pi Coding Agent. In non-interactive installs they are installed for GitHub Copilot only. Repository-scoped installation instructions appear below.
|
|
83
83
|
|
|
84
84
|
## ✨ Your SAP development cockpit, inside your AI assistant
|
|
85
85
|
|
|
@@ -236,13 +236,13 @@ Once the destination server is enabled in your chat's tools picker, ask for outc
|
|
|
236
236
|
| 🔒 **SAP authorization remains authoritative** | The add-on does not bypass backend SAP permissions. |
|
|
237
237
|
| 🚚 **Curated VSP tool surface** | The proxy exposes the same curated developer-lifecycle set for every destination, plus local workflow tools; SAP authorizations and VSP safety checks still apply. |
|
|
238
238
|
| 🧱 **Per-destination isolation** | Generated MCP entries are scoped to a single `SAP_AI_DEV_TOOLKIT_DESTINATION`. |
|
|
239
|
-
| 🔑 **
|
|
239
|
+
| 🔑 **Credential-aware setup** | BAS authentication material stays in Destination configuration. Local SAP GUI setup writes VS Code input prompts for SAP user/password instead of raw credentials. |
|
|
240
240
|
|
|
241
241
|
<a id="install"></a>
|
|
242
242
|
|
|
243
243
|
## 📦 Installation
|
|
244
244
|
|
|
245
|
-
Install globally from a
|
|
245
|
+
Install globally from SAP Business Application Studio or from a local workstation with SAP GUI installed:
|
|
246
246
|
|
|
247
247
|
```sh
|
|
248
248
|
npm install --global sap-ai-dev-toolkit
|
|
@@ -270,18 +270,20 @@ The installer handles VSP provisioning automatically:
|
|
|
270
270
|
- Uses the package's bundled patched VSP binary for the current platform, verified against the npm-published `dist/checksums.txt`. A checksum-anchored remote download is the fallback only when the package has no bundled asset.
|
|
271
271
|
- No Go toolchain is downloaded or required at install time; Go is only used by the repository's own `build:vsp` development script.
|
|
272
272
|
|
|
273
|
-
With `H2O_URL` set,
|
|
273
|
+
Setup detects where it is running. With `H2O_URL` set, it treats the environment as SAP Business Application Studio and opens a checkbox picker for BAS destinations, with no destinations selected by default. Use **Space** to choose destinations and **Enter** to confirm. Press **a** to toggle all destinations (select all if any are unchecked; otherwise clear the selection). Confirming with none selected removes this add-on's managed MCP entries. When the `cf` CLI 8.18 or newer is authenticated to a targeted space, setup first offers an optional import from that space's Destination service; type **y** then **Enter** to include it, or press **Enter** to skip. Accepted CF and BAS destinations appear together in the picker.
|
|
274
274
|
|
|
275
|
+
Without `H2O_URL`, interactive setup looks for local SAP GUI system configuration on macOS and Windows (for example SAP GUI landscape XML and `saplogon.ini`). SAP GUI landscape files describe SAP GUI/DIAG connectivity, not the ADT HTTP(S) endpoint, so setup asks you to confirm the ADT URL for each selected system. The generated MCP entry uses VS Code input prompts for the SAP user and password at server start; raw SAP credentials are not stored in `mcp.json`. On Windows, setup can instead write an experimental `SAP_AUTH_MODE=windows-sso` entry without username/password prompts; this requires the ADT endpoint to support HTTP Negotiate/SPNEGO. Runtime uses the bundled Windows Negotiate helper by default, or a trusted custom helper configured with `SAP_AI_DEV_TOOLKIT_NEGOTIATE_HELPER`. npm may run its install hook without an interactive terminal, even when the shell is interactive; in that case, destination selection is skipped without changing MCP config.
|
|
275
276
|
|
|
276
|
-
|
|
277
|
+
|
|
278
|
+
The same postinstall offers the bundled agents and skills for several AI coding harnesses in a checkbox picker. **GitHub Copilot** and **Claude Code** start pre-checked, so pressing **Enter** alone installs both; **Space** selects or deselects a harness, **a** toggles all seven, and confirming with none selected skips the step without changing files. When destination MCP setup succeeds, the generated managed MCP servers are also mirrored into selected harnesses that have a stable user-level JSON MCP configuration (Claude Code, Cursor, Gemini CLI, and Pi Coding Agent). GitHub Copilot continues to use the BAS/VS Code MCP configuration written during destination setup. With no interactive terminal, the assets are installed for GitHub Copilot only; set `SAP_AI_DEV_TOOLKIT_HARNESSES` to a comma-separated list of harness ids (`github-copilot`, `claude-code`, `codex`, `cursor`, `gemini-cli`, `opencode`, `pi-coding-agent`) to choose non-interactively.
|
|
277
279
|
|
|
278
280
|
Some current npm versions also require install hooks to be approved. If npm reports that `sap-ai-dev-toolkit`'s `postinstall` was blocked, allow it during a fresh install with `npm install --global --allow-scripts=sap-ai-dev-toolkit sap-ai-dev-toolkit`, or run `sap-ai-dev --setup` from an interactive BAS terminal after installation.
|
|
279
281
|
|
|
280
282
|
The setup report uses icons and terminal colors; set `NO_COLOR=1` to disable ANSI colors. The table is a weather report, not a bouncer: green **PASS** means the ADT probe responded, red **FAIL** means it failed, and yellow **SKIPPED** means it was skipped. Probe failures do not block MCP registration or startup for destinations you select.
|
|
281
283
|
|
|
282
|
-
Run `sap-ai-dev --setup` later to change the destination selection or remove generated destination entries. Run `sap-ai-dev --setup --tools` to also choose optional companion tools. Use `--npx` to make generated
|
|
284
|
+
Run `sap-ai-dev --setup` later to change the destination selection or remove generated destination entries. Run `sap-ai-dev --setup --tools` to also choose optional companion tools. Use `--npx` to make generated destination entries start the pinned package through npm instead of relying on a global `sap-ai-dev` command; companion tools already use `npx` with their own npm packages.
|
|
283
285
|
|
|
284
|
-
If a setup step is skipped or fails, rerun it from an interactive
|
|
286
|
+
If a setup step is skipped or fails, rerun it from an interactive terminal:
|
|
285
287
|
|
|
286
288
|
```sh
|
|
287
289
|
sap-ai-dev --setup
|
|
@@ -293,7 +295,7 @@ To configure without a global install, run the guided setup directly through npm
|
|
|
293
295
|
npx --yes --ignore-scripts --package=sap-ai-dev-toolkit sap-ai-dev --setup --npx
|
|
294
296
|
```
|
|
295
297
|
|
|
296
|
-
This lists discovered systems in the same checkbox picker, with nothing selected by default. Use **Space** to choose destinations, **Enter** to confirm, and **a** to toggle all (select all if any are unchecked; otherwise clear the selection). It writes MCP entries that launch the selected servers through npx. The entries pin the package version used during setup, and `--ignore-scripts` avoids running the install-time wizard a second time. After setup, in BAS run **MCP: List Servers**, select each chosen destination, and choose **Start Server**.
|
|
298
|
+
This lists discovered systems in the same checkbox picker, with nothing selected by default. Use **Space** to choose destinations, **Enter** to confirm, and **a** to toggle all (select all if any are unchecked; otherwise clear the selection). It writes MCP entries that launch the selected servers through npx. The entries pin the package version used during setup, and `--ignore-scripts` avoids running the install-time wizard a second time. After setup, in BAS or VS Code run **MCP: List Servers**, select each chosen destination, and choose **Start Server**.
|
|
297
299
|
|
|
298
300
|
For a non-interactive installation or a platform without a published VSP asset, provide a trusted binary override:
|
|
299
301
|
|
|
@@ -348,16 +350,19 @@ The package includes five user-invocable custom agents (**SAP Solution Architect
|
|
|
348
350
|
|
|
349
351
|
After destination setup, the installer prints a 🤖 notice and a checkbox picker asking which AI coding harnesses should receive the bundled agents and skills. **Space** selects or deselects, **a** toggles all, **Enter** confirms. **GitHub Copilot** and **Claude Code** are pre-checked, so pressing **Enter** without changes installs both; confirming with none selected skips the step and leaves your files unchanged.
|
|
350
352
|
|
|
351
|
-
| Harness | Skills land in | Agents land in |
|
|
352
|
-
| --- | --- | --- |
|
|
353
|
-
| GitHub Copilot | `$HOME/.copilot/skills/` | `$HOME/.copilot/agents/*.agent.md` |
|
|
354
|
-
| Claude Code | `$HOME/.claude/skills/` | `$HOME/.claude/agents/<id>.md` (frontmatter reduced to `name` and `description`) |
|
|
355
|
-
| OpenAI Codex | `$HOME/.agents/skills/` | — |
|
|
356
|
-
| Cursor | `$HOME/.cursor/skills/` | — |
|
|
357
|
-
| Gemini CLI | `$HOME/.gemini/skills/` | — |
|
|
358
|
-
| opencode | `$XDG_CONFIG_HOME/opencode/skills/` (default `$HOME/.config/opencode/skills/`) | `$XDG_CONFIG_HOME/opencode/agents/<id>.md` (as subagents) |
|
|
353
|
+
| Harness | Skills land in | Agents land in | MCP auto-wiring |
|
|
354
|
+
| --- | --- | --- | --- |
|
|
355
|
+
| GitHub Copilot | `$HOME/.copilot/skills/` | `$HOME/.copilot/agents/*.agent.md` | Uses the BAS/VS Code `mcp.json` written by destination setup |
|
|
356
|
+
| Claude Code | `$HOME/.claude/skills/` | `$HOME/.claude/agents/<id>.md` (frontmatter reduced to `name` and `description`) | `$HOME/.claude.json` |
|
|
357
|
+
| OpenAI Codex | `$HOME/.agents/skills/` | — | Not changed automatically |
|
|
358
|
+
| Cursor | `$HOME/.cursor/skills/` | — | `$HOME/.cursor/mcp.json` |
|
|
359
|
+
| Gemini CLI | `$HOME/.gemini/skills/` | — | `$HOME/.gemini/settings.json` |
|
|
360
|
+
| opencode | `$XDG_CONFIG_HOME/opencode/skills/` (default `$HOME/.config/opencode/skills/`) | `$XDG_CONFIG_HOME/opencode/agents/<id>.md` (as subagents) | Not changed automatically |
|
|
361
|
+
| Pi Coding Agent | `$PI_CODING_AGENT_DIR/skills/` (default `$HOME/.pi/agent/skills/`) | `$PI_CODING_AGENT_DIR/prompts/<id>.md` (as `/` prompt commands) | `$PI_CODING_AGENT_DIR/mcp.json` (default `$HOME/.pi/agent/mcp.json`) |
|
|
362
|
+
|
|
363
|
+
All 14 skills use the portable `SKILL.md` format and are installed verbatim. The 5 agents use Copilot's `.agent.md` format, so they are installed as native custom agents for harnesses with a custom-agent mechanism (GitHub Copilot, Claude Code, opencode), with frontmatter adapted where needed. Pi receives equivalent prompt templates (`/abap-developer`, `/sap-solution-architect`, and so on) because Pi exposes reusable personas through prompts rather than a separate custom-agent file type. Selecting several harnesses duplicates the small markdown payloads on disk.
|
|
359
364
|
|
|
360
|
-
|
|
365
|
+
MCP auto-wiring preserves unrelated servers in each harness config and replaces only entries previously managed by `sap-ai-dev-toolkit`. Generated entries carry `SAP_AI_DEV_TOOLKIT_MANAGED=true` in their server environment so future reinstalls can reconcile them without touching user-owned MCP servers.
|
|
361
366
|
|
|
362
367
|
These user-level customizations are available across workspaces opened by the same BAS user in the same dev space. The harness must be available in BAS and may need a window reload to discover new files. A non-interactive install skips the picker and installs for GitHub Copilot only; override with `SAP_AI_DEV_TOOLKIT_HARNESSES` (comma-separated harness ids). `npm install --ignore-scripts` skips the postinstall wizard entirely.
|
|
363
368
|
|
|
@@ -744,6 +749,8 @@ The response contains a `tools` array. A `RunQuery` entry resembles this excerpt
|
|
|
744
749
|
| `SAP_AI_DEV_TOOLKIT_DESTINATION` | Comma-separated destination allowlist for normal runtime discovery. Setup clears this temporarily so it can display all eligible systems. |
|
|
745
750
|
| `SAP_AI_DEV_TOOLKIT_MODE` | VSP child mode (`expert` by default; `focused` omits `ActivateMultiple`, `GetUserTransports`, and `GetTransportInfo`). The proxy exposes its curated tool subset and local workflow tools, including `LintABAP`. |
|
|
746
751
|
| `SAP_ALLOW_TRANSPORTABLE_EDITS` | Generated MCP entries set this to `true` to permit source edits in transportable packages; VSP safety checks and SAP authorizations still apply. |
|
|
752
|
+
| `SAP_AUTH_MODE=windows-sso` | Local SAP GUI entry mode for experimental Windows Integrated Authentication. Runtime starts a loopback ADT proxy and does not pass SAP username/password credentials to VSP. |
|
|
753
|
+
| `SAP_AI_DEV_TOOLKIT_NEGOTIATE_HELPER` / `SAP_AI_DEV_TOOLKIT_SSPI_HELPER` | Optional trusted Windows helper executable used by the local Windows SSO proxy to convert an HTTP `WWW-Authenticate: Negotiate` challenge into a Kerberos/SPNEGO token. NTLM fallback is rejected because it is connection-oriented and unsafe for this proxy shape. When unset on Windows, the bundled PowerShell SSPI helper is used. A custom helper receives one JSON line on stdin (`url`, `challenge`, `mechanism`) and prints the base64 token on stdout. |
|
|
747
754
|
| `SAP_AI_DEV_TOOLKIT_HTTP_PROXY` | Egress proxy for BAS discovery and probe traffic. Takes precedence over `HTTP_PROXY`/`http_proxy`; unset means the default BAS proxy for `.dest` hosts, empty means direct. |
|
|
748
755
|
| `SAP_AI_DEV_TOOLKIT_REQUEST_TIMEOUT_MS` | Per-request timeout for calls forwarded to a VSP child (default 600000 = 10 minutes; `0` disables). A stalled request fails with a timeout error; the child is left running. |
|
|
749
756
|
| `SAP_AI_DEV_MCP_CONFIG` | Explicit MCP configuration path; highest precedence. |
|
package/package.json
CHANGED
package/scripts/postinstall.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { installBinary } from '../src/binary.mjs';
|
|
6
6
|
import { runSetup } from '../src/setup.mjs';
|
|
7
7
|
import { installUserAssetsForHarnesses } from '../src/install-user-assets.mjs';
|
|
8
|
+
import { installMcpServersForHarnesses } from '../src/harness-mcp-config.mjs';
|
|
8
9
|
import { DEFAULT_HARNESS_IDS, HARNESSES, HARNESS_ENV_VAR, NON_INTERACTIVE_HARNESS_IDS, envHarnessSelection, harnessRoot } from '../src/harnesses.mjs';
|
|
9
10
|
import { ReadStream as TTYReadStream, WriteStream as TTYWriteStream } from 'node:tty';
|
|
10
11
|
import { homedir } from 'node:os';
|
|
@@ -87,7 +88,9 @@ async function installSelectedHarnessAssets(ids) {
|
|
|
87
88
|
}
|
|
88
89
|
for (const assets of results) {
|
|
89
90
|
const installed = assets.installed + assets.updated;
|
|
90
|
-
|
|
91
|
+
const removed = assets.removed || 0;
|
|
92
|
+
const cleanup = removed > 0 ? `; ${removed} duplicate or legacy file${removed === 1 ? '' : 's'} removed` : '';
|
|
93
|
+
await announce(`${assets.label}: ${installed} file${installed === 1 ? '' : 's'} installed or updated in ${assets.root}; ${assets.unchanged} already current${cleanup}.`, 'success');
|
|
91
94
|
if (assets.conflicts.length > 0) {
|
|
92
95
|
const paths = assets.conflicts.map(path => join(assets.root, path)).join(', ');
|
|
93
96
|
await announce(`${assets.label}: existing customizations were preserved; review these paths: ${paths}`, 'warning');
|
|
@@ -102,13 +105,13 @@ async function installSelectedHarnessAssets(ids) {
|
|
|
102
105
|
async function runHarnessAssetInstall() {
|
|
103
106
|
const home = process.env.HOME || homedir();
|
|
104
107
|
const override = await resolveHarnessOverride();
|
|
105
|
-
|
|
108
|
+
return withInstallTerminal(async terminal => {
|
|
106
109
|
let selected = override;
|
|
107
110
|
if (!terminal) {
|
|
108
111
|
if (!selected) selected = NON_INTERACTIVE_HARNESS_IDS;
|
|
109
112
|
await announce(`Installing bundled agents and skills for ${harnessSelectionLabels(selected)} by default because no interactive terminal is available.`, 'progress');
|
|
110
113
|
await installSelectedHarnessAssets(selected);
|
|
111
|
-
return;
|
|
114
|
+
return selected;
|
|
112
115
|
}
|
|
113
116
|
if (!selected) selected = DEFAULT_HARNESS_IDS;
|
|
114
117
|
await announce([
|
|
@@ -135,18 +138,40 @@ async function runHarnessAssetInstall() {
|
|
|
135
138
|
} catch (error) {
|
|
136
139
|
if (error.message === 'Prompt interrupted') {
|
|
137
140
|
await announce('Bundled agents and skills were skipped because the prompt was interrupted. Your files were not changed.', 'info');
|
|
138
|
-
return;
|
|
141
|
+
return [];
|
|
139
142
|
}
|
|
140
143
|
throw error;
|
|
141
144
|
}
|
|
142
145
|
if (answer.length === 0) {
|
|
143
146
|
await announce('Bundled agents and skills were skipped. Your files were not changed.', 'info');
|
|
144
|
-
return;
|
|
147
|
+
return [];
|
|
145
148
|
}
|
|
146
149
|
await installSelectedHarnessAssets(answer);
|
|
150
|
+
return answer;
|
|
147
151
|
});
|
|
148
152
|
}
|
|
149
153
|
|
|
154
|
+
async function wireHarnessMcpServers(ids, servers) {
|
|
155
|
+
const entries = servers && Object.keys(servers).length ? servers : null;
|
|
156
|
+
if (!entries || !ids?.length) return;
|
|
157
|
+
const hasLocalSapGui = Object.values(entries).some(entry => entry?.env?.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE === 'sap-gui-local');
|
|
158
|
+
if (hasLocalSapGui) {
|
|
159
|
+
await announce('Local SAP GUI MCP entries use VS Code input prompts for SAP login, so automatic wiring into non-VS Code harness MCP files was skipped. Configure those harnesses manually if they support secure runtime prompts.', 'info');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const { results, failures } = await installMcpServersForHarnesses(ids, entries, { env: runtimeEnv });
|
|
163
|
+
for (const result of results) {
|
|
164
|
+
if (!result.supported) {
|
|
165
|
+
await announce(`${result.label}: MCP auto-wiring skipped. ${result.skipped}`, 'info');
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
await announce(`${result.label}: wired ${result.servers} MCP server${result.servers === 1 ? '' : 's'} in ${result.path}.`, 'success');
|
|
169
|
+
}
|
|
170
|
+
for (const failure of failures) {
|
|
171
|
+
await announce(`${failure.label}: MCP auto-wiring failed: ${failure.error.message}`, 'error');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
150
175
|
|
|
151
176
|
function probeCell(destination) {
|
|
152
177
|
const probe = destination.probe || {};
|
|
@@ -172,7 +197,7 @@ export function destinationTable(destinations, registeredNames) {
|
|
|
172
197
|
const registered = registeredNames.has(generatedServerName(destination.serverName || destination.name));
|
|
173
198
|
const source = destination.source === 'cloud-foundry'
|
|
174
199
|
? `CF ${destination.cf?.destinationInstanceName || 'unknown instance'}`
|
|
175
|
-
: 'BAS';
|
|
200
|
+
: (destination.source === 'sap-gui-local' ? 'SAP GUI' : 'BAS');
|
|
176
201
|
return [
|
|
177
202
|
{ text: destination.name, output: destination.name },
|
|
178
203
|
{ text: source, output: source },
|
|
@@ -224,9 +249,10 @@ async function announceSetup(result) {
|
|
|
224
249
|
const registeredNames = new Set(servers.map(([name]) => name));
|
|
225
250
|
const entryDetails = servers.flatMap(([name, entry]) => {
|
|
226
251
|
const destination = destinationsByServer.get(name);
|
|
227
|
-
const
|
|
252
|
+
const configuredSource = brandedEnvValue(entry.env, 'DESTINATION_SOURCE') || destination?.source;
|
|
253
|
+
const source = configuredSource === 'cloud-foundry'
|
|
228
254
|
? 'Cloud Foundry'
|
|
229
|
-
: 'BAS';
|
|
255
|
+
: (configuredSource === 'sap-gui-local' ? 'Local SAP GUI' : 'BAS');
|
|
230
256
|
const destinationName = brandedEnvValue(entry.env, 'DESTINATION') || destination?.name || 'unknown';
|
|
231
257
|
const client = destination?.client || '001';
|
|
232
258
|
const authentication = destination?.authentication || 'unknown';
|
|
@@ -291,11 +317,17 @@ async function main() {
|
|
|
291
317
|
await announce('Rerun sap-ai-dev --setup.', 'info');
|
|
292
318
|
}
|
|
293
319
|
|
|
320
|
+
let harnessIds = [];
|
|
294
321
|
try {
|
|
295
|
-
await runHarnessAssetInstall();
|
|
322
|
+
harnessIds = await runHarnessAssetInstall();
|
|
296
323
|
} catch (error) {
|
|
297
324
|
await announce(`Bundled agent and skill installation failed: ${error.message}`, 'error');
|
|
298
325
|
}
|
|
326
|
+
try {
|
|
327
|
+
if (setupCompleted) await wireHarnessMcpServers(harnessIds, setupResult?.servers);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
await announce(`Harness MCP auto-wiring failed: ${error.message}`, 'error');
|
|
330
|
+
}
|
|
299
331
|
if (setupCompleted) await announceSetup(setupResult);
|
|
300
332
|
}
|
|
301
333
|
if (process.argv[1] && realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve(process.argv[1]))) {
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Windows Integrated Authentication helper for sap-ai-dev-toolkit.
|
|
2
|
+
# Reads one JSON object from stdin with: url, challenge, mechanism.
|
|
3
|
+
# Writes only the base64 Negotiate token to stdout.
|
|
4
|
+
# Errors are written to stderr and exit non-zero.
|
|
5
|
+
|
|
6
|
+
$ErrorActionPreference = 'Stop'
|
|
7
|
+
|
|
8
|
+
$sspiSource = @'
|
|
9
|
+
using System;
|
|
10
|
+
using System.ComponentModel;
|
|
11
|
+
using System.Runtime.InteropServices;
|
|
12
|
+
|
|
13
|
+
public static class SapAiDevSspiNegotiate
|
|
14
|
+
{
|
|
15
|
+
const int SECPKG_CRED_OUTBOUND = 2;
|
|
16
|
+
const int SECBUFFER_VERSION = 0;
|
|
17
|
+
const int SECBUFFER_TOKEN = 2;
|
|
18
|
+
const int SECURITY_NATIVE_DREP = 0x10;
|
|
19
|
+
const int SEC_E_OK = 0;
|
|
20
|
+
const int SEC_I_CONTINUE_NEEDED = 0x00090312;
|
|
21
|
+
|
|
22
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
23
|
+
struct SecHandle
|
|
24
|
+
{
|
|
25
|
+
public IntPtr dwLower;
|
|
26
|
+
public IntPtr dwUpper;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
30
|
+
struct SecBuffer
|
|
31
|
+
{
|
|
32
|
+
public int cbBuffer;
|
|
33
|
+
public int BufferType;
|
|
34
|
+
public IntPtr pvBuffer;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
38
|
+
struct SecBufferDesc
|
|
39
|
+
{
|
|
40
|
+
public int ulVersion;
|
|
41
|
+
public int cBuffers;
|
|
42
|
+
public IntPtr pBuffers;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
46
|
+
struct SECURITY_INTEGER
|
|
47
|
+
{
|
|
48
|
+
public uint LowPart;
|
|
49
|
+
public int HighPart;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[DllImport("secur32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
|
|
53
|
+
static extern int AcquireCredentialsHandle(
|
|
54
|
+
string pszPrincipal,
|
|
55
|
+
string pszPackage,
|
|
56
|
+
int fCredentialUse,
|
|
57
|
+
IntPtr pvLogonID,
|
|
58
|
+
IntPtr pAuthData,
|
|
59
|
+
IntPtr pGetKeyFn,
|
|
60
|
+
IntPtr pvGetKeyArgument,
|
|
61
|
+
out SecHandle phCredential,
|
|
62
|
+
out SECURITY_INTEGER ptsExpiry);
|
|
63
|
+
|
|
64
|
+
[DllImport("secur32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
|
|
65
|
+
static extern int InitializeSecurityContext(
|
|
66
|
+
ref SecHandle phCredential,
|
|
67
|
+
IntPtr phContext,
|
|
68
|
+
string pszTargetName,
|
|
69
|
+
int fContextReq,
|
|
70
|
+
int Reserved1,
|
|
71
|
+
int TargetDataRep,
|
|
72
|
+
IntPtr pInput,
|
|
73
|
+
int Reserved2,
|
|
74
|
+
out SecHandle phNewContext,
|
|
75
|
+
IntPtr pOutput,
|
|
76
|
+
out int pfContextAttr,
|
|
77
|
+
out SECURITY_INTEGER ptsExpiry);
|
|
78
|
+
|
|
79
|
+
[DllImport("secur32.dll", SetLastError = false)]
|
|
80
|
+
static extern int FreeCredentialsHandle(ref SecHandle phCredential);
|
|
81
|
+
|
|
82
|
+
[DllImport("secur32.dll", SetLastError = false)]
|
|
83
|
+
static extern int DeleteSecurityContext(ref SecHandle phContext);
|
|
84
|
+
|
|
85
|
+
static void Check(int status, string operation)
|
|
86
|
+
{
|
|
87
|
+
if (status == SEC_E_OK || status == SEC_I_CONTINUE_NEEDED) return;
|
|
88
|
+
throw new Win32Exception(status, operation + " failed with SSPI status 0x" + status.ToString("X8"));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
static IntPtr BufferDesc(byte[] token, out IntPtr tokenMemory, out IntPtr bufferMemory)
|
|
92
|
+
{
|
|
93
|
+
tokenMemory = IntPtr.Zero;
|
|
94
|
+
SecBuffer buffer = new SecBuffer();
|
|
95
|
+
buffer.BufferType = SECBUFFER_TOKEN;
|
|
96
|
+
if (token != null && token.Length > 0)
|
|
97
|
+
{
|
|
98
|
+
tokenMemory = Marshal.AllocHGlobal(token.Length);
|
|
99
|
+
Marshal.Copy(token, 0, tokenMemory, token.Length);
|
|
100
|
+
buffer.cbBuffer = token.Length;
|
|
101
|
+
buffer.pvBuffer = tokenMemory;
|
|
102
|
+
}
|
|
103
|
+
bufferMemory = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(SecBuffer)));
|
|
104
|
+
Marshal.StructureToPtr(buffer, bufferMemory, false);
|
|
105
|
+
SecBufferDesc desc = new SecBufferDesc();
|
|
106
|
+
desc.ulVersion = SECBUFFER_VERSION;
|
|
107
|
+
desc.cBuffers = 1;
|
|
108
|
+
desc.pBuffers = bufferMemory;
|
|
109
|
+
IntPtr descMemory = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(SecBufferDesc)));
|
|
110
|
+
Marshal.StructureToPtr(desc, descMemory, false);
|
|
111
|
+
return descMemory;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
static void FreeDesc(IntPtr descMemory, IntPtr tokenMemory, IntPtr bufferMemory)
|
|
115
|
+
{
|
|
116
|
+
if (descMemory != IntPtr.Zero) Marshal.FreeHGlobal(descMemory);
|
|
117
|
+
if (bufferMemory != IntPtr.Zero) Marshal.FreeHGlobal(bufferMemory);
|
|
118
|
+
if (tokenMemory != IntPtr.Zero) Marshal.FreeHGlobal(tokenMemory);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public static string CreateToken(string targetName, string incomingBase64)
|
|
122
|
+
{
|
|
123
|
+
SecHandle credential;
|
|
124
|
+
SECURITY_INTEGER expiry;
|
|
125
|
+
int status = AcquireCredentialsHandle(null, "Negotiate", SECPKG_CRED_OUTBOUND, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, out credential, out expiry);
|
|
126
|
+
Check(status, "AcquireCredentialsHandle");
|
|
127
|
+
|
|
128
|
+
SecHandle context = new SecHandle();
|
|
129
|
+
IntPtr inputDesc = IntPtr.Zero, inputToken = IntPtr.Zero, inputBuffer = IntPtr.Zero;
|
|
130
|
+
IntPtr outputDesc = IntPtr.Zero, outputToken = IntPtr.Zero, outputBuffer = IntPtr.Zero;
|
|
131
|
+
try
|
|
132
|
+
{
|
|
133
|
+
byte[] incoming = null;
|
|
134
|
+
if (!String.IsNullOrWhiteSpace(incomingBase64)) incoming = Convert.FromBase64String(incomingBase64);
|
|
135
|
+
if (incoming != null && incoming.Length > 0) inputDesc = BufferDesc(incoming, out inputToken, out inputBuffer);
|
|
136
|
+
|
|
137
|
+
outputDesc = BufferDesc(new byte[12288], out outputToken, out outputBuffer);
|
|
138
|
+
int attrs;
|
|
139
|
+
status = InitializeSecurityContext(ref credential, IntPtr.Zero, targetName, 0, 0, SECURITY_NATIVE_DREP, inputDesc, 0, out context, outputDesc, out attrs, out expiry);
|
|
140
|
+
Check(status, "InitializeSecurityContext");
|
|
141
|
+
|
|
142
|
+
SecBuffer outBuffer = (SecBuffer) Marshal.PtrToStructure(outputBuffer, typeof(SecBuffer));
|
|
143
|
+
if (outBuffer.cbBuffer <= 0 || outBuffer.pvBuffer == IntPtr.Zero) throw new Exception("SSPI returned an empty Negotiate token.");
|
|
144
|
+
byte[] bytes = new byte[outBuffer.cbBuffer];
|
|
145
|
+
Marshal.Copy(outBuffer.pvBuffer, bytes, 0, bytes.Length);
|
|
146
|
+
return Convert.ToBase64String(bytes);
|
|
147
|
+
}
|
|
148
|
+
finally
|
|
149
|
+
{
|
|
150
|
+
FreeDesc(inputDesc, inputToken, inputBuffer);
|
|
151
|
+
FreeDesc(outputDesc, outputToken, outputBuffer);
|
|
152
|
+
DeleteSecurityContext(ref context);
|
|
153
|
+
FreeCredentialsHandle(ref credential);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
'@
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
$raw = [Console]::In.ReadToEnd()
|
|
161
|
+
if ([string]::IsNullOrWhiteSpace($raw)) { throw 'Missing helper request on stdin.' }
|
|
162
|
+
$request = $raw | ConvertFrom-Json
|
|
163
|
+
if (-not $request.url) { throw 'Missing target URL.' }
|
|
164
|
+
|
|
165
|
+
$uri = [Uri] [string] $request.url
|
|
166
|
+
if ($uri.Scheme -ne 'http' -and $uri.Scheme -ne 'https') { throw 'Target URL must use HTTP or HTTPS.' }
|
|
167
|
+
if ($uri.UserInfo) { throw 'Target URL must not contain user information.' }
|
|
168
|
+
|
|
169
|
+
Add-Type -TypeDefinition $sspiSource -Language CSharp
|
|
170
|
+
|
|
171
|
+
$incoming = $null
|
|
172
|
+
$challenge = [string] $request.challenge
|
|
173
|
+
if ($challenge -match '(?i)Negotiate\s+([^,\s]+)') { $incoming = $Matches[1] }
|
|
174
|
+
$targetName = 'HTTP/' + $uri.DnsSafeHost
|
|
175
|
+
$token = [SapAiDevSspiNegotiate]::CreateToken($targetName, $incoming)
|
|
176
|
+
if ([string]::IsNullOrWhiteSpace($token)) { throw 'Negotiate token generation returned an empty token.' }
|
|
177
|
+
[Console]::Out.WriteLine($token.Trim())
|
|
178
|
+
exit 0
|
|
179
|
+
} catch {
|
|
180
|
+
[Console]::Error.WriteLine($_.Exception.Message)
|
|
181
|
+
exit 1
|
|
182
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { basename, dirname, join } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { harnessById, harnessRoot } from './harnesses.mjs';
|
|
6
|
+
|
|
7
|
+
const MANAGED_ENV = 'SAP_AI_DEV_TOOLKIT_MANAGED';
|
|
8
|
+
|
|
9
|
+
function mcpServersPath(harness, { home = process.env.HOME || homedir(), env = process.env } = {}) {
|
|
10
|
+
switch (harness.id) {
|
|
11
|
+
case 'claude-code':
|
|
12
|
+
return join(home, '.claude.json');
|
|
13
|
+
case 'cursor':
|
|
14
|
+
return join(harnessRoot(harness, { home, env }), 'mcp.json');
|
|
15
|
+
case 'gemini-cli':
|
|
16
|
+
return join(harnessRoot(harness, { home, env }), 'settings.json');
|
|
17
|
+
case 'pi-coding-agent':
|
|
18
|
+
return join(harnessRoot(harness, { home, env }), 'mcp.json');
|
|
19
|
+
default:
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function readJsonConfig(path) {
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = await readFile(path, 'utf8');
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (error.code === 'ENOENT') return {};
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('top-level value is not an object');
|
|
35
|
+
return parsed;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw new Error(`${path} contains invalid JSON: ${error.message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function writeJsonConfig(path, config) {
|
|
42
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o755 });
|
|
43
|
+
const temporary = join(dirname(path), `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
|
|
44
|
+
try {
|
|
45
|
+
await writeFile(temporary, `${JSON.stringify(config, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
46
|
+
await rename(temporary, path);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isManagedServer(entry) {
|
|
54
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
55
|
+
const env = entry.env;
|
|
56
|
+
return env && typeof env === 'object' && (
|
|
57
|
+
env[MANAGED_ENV] === 'true'
|
|
58
|
+
|| typeof env.SAP_AI_DEV_TOOLKIT_DESTINATION === 'string'
|
|
59
|
+
|| env.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE === 'cloud-foundry'
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toMcpServersEntry(entry) {
|
|
64
|
+
const converted = {};
|
|
65
|
+
if (entry?.type === 'stdio') converted.type = 'stdio';
|
|
66
|
+
if (typeof entry?.command === 'string') converted.command = entry.command;
|
|
67
|
+
if (Array.isArray(entry?.args)) converted.args = [...entry.args];
|
|
68
|
+
converted.env = entry?.env && typeof entry.env === 'object' && !Array.isArray(entry.env)
|
|
69
|
+
? { ...entry.env, [MANAGED_ENV]: 'true' }
|
|
70
|
+
: { [MANAGED_ENV]: 'true' };
|
|
71
|
+
if (typeof entry?.cwd === 'string') converted.cwd = entry.cwd;
|
|
72
|
+
if (typeof entry?.description === 'string') converted.description = entry.description;
|
|
73
|
+
if (typeof entry?.displayName === 'string' && !converted.description) converted.description = entry.displayName;
|
|
74
|
+
return converted;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function harnessMcpConfigPath(id, options = {}) {
|
|
78
|
+
return mcpServersPath(harnessById(id), options);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function installMcpServersForHarness({ harness, servers, home, env = process.env } = {}) {
|
|
82
|
+
const path = mcpServersPath(harness, { home, env });
|
|
83
|
+
if (!path) {
|
|
84
|
+
const skipped = harness.id === 'github-copilot'
|
|
85
|
+
? 'GitHub Copilot uses the BAS/VS Code MCP configuration already written during destination setup.'
|
|
86
|
+
: 'No stable user-level MCP JSON config is known for this harness.';
|
|
87
|
+
return { harness: harness.id, label: harness.label, supported: false, skipped };
|
|
88
|
+
}
|
|
89
|
+
const config = await readJsonConfig(path);
|
|
90
|
+
const existing = config.mcpServers;
|
|
91
|
+
if (existing !== undefined && (!existing || typeof existing !== 'object' || Array.isArray(existing))) {
|
|
92
|
+
throw new Error(`${path} has a non-object mcpServers value`);
|
|
93
|
+
}
|
|
94
|
+
const mcpServers = { ...(existing || {}) };
|
|
95
|
+
for (const [name, entry] of Object.entries(mcpServers)) {
|
|
96
|
+
if (isManagedServer(entry)) delete mcpServers[name];
|
|
97
|
+
}
|
|
98
|
+
for (const [name, entry] of Object.entries(servers || {})) {
|
|
99
|
+
if (Object.hasOwn(mcpServers, name)) {
|
|
100
|
+
throw new Error(`MCP server "${name}" already exists and is not managed by sap-ai-dev-toolkit`);
|
|
101
|
+
}
|
|
102
|
+
mcpServers[name] = toMcpServersEntry(entry);
|
|
103
|
+
}
|
|
104
|
+
config.mcpServers = mcpServers;
|
|
105
|
+
await writeJsonConfig(path, config);
|
|
106
|
+
return { harness: harness.id, label: harness.label, supported: true, path, servers: Object.keys(servers || {}).length };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function installMcpServersForHarnesses(ids, servers, { home, env = process.env } = {}) {
|
|
110
|
+
const results = [];
|
|
111
|
+
const failures = [];
|
|
112
|
+
for (const id of ids) {
|
|
113
|
+
let harness;
|
|
114
|
+
try {
|
|
115
|
+
harness = harnessById(id);
|
|
116
|
+
results.push(await installMcpServersForHarness({ harness, servers, home, env }));
|
|
117
|
+
} catch (error) {
|
|
118
|
+
failures.push({ id, label: harness?.label || id, error });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { results, failures };
|
|
122
|
+
}
|
package/src/harnesses.mjs
CHANGED
|
@@ -31,6 +31,11 @@ export function agentMarkdownName(fileName) {
|
|
|
31
31
|
return String(fileName).replace(/\.agent\.md$/, '.md');
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export function piCodingAgentRoot({ home = process.env.HOME || homedir(), env = process.env } = {}) {
|
|
35
|
+
const override = typeof env?.PI_CODING_AGENT_DIR === 'string' ? env.PI_CODING_AGENT_DIR.trim() : '';
|
|
36
|
+
return override || join(home, '.pi', 'agent');
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
export function claudeCodeAgentDocument(source) {
|
|
35
40
|
const frontmatter = splitFrontmatter(source);
|
|
36
41
|
if (!frontmatter) throw new Error('unsupported agent frontmatter');
|
|
@@ -51,13 +56,35 @@ export function opencodeAgentDocument(source) {
|
|
|
51
56
|
return renderDocument([{ key: 'description', value: description }, { key: 'mode', value: 'subagent' }], frontmatter.body);
|
|
52
57
|
}
|
|
53
58
|
|
|
59
|
+
export function piCodingAgentPromptDocument(source) {
|
|
60
|
+
const frontmatter = splitFrontmatter(source);
|
|
61
|
+
if (!frontmatter) throw new Error('unsupported agent frontmatter');
|
|
62
|
+
const name = requiredField(frontmatter.fields, 'name');
|
|
63
|
+
const description = requiredField(frontmatter.fields, 'description');
|
|
64
|
+
if (!name || !description) throw new Error('unsupported agent frontmatter');
|
|
65
|
+
const body = frontmatter.body
|
|
66
|
+
.replaceAll('from the Chat tools picker', 'from Pi\'s MCP tool context')
|
|
67
|
+
.replaceAll('the Chat tools picker', 'Pi\'s MCP tool context')
|
|
68
|
+
.replaceAll('current workspace and SAP MCP server', 'current workspace and configured SAP MCP server');
|
|
69
|
+
return renderDocument([
|
|
70
|
+
{ key: 'description', value: `Use the ${name} SAP agent persona. ${description}` },
|
|
71
|
+
{ key: 'argument-hint', value: '"[request]"' }
|
|
72
|
+
], `Act as **${name}** for the following request. Follow these agent instructions and use relevant installed SAP skills when helpful.
|
|
73
|
+
|
|
74
|
+
${body.trim()}
|
|
75
|
+
|
|
76
|
+
User request: $ARGUMENTS
|
|
77
|
+
`);
|
|
78
|
+
}
|
|
79
|
+
|
|
54
80
|
export const HARNESSES = [
|
|
55
81
|
{ id: 'github-copilot', label: 'GitHub Copilot', directory: '.copilot', supportsAgents: true, legacyManifest: true },
|
|
56
82
|
{ id: 'claude-code', label: 'Claude Code', directory: '.claude', supportsAgents: true, agentFileName: agentMarkdownName, agentDocument: claudeCodeAgentDocument },
|
|
57
83
|
{ id: 'codex', label: 'OpenAI Codex', directory: '.agents', supportsAgents: false },
|
|
58
84
|
{ id: 'cursor', label: 'Cursor', directory: '.cursor', supportsAgents: false },
|
|
59
85
|
{ id: 'gemini-cli', label: 'Gemini CLI', directory: '.gemini', supportsAgents: false },
|
|
60
|
-
{ id: 'opencode', label: 'opencode', directory: 'opencode', configHome: true, supportsAgents: true, agentFileName: agentMarkdownName, agentDocument: opencodeAgentDocument }
|
|
86
|
+
{ id: 'opencode', label: 'opencode', directory: 'opencode', configHome: true, supportsAgents: true, agentFileName: agentMarkdownName, agentDocument: opencodeAgentDocument },
|
|
87
|
+
{ id: 'pi-coding-agent', label: 'Pi Coding Agent', directory: '.pi/agent', supportsAgents: true, agentTargetPrefix: 'prompts', agentFileName: agentMarkdownName, agentDocument: piCodingAgentPromptDocument, root: piCodingAgentRoot }
|
|
61
88
|
];
|
|
62
89
|
|
|
63
90
|
export function harnessById(id) {
|
|
@@ -69,6 +96,7 @@ export function harnessById(id) {
|
|
|
69
96
|
}
|
|
70
97
|
|
|
71
98
|
export function harnessRoot(harness, { home = process.env.HOME || homedir(), env = process.env } = {}) {
|
|
99
|
+
if (harness.root) return harness.root({ home, env });
|
|
72
100
|
if (harness.configHome) {
|
|
73
101
|
const override = typeof env?.XDG_CONFIG_HOME === 'string' ? env.XDG_CONFIG_HOME.trim() : '';
|
|
74
102
|
return join(override || join(home, '.config'), harness.directory);
|