@evref-bl/pharo-agent 0.10.2 → 0.12.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/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +8 -0
- package/README.md +5 -3
- package/package.json +3 -2
- package/plugin.json +1 -1
- package/skills/pharo-setup/SKILL.md +27 -4
- package/src/cli.js +57 -34
- package/src/gateway.js +15 -44
- package/src/help.js +23 -24
- package/src/images.js +55 -59
- package/src/init.js +18 -12
- package/src/live.js +146 -124
- package/src/mcpServer.js +154 -0
- package/src/project.js +37 -29
- package/src/runtime.js +27 -30
package/.mcp.json
ADDED
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ npx --yes @evref-bl/pharo-agent init --host <claude|codex|opencode>
|
|
|
14
14
|
|
|
15
15
|
That is all of it. `init` writes the project's Pharo guidance and copies the
|
|
16
16
|
workflow skills that guidance refers to. From there the agent registers the
|
|
17
|
-
image the project needs,
|
|
17
|
+
image the project needs, starts it and uses its tools by following what was
|
|
18
18
|
written; ask it for Pharo work as you would for anything else.
|
|
19
19
|
|
|
20
20
|
## Refreshing an existing project
|
|
@@ -35,8 +35,10 @@ the file ownership rules and [Images](docs/images.md) for image-side MCP updates
|
|
|
35
35
|
|
|
36
36
|
Node 22.12 or later.
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
[fuse-t](https://www.fuse-t.org/)
|
|
38
|
+
Images and their Pharo tools run anywhere Node does. The source projection is
|
|
39
|
+
the exception: it needs macOS with [fuse-t](https://www.fuse-t.org/), whose
|
|
40
|
+
daemon is downloaded for you. Everywhere else an image starts with its tools
|
|
41
|
+
and says that the projection is not available, rather than failing.
|
|
40
42
|
|
|
41
43
|
## What the agent gets
|
|
42
44
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evref-bl/pharo-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Terminal-first Pharo integration for coding agents.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"bin",
|
|
17
17
|
"src",
|
|
18
18
|
"skills",
|
|
19
|
+
".mcp.json",
|
|
19
20
|
"plugin.json",
|
|
20
21
|
".agents",
|
|
21
22
|
".claude-plugin",
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
35
|
"@evref-bl/pharo-mcp": "3.3.1",
|
|
35
|
-
"@evref-bl/plexus": "0.
|
|
36
|
+
"@evref-bl/plexus": "0.12.1",
|
|
36
37
|
"@modelcontextprotocol/sdk": "1.32.1"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
package/plugin.json
CHANGED
|
@@ -8,13 +8,35 @@ Run this only when the user asks to enable or refresh Pharo tooling in this proj
|
|
|
8
8
|
From the project root, run the released CLI that matches this plugin:
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
npx --yes @evref-bl/pharo-agent@0.
|
|
11
|
+
npx --yes @evref-bl/pharo-agent@0.12.0 init --host claude
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
Name the host this chat runs in: `--host claude` in Claude Code,
|
|
15
15
|
`--host codex` in Codex, `--host opencode` in OpenCode.
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
The command writes no host configuration. It adds a conditional instruction
|
|
17
|
+
block and this project's own files, so Pharo guidance stays out of unrelated
|
|
18
|
+
work.
|
|
19
|
+
|
|
20
|
+
To get the image's tools as MCP tools in Claude Code, enable this plugin for
|
|
21
|
+
this project rather than configuring a server by hand. Commit
|
|
22
|
+
`.claude/settings.json`:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"extraKnownMarketplaces": {
|
|
27
|
+
"evref-bl": {
|
|
28
|
+
"source": {
|
|
29
|
+
"source": "git",
|
|
30
|
+
"url": "https://github.com/Evref-BL/pharo-agent-plugin.git"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"enabledPlugins": { "pharo-agent@evref-bl": true }
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
A host reads that file when a session starts, so the tools appear in the next
|
|
39
|
+
one. Enabling it per project keeps every other project untouched.
|
|
18
40
|
|
|
19
41
|
After upgrading the plugin/CLI, re-run the setup command from the project root
|
|
20
42
|
with the same host to refresh the generated guide and copied skills/references.
|
|
@@ -24,7 +46,8 @@ existing settings. Image declarations, runtime state, and live image code are
|
|
|
24
46
|
preserved. This refresh does not install a newer package or upgrade image tools.
|
|
25
47
|
|
|
26
48
|
Setup initializes the current directory, creating `plexus.json` with
|
|
27
|
-
no image when it is absent. Register one before the first
|
|
49
|
+
no image when it is absent. Register one before the first
|
|
50
|
+
`pharo-agent image start`:
|
|
28
51
|
|
|
29
52
|
```sh
|
|
30
53
|
pharo-agent image templates
|
package/src/cli.js
CHANGED
|
@@ -2,15 +2,11 @@ import { hostChoices, hostList } from "./hosts.js";
|
|
|
2
2
|
import { usage } from "./help.js";
|
|
3
3
|
import { initialize } from "./init.js";
|
|
4
4
|
import { addImage, listImages, removeImage, useImage } from "./images.js";
|
|
5
|
-
import { callImageTool, listImageTools
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
closeAgentImage,
|
|
9
|
-
listImageTemplates,
|
|
10
|
-
openAgentImage,
|
|
11
|
-
} from "./live.js";
|
|
5
|
+
import { callImageTool, listImageTools } from "./gateway.js";
|
|
6
|
+
import { serveImagesMcp } from "./mcpServer.js";
|
|
7
|
+
import { listImageTemplates, startAgentImage, stopAgentImage } from "./live.js";
|
|
12
8
|
import { findProjectRoot } from "./project.js";
|
|
13
|
-
import {
|
|
9
|
+
import { readRunningImage } from "./runtime.js";
|
|
14
10
|
|
|
15
11
|
function parseInit(args) {
|
|
16
12
|
let host;
|
|
@@ -33,11 +29,18 @@ function parseInit(args) {
|
|
|
33
29
|
return { host, mountRoot };
|
|
34
30
|
}
|
|
35
31
|
|
|
32
|
+
/**
|
|
33
|
+
* The template is required, and the loop below refuses the call without one,
|
|
34
|
+
* which the dynamic key assignment hides from the typechecker.
|
|
35
|
+
*
|
|
36
|
+
* @returns {{ id: string, template: string, category?: string, profile?: string }}
|
|
37
|
+
*/
|
|
36
38
|
function parseImageAdd(args) {
|
|
37
39
|
const [id, ...rest] = args;
|
|
38
40
|
if (!id || id.startsWith("--")) {
|
|
39
41
|
throw new Error("image add requires an image id");
|
|
40
42
|
}
|
|
43
|
+
/** @type {{ id: string, template?: string, category?: string, profile?: string }} */
|
|
41
44
|
const parsed = { id };
|
|
42
45
|
const options = {
|
|
43
46
|
"--template": "template",
|
|
@@ -60,7 +63,22 @@ function parseImageAdd(args) {
|
|
|
60
63
|
if (!parsed.template) {
|
|
61
64
|
throw new Error("image add requires --template <name>");
|
|
62
65
|
}
|
|
63
|
-
return parsed;
|
|
66
|
+
return { ...parsed, template: parsed.template };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// `image start` and `image stop` name their subject, like `image use` and
|
|
70
|
+
// `image remove`; `--image` stays on the commands that scope a call.
|
|
71
|
+
function parseImageTarget(subcommand, args) {
|
|
72
|
+
const all = args.includes("--all");
|
|
73
|
+
const rest = args.filter((value) => value !== "--all");
|
|
74
|
+
const [id, ...extra] = rest;
|
|
75
|
+
if (extra.length > 0 || (id && id.startsWith("--"))) {
|
|
76
|
+
throw new Error(`image ${subcommand} takes one image id`);
|
|
77
|
+
}
|
|
78
|
+
if (all && subcommand !== "stop") {
|
|
79
|
+
throw new Error(`image ${subcommand} does not accept --all`);
|
|
80
|
+
}
|
|
81
|
+
return { id, all };
|
|
64
82
|
}
|
|
65
83
|
|
|
66
84
|
function takeImageOption(args) {
|
|
@@ -115,6 +133,16 @@ export async function main(args) {
|
|
|
115
133
|
console.log(JSON.stringify({ ok: true, ...result }, null, 2));
|
|
116
134
|
return;
|
|
117
135
|
}
|
|
136
|
+
// Above the project lookup on purpose: a host starts this in whatever
|
|
137
|
+
// directory it likes, which may hold no project at all, and a server that
|
|
138
|
+
// exits is a broken server in the host's list. It resolves a project per
|
|
139
|
+
// request instead, so the reason reaches whoever calls a tool.
|
|
140
|
+
if (command === "mcp") {
|
|
141
|
+
const { image: mcpImage, rest: mcpArgs } = takeImageOption(rest);
|
|
142
|
+
requireNoArguments(command, mcpArgs);
|
|
143
|
+
await serveImagesMcp(findProjectRoot, mcpImage);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
118
146
|
const projectRoot = findProjectRoot();
|
|
119
147
|
if (command === "image") {
|
|
120
148
|
const [subcommand, ...imageArgs] = rest;
|
|
@@ -136,6 +164,24 @@ export async function main(args) {
|
|
|
136
164
|
console.log(JSON.stringify({ ok: true, ...addImage(projectRoot, parseImageAdd(imageArgs)) }, null, 2));
|
|
137
165
|
return;
|
|
138
166
|
}
|
|
167
|
+
if (subcommand === "start") {
|
|
168
|
+
const { id } = parseImageTarget(subcommand, imageArgs);
|
|
169
|
+
console.log(JSON.stringify(
|
|
170
|
+
{ ok: true, ...(await startAgentImage(projectRoot, id)) },
|
|
171
|
+
null,
|
|
172
|
+
2,
|
|
173
|
+
));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (subcommand === "stop") {
|
|
177
|
+
const { id, all } = parseImageTarget(subcommand, imageArgs);
|
|
178
|
+
console.log(JSON.stringify(
|
|
179
|
+
{ ok: true, ...(await stopAgentImage(projectRoot, id, { all })) },
|
|
180
|
+
null,
|
|
181
|
+
2,
|
|
182
|
+
));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
139
185
|
if (subcommand === "use" || subcommand === "remove") {
|
|
140
186
|
const [id, ...extra] = imageArgs;
|
|
141
187
|
if (!id || extra.length > 0) {
|
|
@@ -148,35 +194,12 @@ export async function main(args) {
|
|
|
148
194
|
throw new Error(`Unsupported image command: ${subcommand ?? ""}\n${usage()}`);
|
|
149
195
|
}
|
|
150
196
|
const { image, rest: commandArgs } = takeImageOption(rest);
|
|
151
|
-
if (command === "open") {
|
|
152
|
-
requireNoArguments(command, commandArgs);
|
|
153
|
-
console.log(JSON.stringify({ ok: true, ...(await openAgentImage(projectRoot, image)) }, null, 2));
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
if (command === "close") {
|
|
157
|
-
requireNoArguments(command, commandArgs);
|
|
158
|
-
console.log(JSON.stringify({ ok: true, ...(await closeAgentImage(projectRoot, image)) }, null, 2));
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
if (command === "status") {
|
|
162
|
-
requireNoArguments(command, commandArgs);
|
|
163
|
-
if (image) {
|
|
164
|
-
throw new Error("status reports every open image and does not accept --image");
|
|
165
|
-
}
|
|
166
|
-
console.log(JSON.stringify({ ok: true, ...agentImageStatus(projectRoot) }, null, 2));
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
if (command === "mcp") {
|
|
170
|
-
requireNoArguments(command, commandArgs);
|
|
171
|
-
await serveImagesMcp(projectRoot, image);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
197
|
if (command === "pharo") {
|
|
175
198
|
const [toolName, argumentsJson, ...extra] = commandArgs;
|
|
176
199
|
if (toolName === "list") {
|
|
177
200
|
if (argumentsJson || extra.length > 0) throw new Error("pharo list does not accept arguments");
|
|
178
201
|
console.log(JSON.stringify(
|
|
179
|
-
await listImageTools(projectRoot,
|
|
202
|
+
await listImageTools(projectRoot, readRunningImage(projectRoot, image)),
|
|
180
203
|
null,
|
|
181
204
|
2,
|
|
182
205
|
));
|
|
@@ -187,7 +210,7 @@ export async function main(args) {
|
|
|
187
210
|
}
|
|
188
211
|
const result = await callImageTool(
|
|
189
212
|
projectRoot,
|
|
190
|
-
|
|
213
|
+
readRunningImage(projectRoot, image),
|
|
191
214
|
toolName,
|
|
192
215
|
parseToolArguments(argumentsJson),
|
|
193
216
|
);
|
package/src/gateway.js
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { PlexusGateway } from "@evref-bl/plexus/gateway";
|
|
3
|
-
import {
|
|
4
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { loadProjectState } from "@evref-bl/plexus";
|
|
5
4
|
import {
|
|
6
|
-
CallToolRequestSchema,
|
|
7
|
-
ListToolsRequestSchema,
|
|
8
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
-
import {
|
|
10
|
-
readJson,
|
|
11
|
-
readOpenImage,
|
|
12
5
|
selectRunningImage,
|
|
13
6
|
unappliedPharoMcpPin,
|
|
14
7
|
} from "./runtime.js";
|
|
@@ -20,15 +13,18 @@ export const { version } = createRequire(import.meta.url)("../package.json");
|
|
|
20
13
|
// A route source is the directory, not the route. PLexus re-reads the state
|
|
21
14
|
// file and the image's endpoint handoff every time it resolves one, so there
|
|
22
15
|
// is nothing to register here and nothing that can go stale between the call
|
|
23
|
-
// that
|
|
24
|
-
export function imageGateway(projectRoot,
|
|
25
|
-
const state =
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
// that started the image and this one.
|
|
17
|
+
export function imageGateway(projectRoot, running) {
|
|
18
|
+
const state = loadProjectState(running.statePath);
|
|
19
|
+
if (!state) {
|
|
20
|
+
throw new Error(`PLexus has no state at ${running.statePath}.`);
|
|
21
|
+
}
|
|
22
|
+
const { image, mcpServerId } = selectRunningImage(state, running.imageId);
|
|
23
|
+
if (image.id !== running.imageId || mcpServerId !== running.mcpServerId) {
|
|
24
|
+
throw new Error(`The PLexus image ${running.imageId} changed; run pharo-agent image start again.`);
|
|
29
25
|
}
|
|
30
26
|
const gateway = new PlexusGateway({
|
|
31
|
-
routeSources: [{ projectRoot, statePath:
|
|
27
|
+
routeSources: [{ projectRoot, statePath: running.statePath }],
|
|
32
28
|
pharoScope: {
|
|
33
29
|
projectId: state.projectId,
|
|
34
30
|
workspaceId: state.workspaceId,
|
|
@@ -49,8 +45,8 @@ export function toolFailureMessage(error, image) {
|
|
|
49
45
|
return unappliedPin ? `${error}\n${unappliedPin}` : error;
|
|
50
46
|
}
|
|
51
47
|
|
|
52
|
-
export async function callImageTool(projectRoot,
|
|
53
|
-
const { gateway, mcpServerId, image } = imageGateway(projectRoot,
|
|
48
|
+
export async function callImageTool(projectRoot, running, toolName, argumentsValue) {
|
|
49
|
+
const { gateway, mcpServerId, image } = imageGateway(projectRoot, running);
|
|
54
50
|
const result = await gateway.routeToImage({
|
|
55
51
|
mcpServerId,
|
|
56
52
|
toolName,
|
|
@@ -64,8 +60,8 @@ export async function callImageTool(projectRoot, open, toolName, argumentsValue)
|
|
|
64
60
|
);
|
|
65
61
|
}
|
|
66
62
|
|
|
67
|
-
export async function listImageTools(projectRoot,
|
|
68
|
-
const { gateway } = imageGateway(projectRoot,
|
|
63
|
+
export async function listImageTools(projectRoot, running) {
|
|
64
|
+
const { gateway } = imageGateway(projectRoot, running);
|
|
69
65
|
return gateway.refreshPharoTools();
|
|
70
66
|
}
|
|
71
67
|
|
|
@@ -89,28 +85,3 @@ export function toolResult(data) {
|
|
|
89
85
|
}
|
|
90
86
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
91
87
|
}
|
|
92
|
-
|
|
93
|
-
export async function serveImagesMcp(projectRoot, defaultImage) {
|
|
94
|
-
const defaultOpen = readOpenImage(projectRoot, defaultImage);
|
|
95
|
-
const tools = (await listImageTools(projectRoot, defaultOpen))
|
|
96
|
-
.map((tool) => withImageSelector(tool, defaultImage));
|
|
97
|
-
|
|
98
|
-
const server = new Server(
|
|
99
|
-
{ name: "pharo-agent", version },
|
|
100
|
-
{ capabilities: { tools: {} } },
|
|
101
|
-
);
|
|
102
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
103
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
104
|
-
// Listed tools are the common ones, not the whole catalogue, and the image
|
|
105
|
-
// answers for any tool it holds. Refusing a name this list does not carry
|
|
106
|
-
// would hide tools that tool_search just found, and the image's own error
|
|
107
|
-
// is the better answer for a name nothing holds.
|
|
108
|
-
const { image, ...argumentsValue } = request.params.arguments ?? {};
|
|
109
|
-
const open = readOpenImage(projectRoot, image);
|
|
110
|
-
return toolResult(
|
|
111
|
-
await callImageTool(projectRoot, open, request.params.name, argumentsValue),
|
|
112
|
-
);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
await server.connect(new StdioServerTransport());
|
|
116
|
-
}
|
package/src/help.js
CHANGED
|
@@ -10,7 +10,8 @@ const commands = {
|
|
|
10
10
|
},
|
|
11
11
|
"image list": {
|
|
12
12
|
usage: "image list",
|
|
13
|
-
description: "List the project's declared images
|
|
13
|
+
description: "List the project's declared images and the default one, with the mount and\n"
|
|
14
|
+
+ "projection port of each image this project has running.",
|
|
14
15
|
example: "image list",
|
|
15
16
|
},
|
|
16
17
|
"image templates": {
|
|
@@ -21,55 +22,53 @@ const commands = {
|
|
|
21
22
|
},
|
|
22
23
|
"image add": {
|
|
23
24
|
usage: "image add <id> --template <name> [--category <name>] [--profile <id>]",
|
|
24
|
-
description: "Declare an image with a required id and template name;
|
|
25
|
+
description: "Declare an image with a required id and template name; image start creates it later.\n"
|
|
25
26
|
+ "Use --category for an ambiguous template name and --profile to choose a launcher profile.\n"
|
|
26
27
|
+ "The first declared image becomes the default.",
|
|
27
28
|
example: "image add dev --template '<template name>'",
|
|
28
29
|
},
|
|
29
30
|
"image use": {
|
|
30
31
|
usage: "image use <id>",
|
|
31
|
-
description: "Make the required declared image the project's default. It may already be
|
|
32
|
+
description: "Make the required declared image the project's default. It may already be running.",
|
|
32
33
|
example: "image use dev",
|
|
33
34
|
},
|
|
34
35
|
"image remove": {
|
|
35
36
|
usage: "image remove <id>",
|
|
36
|
-
description: "Remove the required image declaration.
|
|
37
|
+
description: "Remove the required image declaration. Stop the image first. Its image files are kept.\n"
|
|
37
38
|
+ "Removing the default image clears the default selection.",
|
|
38
39
|
example: "image remove dev",
|
|
39
40
|
},
|
|
40
|
-
|
|
41
|
-
usage: "
|
|
42
|
-
description: "Create if needed and
|
|
43
|
-
+ "
|
|
44
|
-
+ "
|
|
45
|
-
|
|
41
|
+
"image start": {
|
|
42
|
+
usage: "image start [<id>]",
|
|
43
|
+
description: "Create if needed and start an image with its Pharo tools. Omit the id to use the\n"
|
|
44
|
+
+ "project's default image. Images already running stay running. A source projection is\n"
|
|
45
|
+
+ "mounted on macOS with fuse-t; elsewhere the image starts without one and says so.\n"
|
|
46
|
+
+ "Pharo versions outside the MCP support range start without tools or a projection.",
|
|
47
|
+
example: "image start dev",
|
|
46
48
|
},
|
|
47
|
-
|
|
48
|
-
usage: "
|
|
49
|
-
description: "
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
close: {
|
|
53
|
-
usage: "close [--image <id>]",
|
|
54
|
-
description: "Close the selected image and its source projection. Omitting --image closes all images\n"
|
|
55
|
-
+ "this project opened, rather than only the default image.",
|
|
56
|
-
example: "close --image dev",
|
|
49
|
+
"image stop": {
|
|
50
|
+
usage: "image stop [<id>] [--all]",
|
|
51
|
+
description: "Stop the named image and any source projection it has, or the project's default\n"
|
|
52
|
+
+ "image when no id is given. Use --all to stop every image this project started.",
|
|
53
|
+
example: "image stop dev",
|
|
57
54
|
},
|
|
58
55
|
mcp: {
|
|
59
56
|
usage: "mcp [--image <id>]",
|
|
60
|
-
description: "
|
|
61
|
-
+ "to use the project's default image.
|
|
57
|
+
description: "Serve this project's images as a command-scoped stdio MCP server. Omit\n"
|
|
58
|
+
+ "--image to use the project's default image. It starts whether or not an image\n"
|
|
59
|
+
+ "is running: image_start starts one and the image's own tools join the list. The\n"
|
|
60
|
+
+ "server exits with this command, and the plugin's .mcp.json starts it for a host.",
|
|
62
61
|
example: "mcp --image dev",
|
|
63
62
|
},
|
|
64
63
|
"pharo list": {
|
|
65
64
|
usage: "pharo list [--image <id>]",
|
|
66
|
-
description: "List
|
|
65
|
+
description: "List a running image's advertised tools. Omit --image to use the project's default image.\n"
|
|
67
66
|
+ "Use tool_search and tool_get to discover and inspect the rest of its catalogue.",
|
|
68
67
|
example: "pharo list",
|
|
69
68
|
},
|
|
70
69
|
pharo: {
|
|
71
70
|
usage: "pharo <tool> [arguments-json] [--image <id>]",
|
|
72
|
-
description: "Call a tool in
|
|
71
|
+
description: "Call a tool in a running image. Arguments must be a JSON object and default to {}.\n"
|
|
73
72
|
+ "Omit --image to use the project's default image. Inspect the tool's schema with tool_get\n"
|
|
74
73
|
+ "before calling it; a failed tool result makes the command exit unsuccessfully.",
|
|
75
74
|
example: 'pharo tool_get \'{"name":"class_search"}\'',
|
package/src/images.js
CHANGED
|
@@ -1,77 +1,78 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
readProjectConfig,
|
|
5
|
-
writeProjectConfig,
|
|
6
|
-
} from "./project.js";
|
|
7
|
-
import { readConfig, readOpenImages, writeConfig } from "./runtime.js";
|
|
1
|
+
import { declareProjectImage, removeProjectImage } from "@evref-bl/plexus";
|
|
2
|
+
import { configuredImages } from "./project.js";
|
|
3
|
+
import { defaultImageId, readConfig, readRunningImages, writeConfig } from "./runtime.js";
|
|
8
4
|
|
|
9
5
|
export const defaultLoadScript = "pharo/load-mcp.st";
|
|
10
6
|
|
|
11
|
-
function
|
|
12
|
-
if (
|
|
13
|
-
throw new Error(
|
|
7
|
+
function assertNotRunning(projectRoot, imageId, action) {
|
|
8
|
+
if (readRunningImages(projectRoot)[imageId]) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
`Image ${imageId} is running. Run pharo-agent image stop ${imageId} before ${action}.`,
|
|
11
|
+
);
|
|
14
12
|
}
|
|
15
13
|
}
|
|
16
14
|
|
|
17
|
-
function derivedImageName(projectRoot, imageId) {
|
|
18
|
-
const { name } = projectIdentity(projectRoot);
|
|
19
|
-
return `${name}-{workspaceId}-${imageId}`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
15
|
export function listImages(projectRoot) {
|
|
23
16
|
const configured = configuredImages(projectRoot);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
const running = readRunningImages(projectRoot);
|
|
18
|
+
let defaultImage;
|
|
19
|
+
try {
|
|
20
|
+
defaultImage = defaultImageId(projectRoot);
|
|
21
|
+
} catch {
|
|
22
|
+
// Several declared and none chosen. The list is how a caller finds that
|
|
23
|
+
// out, so it answers rather than raising the question back at them.
|
|
24
|
+
defaultImage = undefined;
|
|
25
|
+
}
|
|
27
26
|
return {
|
|
28
27
|
...(defaultImage ? { defaultImage } : {}),
|
|
29
|
-
images: configured.map((image) =>
|
|
30
|
-
id
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
images: configured.map((image) => {
|
|
29
|
+
// The record repeats the id it is keyed by; everything else in it is
|
|
30
|
+
// runtime detail this list carries when the image is running.
|
|
31
|
+
const { imageId: _keyed, ...runtime } = running[image.id] ?? {};
|
|
32
|
+
return {
|
|
33
|
+
id: image.id,
|
|
34
|
+
imageName: image.imageName,
|
|
35
|
+
isDefault: image.id === defaultImage,
|
|
36
|
+
running: Boolean(running[image.id]),
|
|
37
|
+
...runtime,
|
|
38
|
+
templateName: image.create?.templateName,
|
|
39
|
+
templateCategory: image.create?.templateCategory,
|
|
40
|
+
};
|
|
41
|
+
}),
|
|
37
42
|
};
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* PLexus owns `plexus.json`, so PLexus writes it. This package chooses what
|
|
47
|
+
* to declare — the load script that puts the pinned MCP in the image — and
|
|
48
|
+
* leaves the schema, the name template, the defaults and the validation to
|
|
49
|
+
* the package that reads them back.
|
|
50
|
+
*
|
|
51
|
+
* @param {{ id: string, template: string, category?: string, profile?: string }} request
|
|
52
|
+
*/
|
|
41
53
|
export function addImage(projectRoot, request) {
|
|
42
54
|
const { id, template, category, profile } = request;
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
const declaredBefore = configuredImages(projectRoot).length;
|
|
56
|
+
const config = declareProjectImage(projectRoot, {
|
|
57
|
+
id,
|
|
58
|
+
templateName: template,
|
|
59
|
+
...(category ? { templateCategory: category } : {}),
|
|
60
|
+
...(profile ? { profileId: profile } : {}),
|
|
61
|
+
loadScript: defaultLoadScript,
|
|
62
|
+
});
|
|
63
|
+
const declared = config.images.find((image) => image.id === id);
|
|
64
|
+
if (!declared) {
|
|
65
|
+
throw new Error(`PLexus declared image ${id} but did not record it.`);
|
|
51
66
|
}
|
|
52
67
|
|
|
53
|
-
const image = {
|
|
54
|
-
id,
|
|
55
|
-
imageName,
|
|
56
|
-
active: true,
|
|
57
|
-
mcp: { loadScript: defaultLoadScript, loadPolicy: "ifMissing" },
|
|
58
|
-
create: {
|
|
59
|
-
kind: "template",
|
|
60
|
-
...(profile ? { profileId: profile } : {}),
|
|
61
|
-
templateName: template,
|
|
62
|
-
...(category ? { templateCategory: category } : {}),
|
|
63
|
-
cleanupPolicy: "workspace_cleanup_only",
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
writeProjectConfig(projectRoot, { ...config, images: [...images, image] });
|
|
67
68
|
const pluginConfig = readConfig(projectRoot);
|
|
68
|
-
const isDefault = pluginConfig.defaultImage === undefined &&
|
|
69
|
+
const isDefault = pluginConfig.defaultImage === undefined && declaredBefore === 0;
|
|
69
70
|
if (isDefault) {
|
|
70
71
|
writeConfig(projectRoot, { ...pluginConfig, defaultImage: id });
|
|
71
72
|
}
|
|
72
73
|
return {
|
|
73
74
|
id,
|
|
74
|
-
imageName,
|
|
75
|
+
imageName: declared.imageName,
|
|
75
76
|
isDefault,
|
|
76
77
|
templateName: template,
|
|
77
78
|
...(category ? { templateCategory: category } : {}),
|
|
@@ -87,16 +88,11 @@ export function useImage(projectRoot, id) {
|
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
export function removeImage(projectRoot, id) {
|
|
90
|
-
|
|
91
|
-
const images = Array.isArray(config.images) ? config.images : [];
|
|
92
|
-
if (!images.some((image) => image.id === id)) {
|
|
91
|
+
if (!configuredImages(projectRoot).some((image) => image.id === id)) {
|
|
93
92
|
throw new Error(`Image ${id} is not configured.`);
|
|
94
93
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
...config,
|
|
98
|
-
images: images.filter((image) => image.id !== id),
|
|
99
|
-
});
|
|
94
|
+
assertNotRunning(projectRoot, id, "removing it");
|
|
95
|
+
removeProjectImage(projectRoot, id);
|
|
100
96
|
const pluginConfig = readConfig(projectRoot);
|
|
101
97
|
if (pluginConfig.defaultImage === id) {
|
|
102
98
|
const { defaultImage: _removed, ...rest } = pluginConfig;
|