@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/src/project.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
// The config file is PLexus's, so its name and its path come from PLexus.
|
|
4
|
+
// Spelling them here made this package a second definition of another
|
|
5
|
+
// package's format, free to drift from it without anything noticing.
|
|
6
|
+
import {
|
|
7
|
+
createProjectConfigFile,
|
|
8
|
+
loadProjectConfig,
|
|
9
|
+
plexusProjectConfigFileName,
|
|
10
|
+
projectConfigPath as plexusProjectConfigPath,
|
|
11
|
+
} from "@evref-bl/plexus";
|
|
3
12
|
|
|
4
|
-
export const projectConfigFileName =
|
|
13
|
+
export const projectConfigFileName = plexusProjectConfigFileName;
|
|
5
14
|
|
|
6
|
-
|
|
15
|
+
function projectRootAbove(start) {
|
|
7
16
|
let directory = path.resolve(start);
|
|
8
17
|
|
|
9
18
|
while (true) {
|
|
@@ -13,12 +22,28 @@ export function findProjectRoot(start = process.cwd()) {
|
|
|
13
22
|
|
|
14
23
|
const parent = path.dirname(directory);
|
|
15
24
|
if (parent === directory) {
|
|
16
|
-
|
|
25
|
+
return undefined;
|
|
17
26
|
}
|
|
18
27
|
directory = parent;
|
|
19
28
|
}
|
|
20
29
|
}
|
|
21
30
|
|
|
31
|
+
export function findProjectRoot(start = process.cwd()) {
|
|
32
|
+
const found = projectRootAbove(start);
|
|
33
|
+
if (found) return found;
|
|
34
|
+
|
|
35
|
+
// A plugin's MCP server is started by the host, in the plugin's own
|
|
36
|
+
// directory rather than the project's, so the working directory leads
|
|
37
|
+
// nowhere. The host says where the project is, and that is only consulted
|
|
38
|
+
// here, after the ordinary search has failed, so running the CLI inside a
|
|
39
|
+
// project still answers with that project.
|
|
40
|
+
const hosted = process.env.CLAUDE_PROJECT_DIR;
|
|
41
|
+
const hostedRoot = hosted ? projectRootAbove(hosted) : undefined;
|
|
42
|
+
if (hostedRoot) return hostedRoot;
|
|
43
|
+
|
|
44
|
+
throw new Error("No plexus.json found above the current directory");
|
|
45
|
+
}
|
|
46
|
+
|
|
22
47
|
export function enclosingProjectRoot(start) {
|
|
23
48
|
const directory = path.resolve(start);
|
|
24
49
|
const parent = path.dirname(directory);
|
|
@@ -31,43 +56,26 @@ export function enclosingProjectRoot(start) {
|
|
|
31
56
|
}
|
|
32
57
|
|
|
33
58
|
export function projectConfigPath(projectRoot) {
|
|
34
|
-
return
|
|
59
|
+
return plexusProjectConfigPath(projectRoot);
|
|
35
60
|
}
|
|
36
61
|
|
|
37
62
|
export function pluginDirectory(projectRoot) {
|
|
38
63
|
return path.join(projectRoot, ".pharo-agent");
|
|
39
64
|
}
|
|
40
65
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.replaceAll(/[^A-Za-z0-9._-]+/g, "-")
|
|
45
|
-
.replaceAll(/^-+|-+$/g, "")
|
|
46
|
-
.toLowerCase();
|
|
47
|
-
return { id: id || "pharo-project", name };
|
|
48
|
-
}
|
|
49
|
-
|
|
66
|
+
// Validated, not merely parsed: a malformed plexus.json then fails naming
|
|
67
|
+
// what is wrong with it, rather than reaching PLexus as a shape it refuses
|
|
68
|
+
// further in.
|
|
50
69
|
export function readProjectConfig(projectRoot) {
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
54
|
-
} catch (error) {
|
|
55
|
-
throw new Error(`Cannot read ${configPath}: ${error.message}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function writeProjectConfig(projectRoot, config) {
|
|
60
|
-
fs.writeFileSync(projectConfigPath(projectRoot), `${JSON.stringify(config, null, 2)}\n`);
|
|
70
|
+
return loadProjectConfig(projectRoot);
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
export function createProjectConfig(projectRoot) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return config;
|
|
74
|
+
return createProjectConfigFile(projectRoot, {
|
|
75
|
+
name: path.basename(path.resolve(projectRoot)),
|
|
76
|
+
});
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
export function configuredImages(projectRoot) {
|
|
71
|
-
|
|
72
|
-
return Array.isArray(config.images) ? config.images : [];
|
|
80
|
+
return readProjectConfig(projectRoot).images;
|
|
73
81
|
}
|
package/src/runtime.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { loadProjectState } from "@evref-bl/plexus";
|
|
3
4
|
import { configuredImages, pluginDirectory } from "./project.js";
|
|
4
5
|
|
|
5
6
|
export function runtimeDirectory(projectRoot) {
|
|
6
7
|
return path.join(pluginDirectory(projectRoot), "runtime");
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
export function
|
|
10
|
-
return path.join(runtimeDirectory(projectRoot), "
|
|
10
|
+
export function runningImagesPath(projectRoot) {
|
|
11
|
+
return path.join(runtimeDirectory(projectRoot), "running.json");
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function readJson(filePath) {
|
|
@@ -30,23 +31,22 @@ export function writeConfig(projectRoot, config) {
|
|
|
30
31
|
fs.writeFileSync(configPath(projectRoot), `${JSON.stringify(config, null, 2)}\n`);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
export function
|
|
34
|
-
const filePath =
|
|
34
|
+
export function readRunningImages(projectRoot) {
|
|
35
|
+
const filePath = runningImagesPath(projectRoot);
|
|
35
36
|
if (!fs.existsSync(filePath)) return {};
|
|
36
37
|
const recorded = readJson(filePath);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return typeof recorded?.imageId === "string" ? { [recorded.imageId]: recorded } : {};
|
|
38
|
+
return recorded && typeof recorded.images === "object" && recorded.images !== null
|
|
39
|
+
? recorded.images
|
|
40
|
+
: {};
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function
|
|
43
|
+
export function writeRunningImages(projectRoot, images) {
|
|
44
44
|
if (Object.keys(images).length === 0) {
|
|
45
|
-
fs.rmSync(
|
|
45
|
+
fs.rmSync(runningImagesPath(projectRoot), { force: true });
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
fs.mkdirSync(runtimeDirectory(projectRoot), { recursive: true });
|
|
49
|
-
fs.writeFileSync(
|
|
49
|
+
fs.writeFileSync(runningImagesPath(projectRoot), `${JSON.stringify({ images }, null, 2)}\n`);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export function defaultImageId(projectRoot) {
|
|
@@ -70,7 +70,7 @@ export function defaultImageId(projectRoot) {
|
|
|
70
70
|
}
|
|
71
71
|
throw new Error(
|
|
72
72
|
`Several images are configured (${images.map((image) => image.id).join(", ")}); `
|
|
73
|
-
+ "choose one with pharo-agent image use <id>, or
|
|
73
|
+
+ "choose one with pharo-agent image use <id>, or name one.",
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -82,26 +82,26 @@ export function resolveImageId(projectRoot, imageId) {
|
|
|
82
82
|
return imageId;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
export function
|
|
85
|
+
export function readRunningImage(projectRoot, imageId) {
|
|
86
86
|
const resolved = resolveImageId(projectRoot, imageId);
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
87
|
+
const running = readRunningImages(projectRoot)[resolved];
|
|
88
|
+
if (!running) {
|
|
89
89
|
throw new Error(
|
|
90
|
-
`Image ${resolved} is not
|
|
90
|
+
`Image ${resolved} is not running. Run pharo-agent image start ${resolved} first.`,
|
|
91
91
|
);
|
|
92
92
|
}
|
|
93
|
-
if (
|
|
93
|
+
if (running.mcp === false) {
|
|
94
94
|
throw new Error(
|
|
95
|
-
`Image ${resolved} is
|
|
95
|
+
`Image ${resolved} is running without MCP, so it offers no Pharo tools. `
|
|
96
96
|
+ "Register an image on a Pharo version the MCP contract supports to use them.",
|
|
97
97
|
);
|
|
98
98
|
}
|
|
99
|
-
return
|
|
99
|
+
return running;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
// A record in the project's runtime state says this project
|
|
102
|
+
// A record in the project's runtime state says this project started the image,
|
|
103
103
|
// not that the image is still there. PLexus reclaims a dead image's port and
|
|
104
|
-
// relaunches on the next
|
|
104
|
+
// relaunches on the next start, so the only thing standing between a crashed
|
|
105
105
|
// image and a working one is believing the record.
|
|
106
106
|
export function imageProcessIsAlive(pid) {
|
|
107
107
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -114,14 +114,11 @@ export function imageProcessIsAlive(pid) {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
export function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
const image = (state.images ?? []).find((candidate) => candidate.id === open.imageId);
|
|
117
|
+
export function recordedImageIsRunning(record) {
|
|
118
|
+
// PLexus owns the state file, so PLexus reads it: a hand-rolled JSON.parse
|
|
119
|
+
// here was this package deciding what another package's format means.
|
|
120
|
+
const state = loadProjectState(record.statePath);
|
|
121
|
+
const image = state?.images.find((candidate) => candidate.id === record.imageId);
|
|
125
122
|
return image?.status === "running" && imageProcessIsAlive(image.pid);
|
|
126
123
|
}
|
|
127
124
|
|
|
@@ -164,7 +161,7 @@ export function selectRunningImage(state, imageId) {
|
|
|
164
161
|
// written against. PLexus records it; this says it.
|
|
165
162
|
export function unappliedPharoMcpPin(image) {
|
|
166
163
|
if (image?.pharoMcpLoad?.state !== "provided") return undefined;
|
|
167
|
-
return `Image ${image.id}
|
|
164
|
+
return `Image ${image.id} started with the Pharo MCP it already had, so the version this project pins`
|
|
168
165
|
+ " was not loaded and its tools may differ from the ones this project expects."
|
|
169
166
|
+ " Recreate the image, or set mcp.loadPolicy to always.";
|
|
170
167
|
}
|