@evref-bl/pharo-agent 0.5.3 → 0.5.5
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/package.json +4 -4
- package/plugin.json +1 -1
- package/skills/pharo-setup/SKILL.md +1 -1
- package/src/gateway.js +24 -6
- package/src/live.js +23 -4
- package/src/runtime.js +38 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evref-bl/pharo-agent",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Terminal-first Pharo integration for coding agents.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"test": "node --test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@evref-bl/pharo-mcp": "2.1.
|
|
35
|
-
"@evref-bl/plexus": "0.5.
|
|
36
|
-
"@evref-bl/plexus-gateway": "0.5.
|
|
34
|
+
"@evref-bl/pharo-mcp": "2.1.1",
|
|
35
|
+
"@evref-bl/plexus": "0.5.5",
|
|
36
|
+
"@evref-bl/plexus-gateway": "0.5.5",
|
|
37
37
|
"@modelcontextprotocol/sdk": "1.32.1"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/plugin.json
CHANGED
|
@@ -8,7 +8,7 @@ Run this only when the user asks to enable Pharo tooling in this project.
|
|
|
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.5.
|
|
11
|
+
npx --yes @evref-bl/pharo-agent@0.5.5 init --host claude
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
Name the host this chat runs in: `--host claude` in Claude Code,
|
package/src/gateway.js
CHANGED
|
@@ -6,7 +6,12 @@ import {
|
|
|
6
6
|
CallToolRequestSchema,
|
|
7
7
|
ListToolsRequestSchema,
|
|
8
8
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
readJson,
|
|
11
|
+
readOpenImage,
|
|
12
|
+
selectRunningImage,
|
|
13
|
+
unappliedPharoMcpPin,
|
|
14
|
+
} from "./runtime.js";
|
|
10
15
|
|
|
11
16
|
// Read rather than restate: a hard-coded version is one more place for a
|
|
12
17
|
// release to forget, and the host shows this one to the user.
|
|
@@ -34,20 +39,33 @@ export async function imageGateway(projectRoot, open) {
|
|
|
34
39
|
if (!registered.ok) {
|
|
35
40
|
throw new Error(registered.error ?? "Cannot register the PLexus route.");
|
|
36
41
|
}
|
|
37
|
-
return { gateway, mcpServerId };
|
|
42
|
+
return { gateway, mcpServerId, image };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// An image whose Pharo MCP predates 2.1.0 resolves tools/call against the
|
|
46
|
+
// advertised list, so a tool it holds but does not advertise answers -32601.
|
|
47
|
+
// That is the loudest way an unapplied pin shows itself, and the message
|
|
48
|
+
// alone sends a reader looking for a tool that is right there.
|
|
49
|
+
const methodNotFound = "-32601";
|
|
50
|
+
|
|
51
|
+
export function toolFailureMessage(error, image) {
|
|
52
|
+
const unappliedPin = error.includes(methodNotFound) ? unappliedPharoMcpPin(image) : undefined;
|
|
53
|
+
return unappliedPin ? `${error}\n${unappliedPin}` : error;
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
export async function callImageTool(projectRoot, open, toolName, argumentsValue) {
|
|
41
|
-
const { gateway, mcpServerId } = await imageGateway(projectRoot, open);
|
|
57
|
+
const { gateway, mcpServerId, image } = await imageGateway(projectRoot, open);
|
|
42
58
|
const result = await gateway.routeToImage({
|
|
43
59
|
mcpServerId,
|
|
44
60
|
toolName,
|
|
45
61
|
arguments: argumentsValue,
|
|
46
62
|
});
|
|
47
|
-
if (
|
|
48
|
-
|
|
63
|
+
if (result.ok) {
|
|
64
|
+
return result.data;
|
|
49
65
|
}
|
|
50
|
-
|
|
66
|
+
throw new Error(
|
|
67
|
+
toolFailureMessage(result.error ?? `Pharo tool ${toolName} failed.`, image),
|
|
68
|
+
);
|
|
51
69
|
}
|
|
52
70
|
|
|
53
71
|
export async function listImageTools(projectRoot, open) {
|
package/src/live.js
CHANGED
|
@@ -12,16 +12,18 @@ import { callImageTool } from "./gateway.js";
|
|
|
12
12
|
import { readProjectConfig, writeProjectConfig } from "./project.js";
|
|
13
13
|
import {
|
|
14
14
|
defaultImageId,
|
|
15
|
+
openedImageIsAlive,
|
|
15
16
|
readConfig,
|
|
16
17
|
readOpenImages,
|
|
17
18
|
resolveImageId,
|
|
18
19
|
resolveMountPath,
|
|
19
20
|
selectRunningImage,
|
|
20
21
|
smalltalkString,
|
|
22
|
+
unappliedPharoMcpPin,
|
|
21
23
|
writeOpenImages,
|
|
22
24
|
} from "./runtime.js";
|
|
23
25
|
|
|
24
|
-
export const pifsRelease = "v0.6.
|
|
26
|
+
export const pifsRelease = "v0.6.2";
|
|
25
27
|
|
|
26
28
|
// The class-side PIFSServer messages this plugin sends into the image. The
|
|
27
29
|
// release automation checks that a published PIFS still implements them: a pin
|
|
@@ -185,9 +187,18 @@ export function fetchPifsRelease(cloneDirectory, release = pifsRelease) {
|
|
|
185
187
|
|
|
186
188
|
export async function openAgentImage(projectRoot, imageId) {
|
|
187
189
|
const resolved = resolveImageId(projectRoot, imageId);
|
|
188
|
-
|
|
190
|
+
let openImages = readOpenImages(projectRoot);
|
|
191
|
+
let restarted = false;
|
|
189
192
|
if (openImages[resolved]) {
|
|
190
|
-
|
|
193
|
+
if (openedImageIsAlive(openImages[resolved])) {
|
|
194
|
+
return { alreadyOpen: true, ...openImages[resolved] };
|
|
195
|
+
}
|
|
196
|
+
// The image this record names has exited. Forget it and open again,
|
|
197
|
+
// rather than hand back an endpoint nothing answers and say it is open.
|
|
198
|
+
const { [resolved]: exited, ...remaining } = openImages;
|
|
199
|
+
openImages = remaining;
|
|
200
|
+
writeOpenImages(projectRoot, openImages);
|
|
201
|
+
restarted = true;
|
|
191
202
|
}
|
|
192
203
|
fetchPifsRelease(pifsCloneDirectory(projectRoot));
|
|
193
204
|
const launcher = new ScopedPharoLauncher({ projectRoot });
|
|
@@ -238,7 +249,15 @@ export async function openAgentImage(projectRoot, imageId) {
|
|
|
238
249
|
throw error;
|
|
239
250
|
}
|
|
240
251
|
writeOpenImages(projectRoot, { ...openImages, [resolved]: { ...open, projectionPort } });
|
|
241
|
-
|
|
252
|
+
const unappliedPin = unappliedPharoMcpPin(image);
|
|
253
|
+
return {
|
|
254
|
+
imageId: image.id,
|
|
255
|
+
created,
|
|
256
|
+
...(restarted ? { restarted } : {}),
|
|
257
|
+
mountPath,
|
|
258
|
+
projectionPort,
|
|
259
|
+
...(unappliedPin ? { warning: unappliedPin } : {}),
|
|
260
|
+
};
|
|
242
261
|
}
|
|
243
262
|
|
|
244
263
|
function unsupportedRunningImage(state, imageId) {
|
package/src/runtime.js
CHANGED
|
@@ -99,6 +99,32 @@ export function readOpenImage(projectRoot, imageId) {
|
|
|
99
99
|
return open;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// A record in the project's runtime state says this project opened the image,
|
|
103
|
+
// not that the image is still there. PLexus reclaims a dead image's port and
|
|
104
|
+
// relaunches on the next open, so the only thing standing between a crashed
|
|
105
|
+
// image and a working one is believing the record.
|
|
106
|
+
export function imageProcessIsAlive(pid) {
|
|
107
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
108
|
+
try {
|
|
109
|
+
process.kill(pid, 0);
|
|
110
|
+
return true;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
// The process is someone else's, which is still a process.
|
|
113
|
+
return error.code === "EPERM";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function openedImageIsAlive(open) {
|
|
118
|
+
let state;
|
|
119
|
+
try {
|
|
120
|
+
state = readJson(open.statePath);
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
const image = (state.images ?? []).find((candidate) => candidate.id === open.imageId);
|
|
125
|
+
return image?.status === "running" && imageProcessIsAlive(image.pid);
|
|
126
|
+
}
|
|
127
|
+
|
|
102
128
|
export function resolveMountPath(projectRoot, config, imageId) {
|
|
103
129
|
const root = config.mountRoot ?? ".pharo-agent/mounts";
|
|
104
130
|
return path.resolve(projectRoot, root, imageId);
|
|
@@ -131,6 +157,18 @@ export function selectRunningImage(state, imageId) {
|
|
|
131
157
|
return { image, mcpServerId: image.mcpServers[0].id };
|
|
132
158
|
}
|
|
133
159
|
|
|
160
|
+
// `mcp.loadPolicy` defaults to ifMissing, so an image that already carries a
|
|
161
|
+
// Pharo MCP keeps it and the version this project pins is never loaded. That
|
|
162
|
+
// is a supported way to run — it is what the policy is for — but nothing said
|
|
163
|
+
// so, and the image's tools then quietly differ from the ones the project was
|
|
164
|
+
// written against. PLexus records it; this says it.
|
|
165
|
+
export function unappliedPharoMcpPin(image) {
|
|
166
|
+
if (image?.pharoMcpLoad?.state !== "provided") return undefined;
|
|
167
|
+
return `Image ${image.id} opened with the Pharo MCP it already had, so the version this project pins`
|
|
168
|
+
+ " was not loaded and its tools may differ from the ones this project expects."
|
|
169
|
+
+ " Recreate the image, or set mcp.loadPolicy to always.";
|
|
170
|
+
}
|
|
171
|
+
|
|
134
172
|
export function smalltalkString(value) {
|
|
135
173
|
return `'${String(value).replaceAll("'", "''")}'`;
|
|
136
174
|
}
|