@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/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 = "plexus.json";
13
+ export const projectConfigFileName = plexusProjectConfigFileName;
5
14
 
6
- export function findProjectRoot(start = process.cwd()) {
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
- throw new Error("No plexus.json found above the current directory");
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 path.join(projectRoot, projectConfigFileName);
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
- export function projectIdentity(projectRoot) {
42
- const name = path.basename(path.resolve(projectRoot));
43
- const id = name
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
- const configPath = projectConfigPath(projectRoot);
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
- const { id, name } = projectIdentity(projectRoot);
65
- const config = { id, name, images: [] };
66
- writeProjectConfig(projectRoot, config);
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
- const config = readProjectConfig(projectRoot);
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 activePath(projectRoot) {
10
- return path.join(runtimeDirectory(projectRoot), "active.json");
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 readOpenImages(projectRoot) {
34
- const filePath = activePath(projectRoot);
34
+ export function readRunningImages(projectRoot) {
35
+ const filePath = runningImagesPath(projectRoot);
35
36
  if (!fs.existsSync(filePath)) return {};
36
37
  const recorded = readJson(filePath);
37
- if (recorded && typeof recorded.images === "object" && recorded.images !== null) {
38
- return recorded.images;
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 writeOpenImages(projectRoot, images) {
43
+ export function writeRunningImages(projectRoot, images) {
44
44
  if (Object.keys(images).length === 0) {
45
- fs.rmSync(activePath(projectRoot), { force: true });
45
+ fs.rmSync(runningImagesPath(projectRoot), { force: true });
46
46
  return;
47
47
  }
48
48
  fs.mkdirSync(runtimeDirectory(projectRoot), { recursive: true });
49
- fs.writeFileSync(activePath(projectRoot), `${JSON.stringify({ images }, null, 2)}\n`);
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 pass --image <id>.",
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 readOpenImage(projectRoot, imageId) {
85
+ export function readRunningImage(projectRoot, imageId) {
86
86
  const resolved = resolveImageId(projectRoot, imageId);
87
- const open = readOpenImages(projectRoot)[resolved];
88
- if (!open) {
87
+ const running = readRunningImages(projectRoot)[resolved];
88
+ if (!running) {
89
89
  throw new Error(
90
- `Image ${resolved} is not open. Run pharo-agent open${imageId ? ` --image ${resolved}` : ""} first.`,
90
+ `Image ${resolved} is not running. Run pharo-agent image start ${resolved} first.`,
91
91
  );
92
92
  }
93
- if (open.mcp === false) {
93
+ if (running.mcp === false) {
94
94
  throw new Error(
95
- `Image ${resolved} is open without MCP, so it offers no Pharo tools. `
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 open;
99
+ return running;
100
100
  }
101
101
 
102
- // A record in the project's runtime state says this project opened the image,
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 open, so the only thing standing between a crashed
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 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);
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} opened with the Pharo MCP it already had, so the version this project pins`
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
  }