@evref-bl/pharo-agent 0.3.0 → 0.3.2

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pharo-agent",
3
3
  "displayName": "Pharo Agent",
4
- "version": "0.3.0",
4
+ "version": "0.3.2",
5
5
  "description": "Terminal-first Pharo image setup for coding agents.",
6
6
  "author": {
7
7
  "name": "Evref-BL",
package/README.md CHANGED
@@ -15,7 +15,7 @@ on-demand Pharo workflow skills and references from the pinned MCP template
15
15
  package. It does not install an agent-host MCP server.
16
16
 
17
17
  `init` acts on the current directory and never searches above it. If that
18
- directory has no `plexus.project.json`, it creates one that declares the
18
+ directory has no `plexus.json`, it creates one that declares the
19
19
  project and no image; an existing one is left untouched. It refuses to run
20
20
  inside an enclosing PLexus project rather than initializing that project root.
21
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evref-bl/pharo-agent",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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": "1.4.1",
35
- "@evref-bl/plexus": "0.3.0",
36
- "@evref-bl/plexus-gateway": "0.3.0",
34
+ "@evref-bl/pharo-mcp": "1.5.0",
35
+ "@evref-bl/plexus": "0.4.1",
36
+ "@evref-bl/plexus-gateway": "0.4.1",
37
37
  "@modelcontextprotocol/sdk": "1.32.1"
38
38
  }
39
39
  }
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "pharo-agent",
4
- "version": "0.3.0",
4
+ "version": "0.3.2",
5
5
  "description": "Terminal-first Pharo image setup for coding agents.",
6
6
  "author": {
7
7
  "name": "Evref-BL",
@@ -8,14 +8,14 @@ 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.3.0 init --host codex
11
+ npx --yes @evref-bl/pharo-agent@0.3.2 init --host codex
12
12
  ```
13
13
 
14
14
  Use `--host claude` in Claude Code and `--host opencode` in OpenCode.
15
15
  Do not configure an MCP server in the agent host. The command adds only a
16
16
  conditional instruction block, so Pharo guidance stays out of unrelated work.
17
17
 
18
- Setup initializes the current directory, creating `plexus.project.json` with
18
+ Setup initializes the current directory, creating `plexus.json` with
19
19
  no image when it is absent. Register one before the first `pharo-agent open`:
20
20
 
21
21
  ```sh
package/src/init.js CHANGED
@@ -77,7 +77,7 @@ function guidance() {
77
77
  function pharoGuide() {
78
78
  return `# Pharo project guide
79
79
 
80
- \`plexus.project.json\` declares this project's images. A freshly initialized
80
+ \`plexus.json\` declares this project's images. A freshly initialized
81
81
  project declares none; register one before the first \`pharo-agent open\`:
82
82
 
83
83
  \`\`\`sh
package/src/live.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { closeProject, openProject } from "@evref-bl/plexus";
3
+ import { ScopedPharoLauncher, closeProject, openProject } from "@evref-bl/plexus";
4
4
  import { callImageTool } from "./gateway.js";
5
5
  import {
6
6
  defaultImageId,
@@ -20,13 +20,21 @@ export function pifsStartCode(mountPath, imageName) {
20
20
  baseline: 'PharoImageFS';
21
21
  repository: 'github://Evref-BL/pharo-image-fs:${pifsRelease}/src';
22
22
  load: 'Core'.
23
- (PIFSServer
23
+ ((Smalltalk globals at: #PIFSServer)
24
24
  startOn: 0
25
25
  mountAt: ${smalltalkString(mountPath)} asFileReference
26
26
  named: ${smalltalkString(imageName)}) port`;
27
27
  }
28
28
 
29
29
  export function resultPrintString(result) {
30
+ // MCP tool results carry the image's answer as JSON inside a text content part.
31
+ if (typeof result === "string") {
32
+ try {
33
+ return resultPrintString(JSON.parse(result));
34
+ } catch {
35
+ return undefined;
36
+ }
37
+ }
30
38
  if (!result || typeof result !== "object") return undefined;
31
39
  if (typeof result.resultPrintString === "string") return result.resultPrintString;
32
40
  for (const value of Object.values(result)) {
@@ -36,16 +44,56 @@ export function resultPrintString(result) {
36
44
  return undefined;
37
45
  }
38
46
 
47
+
48
+ export function needsImageCreation(listed, imageId) {
49
+ const summary = listed.images.find((image) => image.imageId === imageId);
50
+ if (!summary) {
51
+ throw new Error(`PLexus does not know image ${imageId}.`);
52
+ }
53
+ // PLexus reports an image it has no runtime state for as declared.
54
+ return summary.status === "declared";
55
+ }
56
+
57
+ export function describeLifecycleFailure(failure) {
58
+ const stderr = failure.launcherResult?.raw?.stderr?.trim();
59
+ return [
60
+ `${failure.imageId}: ${failure.message}`,
61
+ failure.diagnostic,
62
+ failure.action,
63
+ stderr ? `launcher: ${stderr.slice(0, 500)}` : undefined,
64
+ ]
65
+ .filter(Boolean)
66
+ .join("\n ");
67
+ }
68
+
69
+ // openProject and closeProject throw on failure, carrying the failures on the error.
70
+ export async function runLifecycle(action, fallback) {
71
+ try {
72
+ return await action();
73
+ } catch (error) {
74
+ const failures = error?.result?.failures;
75
+ if (!Array.isArray(failures) || failures.length === 0) {
76
+ throw error;
77
+ }
78
+ throw new Error(`${fallback}\n${failures.map(describeLifecycleFailure).join("\n")}`);
79
+ }
80
+ }
81
+
39
82
  export async function openAgentImage(projectRoot, imageId) {
40
83
  const resolved = resolveImageId(projectRoot, imageId);
41
84
  const openImages = readOpenImages(projectRoot);
42
85
  if (openImages[resolved]) {
43
86
  return { alreadyOpen: true, ...openImages[resolved] };
44
87
  }
45
- const opened = await openProject({ projectRoot, imageIds: [resolved] });
46
- if (!opened.ok || opened.failures.length > 0) {
47
- throw new Error(opened.failures.map((failure) => failure.message).join("\n") || "PLexus could not open the project image.");
88
+ const launcher = new ScopedPharoLauncher({ projectRoot });
89
+ const created = needsImageCreation(launcher.listImages(), resolved);
90
+ if (created) {
91
+ await launcher.createImage(resolved);
48
92
  }
93
+ const opened = await runLifecycle(
94
+ () => openProject({ projectRoot, imageIds: [resolved] }),
95
+ `PLexus could not open image ${resolved}.`,
96
+ );
49
97
  const { image, mcpServerId } = selectRunningImage(opened.state, resolved);
50
98
  const mountPath = resolveMountPath(projectRoot, readConfig(projectRoot), image.id);
51
99
  fs.mkdirSync(path.dirname(mountPath), { recursive: true });
@@ -60,10 +108,12 @@ export async function openAgentImage(projectRoot, imageId) {
60
108
  });
61
109
  const projectionPort = Number(resultPrintString(result));
62
110
  if (!Number.isInteger(projectionPort) || projectionPort < 1) {
63
- throw new Error("PIFS started but did not report its projection port.");
111
+ throw new Error(
112
+ `PIFS did not report a projection port: ${JSON.stringify(result).slice(0, 500)}`,
113
+ );
64
114
  }
65
115
  writeOpenImages(projectRoot, { ...openImages, [resolved]: { ...open, projectionPort } });
66
- return { imageId: image.id, mountPath, projectionPort };
116
+ return { imageId: image.id, created, mountPath, projectionPort };
67
117
  }
68
118
 
69
119
  async function closeOneImage(projectRoot, openImages, imageId) {
@@ -71,15 +121,15 @@ async function closeOneImage(projectRoot, openImages, imageId) {
71
121
  let projectionError;
72
122
  try {
73
123
  await callImageTool(projectRoot, open, "image_evaluate", {
74
- code: `PIFSServer stopOn: ${open.projectionPort}`,
124
+ code: `(Smalltalk globals at: #PIFSServer) stopOn: ${open.projectionPort}`,
75
125
  });
76
126
  } catch (error) {
77
127
  projectionError = error;
78
128
  }
79
- const closed = await closeProject({ projectRoot, imageIds: [imageId] });
80
- if (!closed.ok || closed.failures.length > 0) {
81
- throw new Error(closed.failures.map((failure) => failure.message).join("\n") || "PLexus could not close the project image.");
82
- }
129
+ const closed = await runLifecycle(
130
+ () => closeProject({ projectRoot, imageIds: [imageId] }),
131
+ `PLexus could not close image ${imageId}.`,
132
+ );
83
133
  return { stoppedImages: closed.stoppedImages.map((image) => image.id), projectionError };
84
134
  }
85
135
 
package/src/project.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
- export const projectConfigFileName = "plexus.project.json";
4
+ export const projectConfigFileName = "plexus.json";
5
5
 
6
6
  export function findProjectRoot(start = process.cwd()) {
7
7
  let directory = path.resolve(start);
@@ -13,7 +13,7 @@ export function findProjectRoot(start = process.cwd()) {
13
13
 
14
14
  const parent = path.dirname(directory);
15
15
  if (parent === directory) {
16
- throw new Error("No plexus.project.json found above the current directory");
16
+ throw new Error("No plexus.json found above the current directory");
17
17
  }
18
18
  directory = parent;
19
19
  }