sap-ai-dev-toolkit 0.7.4 → 0.7.8

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 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 opencode">
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">
@@ -57,7 +57,7 @@ Then connect a destination in SAP Business Application Studio:
57
57
  1. Open the Command Palette.
58
58
  2. Run **MCP: List Servers**.
59
59
  3. Start the server named after your selected BAS 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): **SAP Solution Architect**, **ABAP Developer**, **ABAP Runtime Debugger**, **RAP Service Developer**, or **HANA Cloud/HDI Specialist**.
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
61
  5. In the Chat tools picker, enable the server for that BAS destination.
62
62
  6. Ask the agent to inspect, build, test, or verify something in your SAP landscape.
63
63
 
@@ -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 opencode. In non-interactive installs they are installed for GitHub Copilot only. Repository-scoped installation instructions appear below.
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
 
@@ -273,7 +273,7 @@ The installer handles VSP provisioning automatically:
273
273
  With `H2O_URL` set, an interactive install opens a checkbox picker 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. npm may run its install hook without an interactive terminal, even when the shell is interactive; in that case, selection is skipped without changing MCP config.
274
274
 
275
275
 
276
- 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 six, and confirming with none selected skips the step without changing files. 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`) to choose non-interactively.
276
+ 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
277
 
278
278
  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
279
 
@@ -348,16 +348,19 @@ The package includes five user-invocable custom agents (**SAP Solution Architect
348
348
 
349
349
  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
350
 
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) |
359
-
360
- 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 only installed for harnesses with a custom-agent mechanism (GitHub Copilot, Claude Code, opencode), with frontmatter adapted where needed. Selecting several harnesses duplicates the small markdown payloads on disk.
351
+ | Harness | Skills land in | Agents land in | MCP auto-wiring |
352
+ | --- | --- | --- | --- |
353
+ | GitHub Copilot | `$HOME/.copilot/skills/` | `$HOME/.copilot/agents/*.agent.md` | Uses the BAS/VS Code `mcp.json` written by destination setup |
354
+ | Claude Code | `$HOME/.claude/skills/` | `$HOME/.claude/agents/<id>.md` (frontmatter reduced to `name` and `description`) | `$HOME/.claude.json` |
355
+ | OpenAI Codex | `$HOME/.agents/skills/` | — | Not changed automatically |
356
+ | Cursor | `$HOME/.cursor/skills/` | — | `$HOME/.cursor/mcp.json` |
357
+ | Gemini CLI | `$HOME/.gemini/skills/` | — | `$HOME/.gemini/settings.json` |
358
+ | opencode | `$XDG_CONFIG_HOME/opencode/skills/` (default `$HOME/.config/opencode/skills/`) | `$XDG_CONFIG_HOME/opencode/agents/<id>.md` (as subagents) | Not changed automatically |
359
+ | 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`) |
360
+
361
+ 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.
362
+
363
+ 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
364
 
362
365
  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
366
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-ai-dev-toolkit",
3
- "version": "0.7.4",
3
+ "version": "0.7.8",
4
4
  "description": "SAP AI development toolkit for BAS, ABAP, RAP, CAP, HANA Cloud, Fiori, UI5, and MCP",
5
5
  "author": "Gurkan Yilmaz",
6
6
  "type": "module",
@@ -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';
@@ -102,13 +103,13 @@ async function installSelectedHarnessAssets(ids) {
102
103
  async function runHarnessAssetInstall() {
103
104
  const home = process.env.HOME || homedir();
104
105
  const override = await resolveHarnessOverride();
105
- await withInstallTerminal(async terminal => {
106
+ return withInstallTerminal(async terminal => {
106
107
  let selected = override;
107
108
  if (!terminal) {
108
109
  if (!selected) selected = NON_INTERACTIVE_HARNESS_IDS;
109
110
  await announce(`Installing bundled agents and skills for ${harnessSelectionLabels(selected)} by default because no interactive terminal is available.`, 'progress');
110
111
  await installSelectedHarnessAssets(selected);
111
- return;
112
+ return selected;
112
113
  }
113
114
  if (!selected) selected = DEFAULT_HARNESS_IDS;
114
115
  await announce([
@@ -135,18 +136,35 @@ async function runHarnessAssetInstall() {
135
136
  } catch (error) {
136
137
  if (error.message === 'Prompt interrupted') {
137
138
  await announce('Bundled agents and skills were skipped because the prompt was interrupted. Your files were not changed.', 'info');
138
- return;
139
+ return [];
139
140
  }
140
141
  throw error;
141
142
  }
142
143
  if (answer.length === 0) {
143
144
  await announce('Bundled agents and skills were skipped. Your files were not changed.', 'info');
144
- return;
145
+ return [];
145
146
  }
146
147
  await installSelectedHarnessAssets(answer);
148
+ return answer;
147
149
  });
148
150
  }
149
151
 
152
+ async function wireHarnessMcpServers(ids, servers) {
153
+ const entries = servers && Object.keys(servers).length ? servers : null;
154
+ if (!entries || !ids?.length) return;
155
+ const { results, failures } = await installMcpServersForHarnesses(ids, entries, { env: runtimeEnv });
156
+ for (const result of results) {
157
+ if (!result.supported) {
158
+ await announce(`${result.label}: MCP auto-wiring skipped. ${result.skipped}`, 'info');
159
+ continue;
160
+ }
161
+ await announce(`${result.label}: wired ${result.servers} MCP server${result.servers === 1 ? '' : 's'} in ${result.path}.`, 'success');
162
+ }
163
+ for (const failure of failures) {
164
+ await announce(`${failure.label}: MCP auto-wiring failed: ${failure.error.message}`, 'error');
165
+ }
166
+ }
167
+
150
168
 
151
169
  function probeCell(destination) {
152
170
  const probe = destination.probe || {};
@@ -291,11 +309,17 @@ async function main() {
291
309
  await announce('Rerun sap-ai-dev --setup.', 'info');
292
310
  }
293
311
 
312
+ let harnessIds = [];
294
313
  try {
295
- await runHarnessAssetInstall();
314
+ harnessIds = await runHarnessAssetInstall();
296
315
  } catch (error) {
297
316
  await announce(`Bundled agent and skill installation failed: ${error.message}`, 'error');
298
317
  }
318
+ try {
319
+ if (setupCompleted) await wireHarnessMcpServers(harnessIds, setupResult?.servers);
320
+ } catch (error) {
321
+ await announce(`Harness MCP auto-wiring failed: ${error.message}`, 'error');
322
+ }
299
323
  if (setupCompleted) await announceSetup(setupResult);
300
324
  }
301
325
  if (process.argv[1] && realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve(process.argv[1]))) {
@@ -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);
@@ -75,7 +75,7 @@ async function planDestinations(harness, files) {
75
75
  if (!harness.supportsAgents) continue;
76
76
  const fileName = harness.agentFileName ? harness.agentFileName(basename(file.target)) : basename(file.target);
77
77
  const content = harness.agentDocument ? Buffer.from(harness.agentDocument(raw.toString('utf8')), 'utf8') : raw;
78
- destinations.push({ target: join('agents', fileName), content });
78
+ destinations.push({ target: join(harness.agentTargetPrefix || 'agents', fileName), content });
79
79
  }
80
80
  return destinations;
81
81
  }