@merchantduo/code 0.2.0-beta.5 → 0.3.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/app/status.d.ts +1 -0
- package/dist/app/status.js +3 -1
- package/dist/app/system-prompt.js +2 -0
- package/dist/integrations/pi/context.js +2 -1
- package/dist/integrations/pi/workspace.d.ts +16 -0
- package/dist/integrations/pi/workspace.js +42 -4
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -6,17 +6,17 @@ MerchantDuo is a Pi coding agent for Magento 2.4 teams. It starts from the store
|
|
|
6
6
|
|
|
7
7
|
- Works with local checkouts, Warden, and SSH environments.
|
|
8
8
|
- Detects Magento edition, version, PHP version, deployment mode, cache types, and registered theme inheritance.
|
|
9
|
-
- Ships Magento 2.4, Luma, Hyva, Mage2Gen, and
|
|
9
|
+
- Ships Magento 2.4, Luma, Hyva, Mage2Gen, testing, and persistent-goal skills.
|
|
10
10
|
- Provides guided local/Warden store provisioning.
|
|
11
11
|
- Keeps remote writes and Magento operations behind explicit execution boundaries.
|
|
12
12
|
|
|
13
|
-
At session start, MerchantDuo produces a Magento snapshot from the selected environment. Frontend and admin URLs are discovered but remain unverified until an explicit test. The compact status line shows
|
|
13
|
+
At session start, MerchantDuo produces a Magento snapshot from the selected environment. Frontend and admin URLs are discovered but remain unverified until an explicit test. The compact status line shows the Magento version, environment lifecycle, session permission mode, an explicit theme scope when one is selected, and the frontend URL when available.
|
|
14
14
|
|
|
15
15
|
## Commands and tools
|
|
16
16
|
|
|
17
17
|
Extension slash commands are namespaced with `duo-` to avoid collisions with other Pi extensions.
|
|
18
18
|
|
|
19
|
-
- [Command reference](docs/commands.md) covers session theme and permission switching, lifecycle, and
|
|
19
|
+
- [Command reference](docs/commands.md) covers session theme and permission switching, lifecycle, navigation, and bundled persistent-goal commands.
|
|
20
20
|
- [Tool reference](docs/tools.md) covers workspace, environment, Magento workflow, magerun2, testing, PHP-console, and optional knowledge tools, including parameters and confirmation boundaries.
|
|
21
21
|
|
|
22
22
|
## Environments
|
|
@@ -73,7 +73,7 @@ The interactive flow discovers available source, database, and service evidence,
|
|
|
73
73
|
|
|
74
74
|
## Install and start
|
|
75
75
|
|
|
76
|
-
MerchantDuo requires Node.js 22.19 or later, Bash, and Python 3. HTTP/browser testing additionally needs curl and Chrome or Chromium
|
|
76
|
+
MerchantDuo requires Node.js 22.19 or later, Bash, and Python 3. HTTP/browser testing additionally needs curl and Chrome or Chromium. The bundled `fetch_content` internet-research tool runs over host HTTP and has no browser or Python-runner prerequisite. Warden and SSH environments need Warden and OpenSSH respectively.
|
|
77
77
|
|
|
78
78
|
```sh
|
|
79
79
|
pnpm install --frozen-lockfile
|
package/dist/app/status.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { SessionState } from "#app/runtime";
|
|
3
|
+
export declare function setInitializingStatus(ctx: ExtensionContext): void;
|
|
3
4
|
export declare function refreshStatus(ctx: ExtensionContext, state: SessionState): void;
|
package/dist/app/status.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
export function setInitializingStatus(ctx) {
|
|
2
|
+
ctx.ui.setStatus("merchantduo", "Initializing Magento context…");
|
|
3
|
+
}
|
|
1
4
|
export function refreshStatus(ctx, state) {
|
|
2
5
|
const parts = [
|
|
3
|
-
`MerchantDuo ${process.env.MERCHANTDUO_PACKAGE_VERSION ?? "?"}`,
|
|
4
6
|
`M${state.magento.version ?? "?"}`,
|
|
5
7
|
`${state.selected.name} (${state.environmentStatus})`,
|
|
6
8
|
state.permissionMode === "read-only" ? "r-o" : state.permissionMode,
|
|
@@ -5,6 +5,8 @@ export function mage2genRoot() {
|
|
|
5
5
|
export function merchantDuoSystemPrompt(mage2genPath = mage2genRoot()) {
|
|
6
6
|
return `You are MerchantDuo, a senior Magento 2.4 architect and implementation partner. Work directly in the selected MerchantDuo environment and theme scope. Before changing code, inspect the relevant module, theme inheritance chain, configuration, and established local conventions. Make focused, production-quality changes; do not preserve obsolete compatibility paths or add speculative abstractions.
|
|
7
7
|
|
|
8
|
+
Ground every design and implementation decision in existing source before introducing a pattern. Find the closest applicable example and read its surrounding call sites, configuration, and tests. Search Magento core under vendor/magento first; if it has no suitable example, search other installed vendor modules or project app/code modules. Adapt the applicable pattern rather than copying blindly, and explain any material intentional difference. Only when no relevant precedent exists may you introduce a new pattern, and state that finding before doing so.
|
|
9
|
+
|
|
8
10
|
Use Magento service contracts, dependency injection, declarative schema/data patches, layout XML, and theme fallback mechanisms when they are the correct native extension points. Respect the active theme scope: all means cover every detected theme, any means identify the relevant theme from the request and code before theme-specific edits, and a concrete theme limits work to that theme and its inheritance chain. Do not use Luma RequireJS/Knockout conventions for Hyva work.
|
|
9
11
|
|
|
10
12
|
For every request to create a new Magento module, call the \`mage2gen_generate_module\` tool to create the initial module scaffolding before writing custom files. The tool uses Mage2Gen at ${mage2genPath} and requires Python 3; it generates only into the selected Magento project, then inspect its output and adapt it to the task. Do not hand-create standard module boilerplate when Mage2Gen can create it. Skip Mage2Gen only when it cannot represent the requested module, and state the concrete reason before proceeding manually. Do not use it for a small targeted edit to an existing module.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { refreshStatus } from "#app/status";
|
|
1
|
+
import { refreshStatus, setInitializingStatus } from "#app/status";
|
|
2
2
|
import { merchantDuoSystemPrompt } from "#app/system-prompt";
|
|
3
3
|
import { discoverTesting } from "#testing/index";
|
|
4
4
|
import { discoverMagerun2 } from "#features/magerun2/index";
|
|
@@ -7,6 +7,7 @@ import { runtime, sessionEvents } from "#integrations/pi/session";
|
|
|
7
7
|
export default function context(pi) {
|
|
8
8
|
pi.on("session_start", async (_event, ctx) => {
|
|
9
9
|
runtime.resetInjection();
|
|
10
|
+
setInitializingStatus(ctx);
|
|
10
11
|
const state = await runtime.boot(ctx.cwd);
|
|
11
12
|
if (state.environmentStatus === "stopped") {
|
|
12
13
|
refreshStatus(ctx, state);
|
|
@@ -2,4 +2,20 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type { EnvironmentBackend } from "#environments/backend";
|
|
3
3
|
/** Replaces Pi's native workspace tools with selected-environment operations. */
|
|
4
4
|
export default function workspace(pi: ExtensionAPI): void;
|
|
5
|
+
type SearchParameters = {
|
|
6
|
+
pattern: string;
|
|
7
|
+
glob?: string;
|
|
8
|
+
ignoreCase?: boolean;
|
|
9
|
+
literal?: boolean;
|
|
10
|
+
context?: number;
|
|
11
|
+
limit?: number;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Search the selected backend without making its runtime image depend on ripgrep:
|
|
15
|
+
* use rg when present, otherwise enumerate regular files and pass them to grep.
|
|
16
|
+
*/
|
|
17
|
+
export declare function searchCommand(params: SearchParameters, path: string): string;
|
|
5
18
|
export declare function workspacePath(backend: EnvironmentBackend, cwd: string, candidate: string): string;
|
|
19
|
+
/** Return only a declared MerchantDuo skill asset; project files remain environment-routed. */
|
|
20
|
+
export declare function packagedSkillPath(candidate: string): string | undefined;
|
|
21
|
+
export {};
|
|
@@ -1,16 +1,34 @@
|
|
|
1
|
+
import { access as hostAccess, readFile as hostReadFile } from "node:fs/promises";
|
|
1
2
|
import { relative, resolve } from "node:path";
|
|
2
3
|
import { Type } from "typebox";
|
|
3
4
|
import { createEditToolDefinition, createReadToolDefinition, createWriteToolDefinition, } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import { runtime } from "#integrations/pi/session";
|
|
6
|
+
import { packagePath } from "#shared/package-paths";
|
|
5
7
|
const shell = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
6
8
|
const result = (value) => ({ content: [{ type: "text", text: value }], details: {} });
|
|
9
|
+
const packageSkillRoots = [
|
|
10
|
+
packagePath("skills"),
|
|
11
|
+
packagePath("vendor", "hyva-ai-tools", "skills"),
|
|
12
|
+
];
|
|
7
13
|
/** Replaces Pi's native workspace tools with selected-environment operations. */
|
|
8
14
|
export default function workspace(pi) {
|
|
9
15
|
registerFileTools(pi);
|
|
10
16
|
pi.registerTool({ name: "bash", label: "bash", description: "Run a shell command in the selected MerchantDuo environment.", parameters: Type.Object({ command: Type.String(), timeout: Type.Optional(Type.Number()) }), async execute(_id, params, signal, _update, ctx) { const state = await runtime.boot(ctx.cwd); const output = await state.backend.run(["sh", "-lc", params.command], { signal, timeoutMs: params.timeout }); return result(`${output.stdout}${output.stderr}`); } });
|
|
11
17
|
pi.registerTool({ name: "ls", label: "ls", description: "List a selected-environment directory.", parameters: Type.Object({ path: Type.Optional(Type.String()), limit: Type.Optional(Type.Number()) }), async execute(_id, params, signal, _update, ctx) { const state = await runtime.boot(ctx.cwd); const path = workspacePath(state.backend, ctx.cwd, params.path ?? "."); const output = await state.backend.run(["sh", "-lc", `find ${shell(path)} -mindepth 1 -maxdepth 1 -printf '%f\\n' | head -n ${Math.max(1, Math.min(params.limit ?? 200, 1000))}`], { signal }); return result(output.stdout || output.stderr); } });
|
|
12
18
|
pi.registerTool({ name: "find", label: "find", description: "Find files in the selected MerchantDuo environment.", parameters: Type.Object({ pattern: Type.String(), path: Type.Optional(Type.String()), limit: Type.Optional(Type.Number()) }), async execute(_id, params, signal, _update, ctx) { const state = await runtime.boot(ctx.cwd); const path = workspacePath(state.backend, ctx.cwd, params.path ?? "."); const limit = Math.max(1, Math.min(params.limit ?? 1000, 5000)); const output = await state.backend.run(["sh", "-lc", `find ${shell(path)} -path '*/node_modules/*' -prune -o -path '*/.git/*' -prune -o -name ${shell(params.pattern)} -print | head -n ${limit}`], { signal }); return result(output.stdout || output.stderr); } });
|
|
13
|
-
pi.registerTool({ name: "grep", label: "grep", description: "Search text
|
|
19
|
+
pi.registerTool({ name: "grep", label: "grep", description: "Search text with ripgrep or a backend-local grep fallback in the selected MerchantDuo environment.", parameters: Type.Object({ pattern: Type.String(), path: Type.Optional(Type.String()), glob: Type.Optional(Type.String()), ignoreCase: Type.Optional(Type.Boolean()), literal: Type.Optional(Type.Boolean()), context: Type.Optional(Type.Number()), limit: Type.Optional(Type.Number()) }), async execute(_id, params, signal, _update, ctx) { const state = await runtime.boot(ctx.cwd); const path = workspacePath(state.backend, ctx.cwd, params.path ?? "."); const output = await state.backend.run(["sh", "-lc", searchCommand(params, path)], { signal }); return result(output.stdout || output.stderr); } });
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Search the selected backend without making its runtime image depend on ripgrep:
|
|
23
|
+
* use rg when present, otherwise enumerate regular files and pass them to grep.
|
|
24
|
+
*/
|
|
25
|
+
export function searchCommand(params, path) {
|
|
26
|
+
const limit = Math.max(1, Math.min(params.limit ?? 100, 1000));
|
|
27
|
+
const context = params.context ? Math.min(params.context, 20) : undefined;
|
|
28
|
+
const rgFlags = ["--line-number", "--color=never", `--max-count=${limit}`, params.ignoreCase ? "--ignore-case" : "", params.literal ? "--fixed-strings" : "", context ? `--context=${context}` : "", params.glob ? `--glob ${shell(params.glob)}` : ""].filter(Boolean).join(" ");
|
|
29
|
+
const grepFlags = ["-n", params.ignoreCase ? "-i" : "", params.literal ? "-F" : "", context ? `-C ${context}` : ""].filter(Boolean).join(" ");
|
|
30
|
+
const glob = params.glob ? `-name ${shell(params.glob)} ` : "";
|
|
31
|
+
return `if command -v rg >/dev/null 2>&1; then rg ${rgFlags} -- ${shell(params.pattern)} ${shell(path)}; else find ${shell(path)} \\( -path '*/node_modules/*' -o -path '*/.git/*' \\) -prune -o -type f ${glob}-exec grep ${grepFlags} -- ${shell(params.pattern)} {} + 2>/dev/null | head -n ${limit}; fi`;
|
|
14
32
|
}
|
|
15
33
|
function registerFileTools(pi) {
|
|
16
34
|
const localRead = createReadToolDefinition(process.cwd());
|
|
@@ -21,13 +39,33 @@ function registerFileTools(pi) {
|
|
|
21
39
|
pi.registerTool({ ...localEdit, async execute(id, params, signal, update, ctx) { const state = await runtime.boot(ctx.cwd); return createEditToolDefinition(ctx.cwd, { operations: fileOps(state.backend, ctx.cwd) }).execute(id, params, signal, update, ctx); } });
|
|
22
40
|
}
|
|
23
41
|
function fileOps(backend, cwd) {
|
|
24
|
-
const readFile = async (path) => {
|
|
25
|
-
|
|
42
|
+
const readFile = async (path) => {
|
|
43
|
+
const skillPath = packagedSkillPath(path);
|
|
44
|
+
if (skillPath)
|
|
45
|
+
return hostReadFile(skillPath);
|
|
46
|
+
const target = workspacePath(backend, cwd, path);
|
|
47
|
+
const response = await backend.run(["sh", "-lc", `base64 < ${shell(target)}`]);
|
|
48
|
+
if (response.exitCode)
|
|
49
|
+
throw new Error(response.stderr || "Cannot read file");
|
|
50
|
+
return Buffer.from(response.stdout.replace(/\s/g, ""), "base64");
|
|
51
|
+
};
|
|
26
52
|
const writeFile = async (path, content) => { const target = workspacePath(backend, cwd, path); const response = await backend.run(["sh", "-lc", `base64 -d > ${shell(target)}`], { input: Buffer.from(content).toString("base64") }); if (response.exitCode)
|
|
27
53
|
throw new Error(response.stderr || "Cannot write file"); };
|
|
28
|
-
return { readFile, writeFile, access: async (path) => { const
|
|
54
|
+
return { readFile, writeFile, access: async (path) => { const skillPath = packagedSkillPath(path); if (skillPath) {
|
|
55
|
+
await hostAccess(skillPath);
|
|
56
|
+
return;
|
|
57
|
+
} const response = await backend.run(["sh", "-lc", `test -r ${shell(workspacePath(backend, cwd, path))}`]); if (response.exitCode)
|
|
29
58
|
throw new Error("Path is inaccessible"); }, mkdir: async (path) => { const response = await backend.run(["mkdir", "-p", workspacePath(backend, cwd, path)]); if (response.exitCode)
|
|
30
59
|
throw new Error(response.stderr || "Cannot create directory"); } };
|
|
31
60
|
}
|
|
32
61
|
export function workspacePath(backend, cwd, candidate) { const full = resolve(cwd, candidate); const value = relative(resolve(cwd), full); if (value === ".." || value.startsWith("../"))
|
|
33
62
|
throw new Error(`Path escapes workspace: ${candidate}`); return backend.path(value); }
|
|
63
|
+
/** Return only a declared MerchantDuo skill asset; project files remain environment-routed. */
|
|
64
|
+
export function packagedSkillPath(candidate) {
|
|
65
|
+
const full = resolve(candidate);
|
|
66
|
+
return packageSkillRoots.some((root) => contains(root, full)) ? full : undefined;
|
|
67
|
+
}
|
|
68
|
+
function contains(root, candidate) {
|
|
69
|
+
const value = relative(root, candidate);
|
|
70
|
+
return value === "" || (value !== ".." && !value.startsWith("../"));
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@merchantduo/code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-beta.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Magento-native Pi-based Coding Agent",
|
|
6
6
|
"type": "module",
|
|
@@ -48,7 +48,9 @@
|
|
|
48
48
|
"./dist/integrations/pi/mage2gen.js",
|
|
49
49
|
"./dist/integrations/pi/workflows.js",
|
|
50
50
|
"./dist/integrations/pi/magerun2.js",
|
|
51
|
-
"./dist/integrations/pi/testing.js"
|
|
51
|
+
"./dist/integrations/pi/testing.js",
|
|
52
|
+
"./node_modules/pi-web-access",
|
|
53
|
+
"./node_modules/pi-goal"
|
|
52
54
|
],
|
|
53
55
|
"skills": [
|
|
54
56
|
"./skills",
|
|
@@ -58,6 +60,8 @@
|
|
|
58
60
|
"dependencies": {
|
|
59
61
|
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
60
62
|
"@modelcontextprotocol/client": "2.0.0",
|
|
63
|
+
"pi-goal": "0.1.7",
|
|
64
|
+
"pi-web-access": "0.22.0",
|
|
61
65
|
"typebox": "1.3.7",
|
|
62
66
|
"yaml": "^2.8.1",
|
|
63
67
|
"zod": "^3.24.4"
|