@evref-bl/pharo-agent 0.2.1 → 0.3.1
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/README.md +18 -11
- package/package.json +4 -4
- package/plugin.json +1 -1
- package/skills/pharo-setup/SKILL.md +2 -2
- package/src/cli.js +35 -20
- package/src/gateway.js +59 -15
- package/src/images.js +25 -22
- package/src/init.js +10 -4
- package/src/live.js +72 -27
- package/src/project.js +2 -17
- package/src/runtime.js +71 -14
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.
|
|
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
|
|
|
@@ -25,16 +25,23 @@ Register the image the project needs before the first `open`:
|
|
|
25
25
|
pharo-agent image add dev --template 'Pharo 13.0 - 64bit (stable)'
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
`image add` registers a template-created image with the pinned MCP load policy
|
|
29
|
-
and
|
|
30
|
-
|
|
31
|
-
its declaration
|
|
32
|
-
|
|
33
|
-
`open` starts
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
`image add` registers a template-created image with the pinned MCP load policy,
|
|
29
|
+
and the first one registered becomes the default. `image list`,
|
|
30
|
+
`image use <id>` (change the default) and `image remove <id>` cover the rest;
|
|
31
|
+
an open image must be closed before its declaration is removed.
|
|
32
|
+
|
|
33
|
+
`open` starts an image through PLexus, loads PIFS, and mounts its projection
|
|
34
|
+
below `.pharo-agent/mounts`. `close` unmounts the projection and closes the
|
|
35
|
+
PLexus image. `pharo` calls any exposed MCP tool through the same project
|
|
36
|
+
route, while `mcp` exposes that tool surface as a command-scoped stdio MCP
|
|
37
|
+
server.
|
|
38
|
+
|
|
39
|
+
Several images can be open at once. `open`, `close`, `status`, `pharo` and
|
|
40
|
+
`mcp` all accept `--image <id>`; without it the default image answers, so a
|
|
41
|
+
project with one image never mentions an image at all. `open --image <id>`
|
|
42
|
+
adds an image without closing the others, and `close` without `--image` closes
|
|
43
|
+
every image this project opened. Over `mcp`, each Pharo tool takes the same
|
|
44
|
+
optional `image` argument.
|
|
38
45
|
|
|
39
46
|
Pass `--mount-root <path>` to `init` to use a different projection root.
|
|
40
47
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evref-bl/pharo-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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.
|
|
35
|
-
"@evref-bl/plexus": "0.
|
|
36
|
-
"@evref-bl/plexus-gateway": "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
|
@@ -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.
|
|
11
|
+
npx --yes @evref-bl/pharo-agent@0.3.1 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.
|
|
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/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { initialize } from "./init.js";
|
|
2
2
|
import { addImage, listImages, removeImage, useImage } from "./images.js";
|
|
3
|
-
import {
|
|
4
|
-
import { closeAgentImage, openAgentImage } from "./live.js";
|
|
3
|
+
import { callImageTool, listImageTools, serveImagesMcp } from "./gateway.js";
|
|
4
|
+
import { agentImageStatus, closeAgentImage, openAgentImage } from "./live.js";
|
|
5
5
|
import { findProjectRoot } from "./project.js";
|
|
6
|
-
import {
|
|
6
|
+
import { readOpenImage } from "./runtime.js";
|
|
7
7
|
|
|
8
8
|
function parseInit(args) {
|
|
9
9
|
let host;
|
|
@@ -52,6 +52,16 @@ function parseImageAdd(args) {
|
|
|
52
52
|
return parsed;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
function takeImageOption(args) {
|
|
56
|
+
const index = args.indexOf("--image");
|
|
57
|
+
if (index === -1) return { image: undefined, rest: args };
|
|
58
|
+
const image = args[index + 1];
|
|
59
|
+
if (!image) {
|
|
60
|
+
throw new Error("--image requires a value");
|
|
61
|
+
}
|
|
62
|
+
return { image, rest: [...args.slice(0, index), ...args.slice(index + 2)] };
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
function usage() {
|
|
56
66
|
return [
|
|
57
67
|
"Usage:",
|
|
@@ -60,12 +70,12 @@ function usage() {
|
|
|
60
70
|
" pharo-agent image add <id> --template <name> [--category <name>] [--profile <id>]",
|
|
61
71
|
" pharo-agent image use <id>",
|
|
62
72
|
" pharo-agent image remove <id>",
|
|
63
|
-
" pharo-agent open",
|
|
73
|
+
" pharo-agent open [--image <id>]",
|
|
64
74
|
" pharo-agent status",
|
|
65
|
-
" pharo-agent close",
|
|
66
|
-
" pharo-agent mcp",
|
|
67
|
-
" pharo-agent pharo list",
|
|
68
|
-
" pharo-agent pharo <tool> [arguments-json]",
|
|
75
|
+
" pharo-agent close [--image <id>]",
|
|
76
|
+
" pharo-agent mcp [--image <id>]",
|
|
77
|
+
" pharo-agent pharo list [--image <id>]",
|
|
78
|
+
" pharo-agent pharo <tool> [arguments-json] [--image <id>]",
|
|
69
79
|
].join("\n");
|
|
70
80
|
}
|
|
71
81
|
|
|
@@ -123,39 +133,44 @@ export async function main(args) {
|
|
|
123
133
|
}
|
|
124
134
|
throw new Error(`Unsupported image command: ${subcommand ?? ""}\n${usage()}`);
|
|
125
135
|
}
|
|
136
|
+
const { image, rest: commandArgs } = takeImageOption(rest);
|
|
126
137
|
if (command === "open") {
|
|
127
|
-
requireNoArguments(command,
|
|
128
|
-
console.log(JSON.stringify({ ok: true, ...(await openAgentImage(projectRoot)) }, null, 2));
|
|
138
|
+
requireNoArguments(command, commandArgs);
|
|
139
|
+
console.log(JSON.stringify({ ok: true, ...(await openAgentImage(projectRoot, image)) }, null, 2));
|
|
129
140
|
return;
|
|
130
141
|
}
|
|
131
142
|
if (command === "close") {
|
|
132
|
-
requireNoArguments(command,
|
|
133
|
-
console.log(JSON.stringify({ ok: true, ...(await closeAgentImage(projectRoot)) }, null, 2));
|
|
143
|
+
requireNoArguments(command, commandArgs);
|
|
144
|
+
console.log(JSON.stringify({ ok: true, ...(await closeAgentImage(projectRoot, image)) }, null, 2));
|
|
134
145
|
return;
|
|
135
146
|
}
|
|
136
147
|
if (command === "status") {
|
|
137
|
-
requireNoArguments(command,
|
|
138
|
-
console.log(JSON.stringify({ ok: true, ...
|
|
148
|
+
requireNoArguments(command, commandArgs);
|
|
149
|
+
console.log(JSON.stringify({ ok: true, ...agentImageStatus(projectRoot) }, null, 2));
|
|
139
150
|
return;
|
|
140
151
|
}
|
|
141
152
|
if (command === "mcp") {
|
|
142
|
-
requireNoArguments(command,
|
|
143
|
-
await
|
|
153
|
+
requireNoArguments(command, commandArgs);
|
|
154
|
+
await serveImagesMcp(projectRoot, image);
|
|
144
155
|
return;
|
|
145
156
|
}
|
|
146
157
|
if (command === "pharo") {
|
|
147
|
-
const [toolName, argumentsJson, ...extra] =
|
|
158
|
+
const [toolName, argumentsJson, ...extra] = commandArgs;
|
|
148
159
|
if (toolName === "list") {
|
|
149
160
|
if (argumentsJson || extra.length > 0) throw new Error("pharo list does not accept arguments");
|
|
150
|
-
console.log(JSON.stringify(
|
|
161
|
+
console.log(JSON.stringify(
|
|
162
|
+
await listImageTools(projectRoot, readOpenImage(projectRoot, image)),
|
|
163
|
+
null,
|
|
164
|
+
2,
|
|
165
|
+
));
|
|
151
166
|
return;
|
|
152
167
|
}
|
|
153
168
|
if (!toolName || extra.length > 0) {
|
|
154
169
|
throw new Error("pharo requires one tool name and an optional JSON object");
|
|
155
170
|
}
|
|
156
|
-
console.log(JSON.stringify(await
|
|
171
|
+
console.log(JSON.stringify(await callImageTool(
|
|
157
172
|
projectRoot,
|
|
158
|
-
|
|
173
|
+
readOpenImage(projectRoot, image),
|
|
159
174
|
toolName,
|
|
160
175
|
parseToolArguments(argumentsJson),
|
|
161
176
|
), null, 2));
|
package/src/gateway.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { PlexusGateway } from "@evref-bl/plexus-gateway";
|
|
2
|
-
import {
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ListToolsRequestSchema,
|
|
7
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { readJson, readOpenImage, selectRunningImage } from "./runtime.js";
|
|
5
9
|
|
|
6
|
-
export async function
|
|
7
|
-
const state = readJson(
|
|
8
|
-
const { image, mcpServerId } = selectRunningImage(state,
|
|
9
|
-
if (image.id !==
|
|
10
|
-
throw new Error(
|
|
10
|
+
export async function imageGateway(projectRoot, open) {
|
|
11
|
+
const state = readJson(open.statePath);
|
|
12
|
+
const { image, mcpServerId } = selectRunningImage(state, open.imageId);
|
|
13
|
+
if (image.id !== open.imageId || mcpServerId !== open.mcpServerId) {
|
|
14
|
+
throw new Error(`The PLexus image ${open.imageId} changed; run pharo-agent open again.`);
|
|
11
15
|
}
|
|
12
16
|
const gateway = new PlexusGateway({
|
|
13
17
|
pharoScope: {
|
|
@@ -19,7 +23,7 @@ export async function activeGateway(projectRoot, active) {
|
|
|
19
23
|
});
|
|
20
24
|
const registered = await gateway.registerProjectRoute({
|
|
21
25
|
projectRoot,
|
|
22
|
-
statePath:
|
|
26
|
+
statePath: open.statePath,
|
|
23
27
|
state,
|
|
24
28
|
});
|
|
25
29
|
if (!registered.ok) {
|
|
@@ -28,8 +32,8 @@ export async function activeGateway(projectRoot, active) {
|
|
|
28
32
|
return { gateway, mcpServerId };
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
export async function
|
|
32
|
-
const { gateway, mcpServerId } = await
|
|
35
|
+
export async function callImageTool(projectRoot, open, toolName, argumentsValue) {
|
|
36
|
+
const { gateway, mcpServerId } = await imageGateway(projectRoot, open);
|
|
33
37
|
const result = await gateway.routeToImage({
|
|
34
38
|
mcpServerId,
|
|
35
39
|
toolName,
|
|
@@ -41,13 +45,53 @@ export async function callActiveTool(projectRoot, active, toolName, argumentsVal
|
|
|
41
45
|
return result.data;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
export async function
|
|
45
|
-
const { gateway } = await
|
|
48
|
+
export async function listImageTools(projectRoot, open) {
|
|
49
|
+
const { gateway } = await imageGateway(projectRoot, open);
|
|
46
50
|
return gateway.refreshPharoTools();
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
export
|
|
50
|
-
const
|
|
51
|
-
const
|
|
53
|
+
export function withImageSelector(tool, defaultImage) {
|
|
54
|
+
const inputSchema = tool.inputSchema ?? {};
|
|
55
|
+
const properties = { ...(inputSchema.properties ?? {}) };
|
|
56
|
+
delete properties.mcpServerId;
|
|
57
|
+
properties.image = {
|
|
58
|
+
type: "string",
|
|
59
|
+
description: `Project image to call. Defaults to ${defaultImage}.`,
|
|
60
|
+
};
|
|
61
|
+
const required = Array.isArray(inputSchema.required)
|
|
62
|
+
? inputSchema.required.filter((value) => value !== "mcpServerId")
|
|
63
|
+
: [];
|
|
64
|
+
return { ...tool, inputSchema: { ...inputSchema, properties, required } };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function toolResult(data) {
|
|
68
|
+
if (data && typeof data === "object" && Array.isArray(data.content)) {
|
|
69
|
+
return data;
|
|
70
|
+
}
|
|
71
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function serveImagesMcp(projectRoot, defaultImage) {
|
|
75
|
+
const defaultOpen = readOpenImage(projectRoot, defaultImage);
|
|
76
|
+
const tools = (await listImageTools(projectRoot, defaultOpen))
|
|
77
|
+
.map((tool) => withImageSelector(tool, defaultImage));
|
|
78
|
+
const toolNames = new Set(tools.map((tool) => tool.name));
|
|
79
|
+
|
|
80
|
+
const server = new Server(
|
|
81
|
+
{ name: "pharo-agent", version: "0.0.0" },
|
|
82
|
+
{ capabilities: { tools: {} } },
|
|
83
|
+
);
|
|
84
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
85
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
86
|
+
if (!toolNames.has(request.params.name)) {
|
|
87
|
+
throw new Error(`Unknown Pharo tool: ${request.params.name}`);
|
|
88
|
+
}
|
|
89
|
+
const { image, ...argumentsValue } = request.params.arguments ?? {};
|
|
90
|
+
const open = readOpenImage(projectRoot, image);
|
|
91
|
+
return toolResult(
|
|
92
|
+
await callImageTool(projectRoot, open, request.params.name, argumentsValue),
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
52
96
|
await server.connect(new StdioServerTransport());
|
|
53
97
|
}
|
package/src/images.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import {
|
|
3
2
|
configuredImages,
|
|
4
3
|
projectIdentity,
|
|
5
4
|
readProjectConfig,
|
|
6
5
|
writeProjectConfig,
|
|
7
6
|
} from "./project.js";
|
|
8
|
-
import {
|
|
7
|
+
import { readConfig, readOpenImages, writeConfig } from "./runtime.js";
|
|
9
8
|
|
|
10
9
|
export const defaultLoadScript = "pharo/load-mcp.st";
|
|
11
10
|
|
|
12
11
|
function assertNoOpenImage(projectRoot, imageId, action) {
|
|
13
|
-
if (
|
|
14
|
-
const active = readActive(projectRoot);
|
|
15
|
-
if (active.imageId === imageId) {
|
|
12
|
+
if (readOpenImages(projectRoot)[imageId]) {
|
|
16
13
|
throw new Error(`Image ${imageId} is open. Run pharo-agent close before ${action}.`);
|
|
17
14
|
}
|
|
18
15
|
}
|
|
@@ -23,11 +20,17 @@ function derivedImageName(projectRoot, imageId) {
|
|
|
23
20
|
}
|
|
24
21
|
|
|
25
22
|
export function listImages(projectRoot) {
|
|
23
|
+
const configured = configuredImages(projectRoot);
|
|
24
|
+
const open = readOpenImages(projectRoot);
|
|
25
|
+
const defaultImage = readConfig(projectRoot).defaultImage
|
|
26
|
+
?? (configured.length === 1 ? configured[0].id : undefined);
|
|
26
27
|
return {
|
|
27
|
-
|
|
28
|
+
...(defaultImage ? { defaultImage } : {}),
|
|
29
|
+
images: configured.map((image) => ({
|
|
28
30
|
id: image.id,
|
|
29
31
|
imageName: image.imageName,
|
|
30
|
-
|
|
32
|
+
isDefault: image.id === defaultImage,
|
|
33
|
+
open: Boolean(open[image.id]),
|
|
31
34
|
templateName: image.create?.templateName,
|
|
32
35
|
templateCategory: image.create?.templateCategory,
|
|
33
36
|
})),
|
|
@@ -48,7 +51,7 @@ export function addImage(projectRoot, { id, template, category, profile }) {
|
|
|
48
51
|
const image = {
|
|
49
52
|
id,
|
|
50
53
|
imageName,
|
|
51
|
-
active:
|
|
54
|
+
active: true,
|
|
52
55
|
mcp: { loadScript: defaultLoadScript, loadPolicy: "ifMissing" },
|
|
53
56
|
create: {
|
|
54
57
|
kind: "template",
|
|
@@ -59,25 +62,20 @@ export function addImage(projectRoot, { id, template, category, profile }) {
|
|
|
59
62
|
},
|
|
60
63
|
};
|
|
61
64
|
writeProjectConfig(projectRoot, { ...config, images: [...images, image] });
|
|
62
|
-
|
|
65
|
+
const pluginConfig = readConfig(projectRoot);
|
|
66
|
+
const isDefault = pluginConfig.defaultImage === undefined && images.length === 0;
|
|
67
|
+
if (isDefault) {
|
|
68
|
+
writeConfig(projectRoot, { ...pluginConfig, defaultImage: id });
|
|
69
|
+
}
|
|
70
|
+
return { id, imageName, isDefault, templateName: template };
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
export function useImage(projectRoot, id) {
|
|
66
|
-
|
|
67
|
-
const images = Array.isArray(config.images) ? config.images : [];
|
|
68
|
-
if (!images.some((image) => image.id === id)) {
|
|
74
|
+
if (!configuredImages(projectRoot).some((image) => image.id === id)) {
|
|
69
75
|
throw new Error(`Image ${id} is not configured.`);
|
|
70
76
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
assertNoOpenImage(projectRoot, image.id, "switching the active image");
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
writeProjectConfig(projectRoot, {
|
|
77
|
-
...config,
|
|
78
|
-
images: images.map((image) => ({ ...image, active: image.id === id })),
|
|
79
|
-
});
|
|
80
|
-
return { activeImageId: id };
|
|
77
|
+
writeConfig(projectRoot, { ...readConfig(projectRoot), defaultImage: id });
|
|
78
|
+
return { defaultImage: id };
|
|
81
79
|
}
|
|
82
80
|
|
|
83
81
|
export function removeImage(projectRoot, id) {
|
|
@@ -91,5 +89,10 @@ export function removeImage(projectRoot, id) {
|
|
|
91
89
|
...config,
|
|
92
90
|
images: images.filter((image) => image.id !== id),
|
|
93
91
|
});
|
|
92
|
+
const pluginConfig = readConfig(projectRoot);
|
|
93
|
+
if (pluginConfig.defaultImage === id) {
|
|
94
|
+
const { defaultImage: _removed, ...rest } = pluginConfig;
|
|
95
|
+
writeConfig(projectRoot, rest);
|
|
96
|
+
}
|
|
94
97
|
return { removedImageId: id };
|
|
95
98
|
}
|
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.
|
|
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
|
|
@@ -85,9 +85,9 @@ pharo-agent image list
|
|
|
85
85
|
pharo-agent image add dev --template '<launcher template name>'
|
|
86
86
|
\`\`\`
|
|
87
87
|
|
|
88
|
-
\`image add\` makes the first registered image
|
|
89
|
-
<id>\`
|
|
90
|
-
declaration. Close an image before
|
|
88
|
+
\`image add\` makes the first registered image the default. \`pharo-agent image
|
|
89
|
+
use <id>\` changes the default, and \`pharo-agent image remove <id>\` drops a
|
|
90
|
+
declaration. Close an image before removing its declaration.
|
|
91
91
|
|
|
92
92
|
Use \`pharo-agent open\` before live-image work and edit projected source under
|
|
93
93
|
\`.pharo-agent/mounts\`. Use \`pharo-agent pharo list\` to inspect the complete
|
|
@@ -95,6 +95,12 @@ live MCP tool surface and \`pharo-agent pharo <tool> [arguments-json]\` to call
|
|
|
95
95
|
a tool. \`pharo-agent mcp\` exposes the image as a command-scoped stdio MCP
|
|
96
96
|
server.
|
|
97
97
|
|
|
98
|
+
Several images can be open at once. Every image-facing command takes
|
|
99
|
+
\`--image <id>\`; without it the default image answers. \`pharo-agent open
|
|
100
|
+
--image <id>\` opens another image without closing the first,
|
|
101
|
+
\`pharo-agent status\` lists what is open, and \`pharo-agent close\` closes
|
|
102
|
+
everything this project opened.
|
|
103
|
+
|
|
98
104
|
Image, Tonel, Iceberg, disk Git, and host processes are separate states.
|
|
99
105
|
|
|
100
106
|
## Read first
|
package/src/live.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { closeProject, openProject } from "@evref-bl/plexus";
|
|
4
|
-
import {
|
|
5
|
-
import { configuredActiveImageId } from "./project.js";
|
|
4
|
+
import { callImageTool } from "./gateway.js";
|
|
6
5
|
import {
|
|
7
|
-
|
|
8
|
-
clearActive,
|
|
9
|
-
readActive,
|
|
6
|
+
defaultImageId,
|
|
10
7
|
readConfig,
|
|
8
|
+
readOpenImages,
|
|
9
|
+
resolveImageId,
|
|
11
10
|
resolveMountPath,
|
|
12
11
|
selectRunningImage,
|
|
13
12
|
smalltalkString,
|
|
14
|
-
|
|
13
|
+
writeOpenImages,
|
|
15
14
|
} from "./runtime.js";
|
|
16
15
|
|
|
17
16
|
export const pifsRelease = "v0.4.0";
|
|
@@ -37,52 +36,98 @@ export function resultPrintString(result) {
|
|
|
37
36
|
return undefined;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
export async function openAgentImage(projectRoot) {
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
export async function openAgentImage(projectRoot, imageId) {
|
|
40
|
+
const resolved = resolveImageId(projectRoot, imageId);
|
|
41
|
+
const openImages = readOpenImages(projectRoot);
|
|
42
|
+
if (openImages[resolved]) {
|
|
43
|
+
return { alreadyOpen: true, ...openImages[resolved] };
|
|
43
44
|
}
|
|
44
|
-
const
|
|
45
|
-
const opened = await openProject({ projectRoot, imageIds: [configuredImageId] });
|
|
45
|
+
const opened = await openProject({ projectRoot, imageIds: [resolved] });
|
|
46
46
|
if (!opened.ok || opened.failures.length > 0) {
|
|
47
47
|
throw new Error(opened.failures.map((failure) => failure.message).join("\n") || "PLexus could not open the project image.");
|
|
48
48
|
}
|
|
49
|
-
const { image, mcpServerId } = selectRunningImage(opened.state,
|
|
49
|
+
const { image, mcpServerId } = selectRunningImage(opened.state, resolved);
|
|
50
50
|
const mountPath = resolveMountPath(projectRoot, readConfig(projectRoot), image.id);
|
|
51
51
|
fs.mkdirSync(path.dirname(mountPath), { recursive: true });
|
|
52
|
-
const
|
|
52
|
+
const open = {
|
|
53
53
|
statePath: opened.statePath,
|
|
54
54
|
imageId: image.id,
|
|
55
55
|
mcpServerId,
|
|
56
56
|
mountPath,
|
|
57
57
|
};
|
|
58
|
-
const result = await
|
|
58
|
+
const result = await callImageTool(projectRoot, open, "image_evaluate", {
|
|
59
59
|
code: pifsStartCode(mountPath, image.imageName),
|
|
60
60
|
});
|
|
61
|
-
const
|
|
62
|
-
if (!Number.isInteger(
|
|
61
|
+
const projectionPort = Number(resultPrintString(result));
|
|
62
|
+
if (!Number.isInteger(projectionPort) || projectionPort < 1) {
|
|
63
63
|
throw new Error("PIFS started but did not report its projection port.");
|
|
64
64
|
}
|
|
65
|
-
|
|
66
|
-
return { imageId: image.id, mountPath, projectionPort
|
|
65
|
+
writeOpenImages(projectRoot, { ...openImages, [resolved]: { ...open, projectionPort } });
|
|
66
|
+
return { imageId: image.id, mountPath, projectionPort };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
const
|
|
69
|
+
async function closeOneImage(projectRoot, openImages, imageId) {
|
|
70
|
+
const open = openImages[imageId];
|
|
71
71
|
let projectionError;
|
|
72
72
|
try {
|
|
73
|
-
await
|
|
74
|
-
code: `PIFSServer stopOn: ${
|
|
73
|
+
await callImageTool(projectRoot, open, "image_evaluate", {
|
|
74
|
+
code: `PIFSServer stopOn: ${open.projectionPort}`,
|
|
75
75
|
});
|
|
76
76
|
} catch (error) {
|
|
77
77
|
projectionError = error;
|
|
78
78
|
}
|
|
79
|
-
const closed = await closeProject({ projectRoot, imageIds: [
|
|
79
|
+
const closed = await closeProject({ projectRoot, imageIds: [imageId] });
|
|
80
80
|
if (!closed.ok || closed.failures.length > 0) {
|
|
81
81
|
throw new Error(closed.failures.map((failure) => failure.message).join("\n") || "PLexus could not close the project image.");
|
|
82
82
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
return { stoppedImages: closed.stoppedImages.map((image) => image.id), projectionError };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function closeAgentImage(projectRoot, imageId) {
|
|
87
|
+
const openImages = readOpenImages(projectRoot);
|
|
88
|
+
const identifiers = imageId === undefined
|
|
89
|
+
? Object.keys(openImages)
|
|
90
|
+
: [resolveImageId(projectRoot, imageId)];
|
|
91
|
+
if (identifiers.length === 0) {
|
|
92
|
+
throw new Error("No open pharo-agent image.");
|
|
93
|
+
}
|
|
94
|
+
for (const identifier of identifiers) {
|
|
95
|
+
if (!openImages[identifier]) {
|
|
96
|
+
throw new Error(`Image ${identifier} is not open.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const stoppedImages = [];
|
|
101
|
+
const projectionErrors = [];
|
|
102
|
+
const remaining = { ...openImages };
|
|
103
|
+
for (const identifier of identifiers) {
|
|
104
|
+
const closed = await closeOneImage(projectRoot, openImages, identifier);
|
|
105
|
+
delete remaining[identifier];
|
|
106
|
+
writeOpenImages(projectRoot, remaining);
|
|
107
|
+
stoppedImages.push(...closed.stoppedImages);
|
|
108
|
+
if (closed.projectionError) {
|
|
109
|
+
projectionErrors.push(`${identifier}: ${closed.projectionError.message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (projectionErrors.length > 0) {
|
|
113
|
+
throw new Error(`Images closed, but PIFS cleanup failed: ${projectionErrors.join("; ")}`);
|
|
86
114
|
}
|
|
87
|
-
return { stoppedImages
|
|
115
|
+
return { stoppedImages };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function agentImageStatus(projectRoot) {
|
|
119
|
+
const openImages = readOpenImages(projectRoot);
|
|
120
|
+
let defaultImage;
|
|
121
|
+
try {
|
|
122
|
+
defaultImage = defaultImageId(projectRoot);
|
|
123
|
+
} catch {
|
|
124
|
+
defaultImage = undefined;
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
...(defaultImage ? { defaultImage } : {}),
|
|
128
|
+
images: Object.values(openImages).map((open) => ({
|
|
129
|
+
...open,
|
|
130
|
+
isDefault: open.imageId === defaultImage,
|
|
131
|
+
})),
|
|
132
|
+
};
|
|
88
133
|
}
|
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.
|
|
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.
|
|
16
|
+
throw new Error("No plexus.json found above the current directory");
|
|
17
17
|
}
|
|
18
18
|
directory = parent;
|
|
19
19
|
}
|
|
@@ -71,18 +71,3 @@ export function configuredImages(projectRoot) {
|
|
|
71
71
|
const config = readProjectConfig(projectRoot);
|
|
72
72
|
return Array.isArray(config.images) ? config.images : [];
|
|
73
73
|
}
|
|
74
|
-
|
|
75
|
-
export function configuredActiveImageId(projectRoot) {
|
|
76
|
-
const images = configuredImages(projectRoot).filter((image) => image?.active);
|
|
77
|
-
if (images.length === 0) {
|
|
78
|
-
throw new Error(
|
|
79
|
-
"No active image in plexus.project.json. Register one with pharo-agent image add <id> --template <name>.",
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
if (images.length !== 1 || typeof images[0].id !== "string") {
|
|
83
|
-
throw new Error(
|
|
84
|
-
`pharo-agent needs exactly one active image in plexus.project.json; found ${images.length}.`,
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
return images[0].id;
|
|
88
|
-
}
|
package/src/runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { pluginDirectory } from "./project.js";
|
|
3
|
+
import { configuredImages, pluginDirectory } from "./project.js";
|
|
4
4
|
|
|
5
5
|
export function runtimeDirectory(projectRoot) {
|
|
6
6
|
return path.join(pluginDirectory(projectRoot), "runtime");
|
|
@@ -18,25 +18,79 @@ export function readJson(filePath) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export function configPath(projectRoot) {
|
|
22
|
+
return path.join(pluginDirectory(projectRoot), "config.json");
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
export function readConfig(projectRoot) {
|
|
22
|
-
return readJson(
|
|
26
|
+
return readJson(configPath(projectRoot));
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
export function
|
|
29
|
+
export function writeConfig(projectRoot, config) {
|
|
30
|
+
fs.writeFileSync(configPath(projectRoot), `${JSON.stringify(config, null, 2)}\n`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readOpenImages(projectRoot) {
|
|
26
34
|
const filePath = activePath(projectRoot);
|
|
27
|
-
if (!fs.existsSync(filePath)) {
|
|
28
|
-
|
|
35
|
+
if (!fs.existsSync(filePath)) return {};
|
|
36
|
+
const recorded = readJson(filePath);
|
|
37
|
+
if (recorded && typeof recorded.images === "object" && recorded.images !== null) {
|
|
38
|
+
return recorded.images;
|
|
29
39
|
}
|
|
30
|
-
return
|
|
40
|
+
return typeof recorded?.imageId === "string" ? { [recorded.imageId]: recorded } : {};
|
|
31
41
|
}
|
|
32
42
|
|
|
33
|
-
export function
|
|
43
|
+
export function writeOpenImages(projectRoot, images) {
|
|
44
|
+
if (Object.keys(images).length === 0) {
|
|
45
|
+
fs.rmSync(activePath(projectRoot), { force: true });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
34
48
|
fs.mkdirSync(runtimeDirectory(projectRoot), { recursive: true });
|
|
35
|
-
fs.writeFileSync(activePath(projectRoot), `${JSON.stringify(
|
|
49
|
+
fs.writeFileSync(activePath(projectRoot), `${JSON.stringify({ images }, null, 2)}\n`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function defaultImageId(projectRoot) {
|
|
53
|
+
const configured = readConfig(projectRoot).defaultImage;
|
|
54
|
+
const images = configuredImages(projectRoot);
|
|
55
|
+
if (typeof configured === "string") {
|
|
56
|
+
if (!images.some((image) => image.id === configured)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`The default image ${configured} is not configured. Run pharo-agent image use <id>.`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return configured;
|
|
62
|
+
}
|
|
63
|
+
if (images.length === 1 && typeof images[0].id === "string") {
|
|
64
|
+
return images[0].id;
|
|
65
|
+
}
|
|
66
|
+
if (images.length === 0) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
"No image is configured. Register one with pharo-agent image add <id> --template <name>.",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Several images are configured (${images.map((image) => image.id).join(", ")}); `
|
|
73
|
+
+ "choose one with pharo-agent image use <id>, or pass --image <id>.",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function resolveImageId(projectRoot, imageId) {
|
|
78
|
+
if (imageId === undefined) return defaultImageId(projectRoot);
|
|
79
|
+
if (!configuredImages(projectRoot).some((image) => image.id === imageId)) {
|
|
80
|
+
throw new Error(`Image ${imageId} is not configured.`);
|
|
81
|
+
}
|
|
82
|
+
return imageId;
|
|
36
83
|
}
|
|
37
84
|
|
|
38
|
-
export function
|
|
39
|
-
|
|
85
|
+
export function readOpenImage(projectRoot, imageId) {
|
|
86
|
+
const resolved = resolveImageId(projectRoot, imageId);
|
|
87
|
+
const open = readOpenImages(projectRoot)[resolved];
|
|
88
|
+
if (!open) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Image ${resolved} is not open. Run pharo-agent open${imageId ? ` --image ${resolved}` : ""} first.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return open;
|
|
40
94
|
}
|
|
41
95
|
|
|
42
96
|
export function resolveMountPath(projectRoot, config, imageId) {
|
|
@@ -45,12 +99,15 @@ export function resolveMountPath(projectRoot, config, imageId) {
|
|
|
45
99
|
}
|
|
46
100
|
|
|
47
101
|
export function selectRunningImage(state, imageId) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
102
|
+
const running = (state.images ?? []).filter((image) => image.status === "running");
|
|
103
|
+
const images = imageId === undefined
|
|
104
|
+
? running
|
|
105
|
+
: running.filter((image) => image.id === imageId);
|
|
51
106
|
if (images.length !== 1) {
|
|
52
107
|
throw new Error(
|
|
53
|
-
|
|
108
|
+
imageId === undefined
|
|
109
|
+
? `pharo-agent needs exactly one running image; PLexus reported ${images.length}.`
|
|
110
|
+
: `PLexus reports ${images.length} running images with id ${imageId}.`,
|
|
54
111
|
);
|
|
55
112
|
}
|
|
56
113
|
const image = images[0];
|