@evref-bl/pharo-agent 0.3.7 → 0.3.8

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.7",
4
+ "version": "0.3.8",
5
5
  "description": "Terminal-first Pharo image setup for coding agents.",
6
6
  "author": {
7
7
  "name": "Evref-BL",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evref-bl/pharo-agent",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
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.5.0",
35
- "@evref-bl/plexus": "0.4.5",
36
- "@evref-bl/plexus-gateway": "0.4.5",
34
+ "@evref-bl/pharo-mcp": "1.5.1",
35
+ "@evref-bl/plexus": "0.4.7",
36
+ "@evref-bl/plexus-gateway": "0.4.7",
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.7",
4
+ "version": "0.3.8",
5
5
  "description": "Terminal-first Pharo image setup for coding agents.",
6
6
  "author": {
7
7
  "name": "Evref-BL",
@@ -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.3.7 init --host codex
11
+ npx --yes @evref-bl/pharo-agent@0.3.8 init --host codex
12
12
  ```
13
13
 
14
14
  Use `--host claude` in Claude Code and `--host opencode` in OpenCode.
package/src/cli.js CHANGED
@@ -32,7 +32,7 @@ function parseImageAdd(args) {
32
32
  throw new Error("image add requires an image id");
33
33
  }
34
34
  const parsed = { id };
35
- const options = { "--template": "template", "--category": "category", "--profile": "profile" };
35
+ const options = { "--template": "template", "--profile": "profile" };
36
36
  for (let index = 0; index < rest.length; index += 1) {
37
37
  const option = rest[index];
38
38
  const key = options[option];
@@ -67,7 +67,7 @@ function usage() {
67
67
  "Usage:",
68
68
  " pharo-agent init --host <codex|claude|opencode> [--mount-root <path>]",
69
69
  " pharo-agent image list",
70
- " pharo-agent image add <id> --template <name> [--category <name>] [--profile <id>]",
70
+ " pharo-agent image add <id> --template <name> [--profile <id>]",
71
71
  " pharo-agent image use <id>",
72
72
  " pharo-agent image remove <id>",
73
73
  " pharo-agent open [--image <id>]",
package/src/images.js CHANGED
@@ -37,7 +37,7 @@ export function listImages(projectRoot) {
37
37
  };
38
38
  }
39
39
 
40
- export function addImage(projectRoot, { id, template, category, profile }) {
40
+ export function addImage(projectRoot, { id, template, profile }) {
41
41
  const config = readProjectConfig(projectRoot);
42
42
  const images = Array.isArray(config.images) ? config.images : [];
43
43
  if (images.some((image) => image.id === id)) {
@@ -57,7 +57,6 @@ export function addImage(projectRoot, { id, template, category, profile }) {
57
57
  kind: "template",
58
58
  ...(profile ? { profileId: profile } : {}),
59
59
  templateName: template,
60
- ...(category ? { templateCategory: category } : {}),
61
60
  cleanupPolicy: "workspace_cleanup_only",
62
61
  },
63
62
  };
package/src/live.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  resolvePlexusHomePath,
10
10
  } from "@evref-bl/plexus";
11
11
  import { callImageTool } from "./gateway.js";
12
- import { readProjectConfig } from "./project.js";
12
+ import { readProjectConfig, writeProjectConfig } from "./project.js";
13
13
  import {
14
14
  defaultImageId,
15
15
  readConfig,
@@ -62,6 +62,48 @@ export function needsImageCreation(listed, imageId) {
62
62
  return summary.status === "declared";
63
63
  }
64
64
 
65
+ // The launcher resolves a template name against its default group only, so an
66
+ // image naming a template from any other group cannot be created until the
67
+ // project config says which group that is. Find it once, on first creation.
68
+ export async function resolveTemplateCategory(projectRoot, launcher, imageId) {
69
+ const config = readProjectConfig(projectRoot);
70
+ const images = Array.isArray(config.images) ? config.images : [];
71
+ const configured = images.find((image) => image.id === imageId);
72
+ const create = configured?.create;
73
+ if (!create?.templateName || create.templateCategory) {
74
+ return create?.templateCategory;
75
+ }
76
+
77
+ const listed = await launcher.listTemplates();
78
+ const templates = Array.isArray(listed?.templates) ? listed.templates : [];
79
+ const categories = [
80
+ ...new Set(
81
+ templates
82
+ .filter((template) => template?.name === create.templateName)
83
+ .map((template) => template?.templateCategory)
84
+ .filter(Boolean),
85
+ ),
86
+ ];
87
+ if (categories.length === 0) {
88
+ throw new Error(`No template named ${create.templateName} in any category.`);
89
+ }
90
+ if (categories.length > 1) {
91
+ throw new Error(
92
+ `Template ${create.templateName} exists in several categories: ${categories.join(", ")}.`
93
+ + ` Set create.templateCategory for image ${imageId} to choose one.`,
94
+ );
95
+ }
96
+
97
+ const [templateCategory] = categories;
98
+ writeProjectConfig(projectRoot, {
99
+ ...config,
100
+ images: images.map((image) => (image.id === imageId
101
+ ? { ...image, create: { ...image.create, templateCategory } }
102
+ : image)),
103
+ });
104
+ return templateCategory;
105
+ }
106
+
65
107
  export function describeLifecycleFailure(failure) {
66
108
  const stderr = failure.launcherResult?.raw?.stderr?.trim();
67
109
  return [
@@ -116,13 +158,22 @@ export async function openAgentImage(projectRoot, imageId) {
116
158
  const launcher = new ScopedPharoLauncher({ projectRoot });
117
159
  const created = needsImageCreation(launcher.listImages(), resolved);
118
160
  if (created) {
161
+ await resolveTemplateCategory(projectRoot, launcher, resolved);
119
162
  await launcher.createImage(resolved);
120
163
  }
121
164
  const opened = await runLifecycle(
122
165
  () => openProject({ projectRoot, imageIds: [resolved] }),
123
166
  `PLexus could not open image ${resolved}.`,
124
167
  );
125
- const { image, mcpServerId } = selectRunningImage(opened.state, resolved);
168
+ let image;
169
+ let mcpServerId;
170
+ try {
171
+ ({ image, mcpServerId } = selectRunningImage(opened.state, resolved));
172
+ } catch (error) {
173
+ // The image is running but unusable to us, so do not leave it behind.
174
+ await closeProject({ projectRoot, imageIds: [resolved] }).catch(() => {});
175
+ throw error;
176
+ }
126
177
  const mountPath = resolveMountPath(projectRoot, readConfig(projectRoot), image.id);
127
178
  fs.mkdirSync(path.dirname(mountPath), { recursive: true });
128
179
  const open = {
package/src/runtime.js CHANGED
@@ -112,8 +112,14 @@ export function selectRunningImage(state, imageId) {
112
112
  }
113
113
  const image = images[0];
114
114
  if (!Array.isArray(image.mcpServers) || image.mcpServers.length !== 1) {
115
+ // PLexus withholds the route when the image is outside the MCP contract,
116
+ // and records why. Say that rather than only the endpoint count.
117
+ const contract = image.pharoMcpContract;
118
+ const reason = contract?.status === "unsupported" && contract.reason
119
+ ? ` ${contract.reason}`
120
+ : "";
115
121
  throw new Error(
116
- `Image ${image.id} needs exactly one MCP endpoint; PLexus reported ${image.mcpServers?.length ?? 0}.`,
122
+ `Image ${image.id} needs exactly one MCP endpoint; PLexus reported ${image.mcpServers?.length ?? 0}.${reason}`,
117
123
  );
118
124
  }
119
125
  return { image, mcpServerId: image.mcpServers[0].id };