diff --git a/README.md b/README.md
index fa2bebf..d30aaea 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,7 @@ The current `golemcore/*` modules in this repository are:
| Plugin | Purpose |
| --- | --- |
| `golemcore/brave-search` | Brave Search web search tool plugin with API-key backed queries. |
+| `golemcore/brain` | GolemCore Brain wiki tool plugin for CRUD operations and optional dynamic relevance search. |
| `golemcore/browser` | Playwright-backed browser automation tool with screenshot support. |
| `golemcore/browserless` | Browserless smart scrape plugin for rendered HTML, markdown, and link extraction. |
| `golemcore/elevenlabs` | ElevenLabs-backed STT/TTS provider plugin. |
diff --git a/golemcore/brain/plugin.yaml b/golemcore/brain/plugin.yaml
new file mode 100644
index 0000000..3c6ee52
--- /dev/null
+++ b/golemcore/brain/plugin.yaml
@@ -0,0 +1,12 @@
+id: golemcore/brain
+provider: golemcore
+name: brain
+version: 1.0.0
+pluginApiVersion: 1
+engineVersion: ">=0.0.0 <1.0.0"
+entrypoint: me.golemcore.plugins.golemcore.brain.BrainPluginBootstrap
+description: "GolemCore Brain wiki plugin for CRUD operations and optional dynamic relevance search."
+sourceUrl: "https://github.com/alexk-dev/golemcore-plugins/tree/main/golemcore/brain"
+license: "Apache-2.0"
+maintainers:
+ - alexk-dev
diff --git a/golemcore/brain/pom.xml b/golemcore/brain/pom.xml
new file mode 100644
index 0000000..61f23e6
--- /dev/null
+++ b/golemcore/brain/pom.xml
@@ -0,0 +1,104 @@
+
+
+ 4.0.0
+
+
+ me.golemcore.plugins
+ golemcore-plugins
+ 1.0.0
+ ../../pom.xml
+
+
+ 1.0.0
+ golemcore-brain-plugin
+ golemcore/brain
+ GolemCore Brain wiki tool plugin for GolemCore
+
+
+ golemcore
+ brain
+ ../../misc/formatter_eclipse.xml
+
+
+
+
+ me.golemcore.plugins
+ golemcore-plugin-extension-api
+ provided
+
+
+ me.golemcore.plugins
+ golemcore-plugin-runtime-api
+ provided
+
+
+ org.projectlombok
+ lombok
+ provided
+
+
+ org.springframework
+ spring-context
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.squareup.okhttp3
+ okhttp-jvm
+ ${okhttp.version}
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ src/main/resources
+
+
+ ${project.basedir}
+
+ plugin.yaml
+
+ META-INF/golemcore
+
+
+
+
+ net.revelc.code.formatter
+ formatter-maven-plugin
+
+
+ maven-shade-plugin
+
+
+ maven-antrun-plugin
+
+
+ copy-plugin-artifact
+ package
+
+
+
+
+
+
+
+ run
+
+
+
+
+
+
+
diff --git a/golemcore/brain/src/main/java/me/golemcore/plugins/golemcore/brain/BrainPluginBootstrap.java b/golemcore/brain/src/main/java/me/golemcore/plugins/golemcore/brain/BrainPluginBootstrap.java
new file mode 100644
index 0000000..16b7ad2
--- /dev/null
+++ b/golemcore/brain/src/main/java/me/golemcore/plugins/golemcore/brain/BrainPluginBootstrap.java
@@ -0,0 +1,22 @@
+package me.golemcore.plugins.golemcore.brain;
+
+import me.golemcore.plugin.api.extension.spi.PluginBootstrap;
+import me.golemcore.plugin.api.extension.spi.PluginDescriptor;
+
+public class BrainPluginBootstrap implements PluginBootstrap {
+
+ @Override
+ public PluginDescriptor descriptor() {
+ return PluginDescriptor.builder()
+ .id("golemcore/brain")
+ .provider("golemcore")
+ .name("brain")
+ .entrypoint(getClass().getName())
+ .build();
+ }
+
+ @Override
+ public Class> configurationClass() {
+ return BrainPluginConfiguration.class;
+ }
+}
diff --git a/golemcore/brain/src/main/java/me/golemcore/plugins/golemcore/brain/BrainPluginConfig.java b/golemcore/brain/src/main/java/me/golemcore/plugins/golemcore/brain/BrainPluginConfig.java
new file mode 100644
index 0000000..c210ace
--- /dev/null
+++ b/golemcore/brain/src/main/java/me/golemcore/plugins/golemcore/brain/BrainPluginConfig.java
@@ -0,0 +1,76 @@
+package me.golemcore.plugins.golemcore.brain;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class BrainPluginConfig {
+
+ private static final int DEFAULT_INTELLISEARCH_LIMIT = 5;
+ private static final int MAX_INTELLISEARCH_LIMIT = 20;
+
+ @Builder.Default
+ private Boolean enabled = false;
+
+ private String baseUrl;
+ private String apiToken;
+
+ @Builder.Default
+ private String defaultSpaceSlug = "default";
+
+ private String dynamicApiSlug;
+
+ @Builder.Default
+ private Integer defaultIntellisearchLimit = DEFAULT_INTELLISEARCH_LIMIT;
+
+ @Builder.Default
+ private Boolean allowWrite = false;
+
+ public void normalize() {
+ if (enabled == null) {
+ enabled = false;
+ }
+ baseUrl = trimToNull(baseUrl);
+ if (baseUrl != null) {
+ baseUrl = stripTrailingSlash(baseUrl);
+ }
+ apiToken = trimToNull(apiToken);
+ defaultSpaceSlug = normalizeDefaultSpace(defaultSpaceSlug);
+ dynamicApiSlug = trimToNull(dynamicApiSlug);
+ if (defaultIntellisearchLimit == null || defaultIntellisearchLimit <= 0) {
+ defaultIntellisearchLimit = DEFAULT_INTELLISEARCH_LIMIT;
+ }
+ if (defaultIntellisearchLimit > MAX_INTELLISEARCH_LIMIT) {
+ defaultIntellisearchLimit = MAX_INTELLISEARCH_LIMIT;
+ }
+ if (allowWrite == null) {
+ allowWrite = false;
+ }
+ }
+
+ private String normalizeDefaultSpace(String value) {
+ String normalized = trimToNull(value);
+ return normalized != null ? normalized : "default";
+ }
+
+ private String stripTrailingSlash(String value) {
+ String stripped = value;
+ while (stripped.endsWith("/")) {
+ stripped = stripped.substring(0, stripped.length() - 1);
+ }
+ return stripped;
+ }
+
+ private String trimToNull(String value) {
+ if (value == null) {
+ return null;
+ }
+ String trimmed = value.trim();
+ return trimmed.isEmpty() ? null : trimmed;
+ }
+}
diff --git a/golemcore/brain/src/main/java/me/golemcore/plugins/golemcore/brain/BrainPluginConfigService.java b/golemcore/brain/src/main/java/me/golemcore/plugins/golemcore/brain/BrainPluginConfigService.java
new file mode 100644
index 0000000..b620e47
--- /dev/null
+++ b/golemcore/brain/src/main/java/me/golemcore/plugins/golemcore/brain/BrainPluginConfigService.java
@@ -0,0 +1,42 @@
+package me.golemcore.plugins.golemcore.brain;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import me.golemcore.plugin.api.runtime.PluginConfigurationService;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+
+@Service
+public class BrainPluginConfigService {
+
+ static final String PLUGIN_ID = "golemcore/brain";
+
+ private final PluginConfigurationService pluginConfigurationService;
+ private final ObjectMapper objectMapper;
+
+ @Autowired
+ public BrainPluginConfigService(PluginConfigurationService pluginConfigurationService) {
+ this.pluginConfigurationService = pluginConfigurationService;
+ this.objectMapper = new ObjectMapper();
+ }
+
+ public BrainPluginConfig getConfig() {
+ Map raw = pluginConfigurationService == null
+ ? Map.of()
+ : pluginConfigurationService.getPluginConfig(PLUGIN_ID);
+ BrainPluginConfig config = raw.isEmpty()
+ ? BrainPluginConfig.builder().build()
+ : objectMapper.convertValue(raw, BrainPluginConfig.class);
+ config.normalize();
+ return config;
+ }
+
+ public void save(BrainPluginConfig config) {
+ config.normalize();
+ Map saved = objectMapper.convertValue(config, new TypeReference<>() {
+ });
+ pluginConfigurationService.savePluginConfig(PLUGIN_ID, saved);
+ }
+}
diff --git a/golemcore/brain/src/main/java/me/golemcore/plugins/golemcore/brain/BrainPluginConfiguration.java b/golemcore/brain/src/main/java/me/golemcore/plugins/golemcore/brain/BrainPluginConfiguration.java
new file mode 100644
index 0000000..d37dc51
--- /dev/null
+++ b/golemcore/brain/src/main/java/me/golemcore/plugins/golemcore/brain/BrainPluginConfiguration.java
@@ -0,0 +1,12 @@
+package me.golemcore.plugins.golemcore.brain;
+
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration(proxyBeanMethods = false)
+@ComponentScan(basePackageClasses = {
+ BrainToolProvider.class,
+ BrainPluginSettingsContributor.class
+})
+public class BrainPluginConfiguration {
+}
diff --git a/golemcore/brain/src/main/java/me/golemcore/plugins/golemcore/brain/BrainPluginSettingsContributor.java b/golemcore/brain/src/main/java/me/golemcore/plugins/golemcore/brain/BrainPluginSettingsContributor.java
new file mode 100644
index 0000000..597ebbd
--- /dev/null
+++ b/golemcore/brain/src/main/java/me/golemcore/plugins/golemcore/brain/BrainPluginSettingsContributor.java
@@ -0,0 +1,176 @@
+package me.golemcore.plugins.golemcore.brain;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import me.golemcore.plugin.api.extension.spi.PluginActionResult;
+import me.golemcore.plugin.api.extension.spi.PluginSettingsCatalogItem;
+import me.golemcore.plugin.api.extension.spi.PluginSettingsContributor;
+import me.golemcore.plugin.api.extension.spi.PluginSettingsField;
+import me.golemcore.plugin.api.extension.spi.PluginSettingsSection;
+import org.springframework.stereotype.Component;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Component
+public class BrainPluginSettingsContributor implements PluginSettingsContributor {
+
+ private static final String SECTION_KEY = "main";
+
+ private final BrainPluginConfigService configService;
+
+ @Autowired
+ public BrainPluginSettingsContributor(BrainPluginConfigService configService) {
+ this.configService = configService;
+ }
+
+ @Override
+ public String getPluginId() {
+ return BrainPluginConfigService.PLUGIN_ID;
+ }
+
+ @Override
+ public List getCatalogItems() {
+ return List.of(PluginSettingsCatalogItem.builder()
+ .pluginId(BrainPluginConfigService.PLUGIN_ID)
+ .pluginName("brain")
+ .provider("golemcore")
+ .sectionKey(SECTION_KEY)
+ .title("GolemCore Brain")
+ .description("GolemCore Brain API credentials and wiki tool behavior.")
+ .blockKey("tools")
+ .blockTitle("Tools")
+ .blockDescription("Tool-specific runtime behavior and integrations")
+ .order(62)
+ .build());
+ }
+
+ @Override
+ public PluginSettingsSection getSection(String sectionKey) {
+ requireSection(sectionKey);
+ BrainPluginConfig config = configService.getConfig();
+ Map values = new LinkedHashMap<>();
+ values.put("enabled", Boolean.TRUE.equals(config.getEnabled()));
+ values.put("baseUrl", config.getBaseUrl());
+ values.put("apiToken", "");
+ values.put("defaultSpaceSlug", config.getDefaultSpaceSlug());
+ values.put("dynamicApiSlug", config.getDynamicApiSlug());
+ values.put("defaultIntellisearchLimit", config.getDefaultIntellisearchLimit());
+ values.put("allowWrite", Boolean.TRUE.equals(config.getAllowWrite()));
+ return PluginSettingsSection.builder()
+ .title("GolemCore Brain")
+ .description("Configure the Brain wiki API tool and optional dynamic relevance endpoint.")
+ .fields(List.of(
+ PluginSettingsField.builder()
+ .key("enabled")
+ .type("boolean")
+ .label("Enable Brain tool")
+ .description("Allow the model to use the GolemCore Brain wiki tool.")
+ .build(),
+ PluginSettingsField.builder()
+ .key("baseUrl")
+ .type("text")
+ .label("Brain Base URL")
+ .description(
+ "Base URL of the GolemCore Brain instance, for example https://brain.example.")
+ .placeholder("https://brain.example")
+ .build(),
+ PluginSettingsField.builder()
+ .key("apiToken")
+ .type("secret")
+ .label("Brain API Token")
+ .description("Optional bearer token generated by Brain API key settings.")
+ .placeholder("Enter API token")
+ .build(),
+ PluginSettingsField.builder()
+ .key("defaultSpaceSlug")
+ .type("text")
+ .label("Default Space Slug")
+ .description("Brain space used when the tool call does not provide space_slug.")
+ .placeholder("default")
+ .build(),
+ PluginSettingsField.builder()
+ .key("dynamicApiSlug")
+ .type("text")
+ .label("Dynamic Relevance Endpoint")
+ .description("Optional Brain dynamic API slug used by intellisearch.")
+ .placeholder("brain-relevance")
+ .build(),
+ PluginSettingsField.builder()
+ .key("defaultIntellisearchLimit")
+ .type("number")
+ .label("Default Intellisearch Limit")
+ .description("Maximum fallback pages to fetch when dynamic endpoint is not configured.")
+ .min(1.0)
+ .max(20.0)
+ .step(1.0)
+ .build(),
+ PluginSettingsField.builder()
+ .key("allowWrite")
+ .type("boolean")
+ .label("Allow Write Operations")
+ .description("Allow create, update, delete, ensure, move, and copy operations.")
+ .build()))
+ .values(values)
+ .build();
+ }
+
+ @Override
+ public PluginSettingsSection saveSection(String sectionKey, Map values) {
+ requireSection(sectionKey);
+ BrainPluginConfig config = configService.getConfig();
+ config.setEnabled(readBoolean(values, "enabled", false));
+ config.setBaseUrl(readString(values, "baseUrl", config.getBaseUrl()));
+ String apiToken = readString(values, "apiToken", null);
+ if (apiToken != null && !apiToken.isBlank()) {
+ config.setApiToken(apiToken);
+ }
+ config.setDefaultSpaceSlug(readString(values, "defaultSpaceSlug", config.getDefaultSpaceSlug()));
+ config.setDynamicApiSlug(readString(values, "dynamicApiSlug", config.getDynamicApiSlug()));
+ config.setDefaultIntellisearchLimit(readInteger(values, "defaultIntellisearchLimit",
+ config.getDefaultIntellisearchLimit()));
+ config.setAllowWrite(readBoolean(values, "allowWrite", false));
+ configService.save(config);
+ return getSection(sectionKey);
+ }
+
+ @Override
+ public PluginActionResult executeAction(String sectionKey, String actionId, Map payload) {
+ requireSection(sectionKey);
+ throw new IllegalArgumentException("Unknown Brain action: " + actionId);
+ }
+
+ private void requireSection(String sectionKey) {
+ if (!SECTION_KEY.equals(sectionKey)) {
+ throw new IllegalArgumentException("Unknown Brain settings section: " + sectionKey);
+ }
+ }
+
+ private boolean readBoolean(Map values, String key, boolean defaultValue) {
+ Object value = values.get(key);
+ return value instanceof Boolean bool ? bool : defaultValue;
+ }
+
+ private int readInteger(Map values, String key, int defaultValue) {
+ Object value = values.get(key);
+ if (value instanceof Number number) {
+ return number.intValue();
+ }
+ if (value instanceof String text && !text.isBlank()) {
+ try {
+ return Integer.parseInt(text);
+ } catch (NumberFormatException ignored) {
+ return defaultValue;
+ }
+ }
+ return defaultValue;
+ }
+
+ private String readString(Map values, String key, String defaultValue) {
+ Object value = values.get(key);
+ if (value instanceof String text) {
+ return text;
+ }
+ return defaultValue;
+ }
+}
diff --git a/golemcore/brain/src/main/java/me/golemcore/plugins/golemcore/brain/BrainService.java b/golemcore/brain/src/main/java/me/golemcore/plugins/golemcore/brain/BrainService.java
new file mode 100644
index 0000000..c9a4e5b
--- /dev/null
+++ b/golemcore/brain/src/main/java/me/golemcore/plugins/golemcore/brain/BrainService.java
@@ -0,0 +1,442 @@
+package me.golemcore.plugins.golemcore.brain;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import me.golemcore.plugin.api.extension.model.ToolFailureKind;
+import me.golemcore.plugin.api.extension.model.ToolResult;
+import okhttp3.HttpUrl;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+public class BrainService {
+
+ private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
+ private static final int DEFAULT_SEARCH_LIMIT = 5;
+
+ private final BrainPluginConfigService configService;
+ private final OkHttpClient httpClient;
+ private final ObjectMapper objectMapper;
+
+ public BrainService(BrainPluginConfigService configService) {
+ this(configService, new OkHttpClient());
+ }
+
+ public BrainService(BrainPluginConfigService configService, OkHttpClient httpClient) {
+ this.configService = configService;
+ this.httpClient = httpClient;
+ this.objectMapper = new ObjectMapper();
+ }
+
+ public boolean isAvailable() {
+ BrainPluginConfig config = configService.getConfig();
+ return Boolean.TRUE.equals(config.getEnabled()) && config.getBaseUrl() != null
+ && !config.getBaseUrl().isBlank();
+ }
+
+ public ToolResult listSpaces() {
+ try {
+ JsonNode node = executeJson("GET", apiUrl("/api/spaces"), null);
+ int count = node.isArray() ? node.size() : 0;
+ return ToolResult.success("Listed " + count + " Brain space(s)", nodeToData(node));
+ } catch (IOException | IllegalStateException exception) {
+ return executionFailure(exception.getMessage());
+ }
+ }
+
+ public ToolResult listTree(String spaceSlug) {
+ String space = resolveSpace(spaceSlug);
+ try {
+ JsonNode node = executeJson("GET", spaceUrl(space, "/tree"), null);
+ return ToolResult.success("Loaded Brain tree for space " + space, nodeToData(node));
+ } catch (IOException | IllegalStateException exception) {
+ return executionFailure(exception.getMessage());
+ }
+ }
+
+ public ToolResult searchPages(String spaceSlug, String query, Integer limit) {
+ String resolvedQuery = requireText(query, "query");
+ String space = resolveSpace(spaceSlug);
+ int resolvedLimit = resolveLimit(limit, DEFAULT_SEARCH_LIMIT);
+ try {
+ JsonNode node = executeJson("GET", searchUrl(space, resolvedQuery, resolvedLimit), null);
+ return ToolResult.success(formatSearchResults(resolvedQuery, node), nodeToData(node));
+ } catch (IOException | IllegalStateException exception) {
+ return executionFailure(exception.getMessage());
+ }
+ }
+
+ public ToolResult intellisearch(
+ String spaceSlug,
+ String context,
+ String query,
+ Integer limit,
+ String dynamicApiSlug,
+ Boolean useDynamicEndpoint) {
+ String resolvedContext = requireText(firstNonBlank(context, query), "context or query");
+ String resolvedQuery = firstNonBlank(query, context);
+ String space = resolveSpace(spaceSlug);
+ int resolvedLimit = resolveLimit(limit, configService.getConfig().getDefaultIntellisearchLimit());
+ String resolvedDynamicApiSlug = trimToNull(dynamicApiSlug);
+ if (resolvedDynamicApiSlug == null) {
+ resolvedDynamicApiSlug = trimToNull(configService.getConfig().getDynamicApiSlug());
+ }
+ boolean shouldUseDynamicEndpoint = !Boolean.FALSE.equals(useDynamicEndpoint) && resolvedDynamicApiSlug != null;
+ if (shouldUseDynamicEndpoint) {
+ return runDynamicIntellisearch(space, resolvedDynamicApiSlug, resolvedContext, resolvedQuery,
+ resolvedLimit);
+ }
+ return fallbackIntellisearch(space, resolvedContext, resolvedQuery, resolvedLimit);
+ }
+
+ public ToolResult readPage(String spaceSlug, String path) {
+ String pagePath = requireText(path, "path");
+ String space = resolveSpace(spaceSlug);
+ try {
+ JsonNode node = executeJson("GET", pageUrl(space, pagePath), null);
+ String content = node.path("content").asText(node.toPrettyString());
+ return ToolResult.success(content, nodeToData(node));
+ } catch (IOException | IllegalStateException exception) {
+ return executionFailure(exception.getMessage());
+ }
+ }
+
+ public ToolResult createPage(String spaceSlug, String parentPath, String title, String slug, String content,
+ String kind) {
+ requireWriteAllowed();
+ String space = resolveSpace(spaceSlug);
+ Map payload = new LinkedHashMap<>();
+ payload.put("parentPath", valueOrEmpty(parentPath));
+ payload.put("title", requireText(title, "title"));
+ payload.put("slug", trimToNull(slug));
+ payload.put("content", valueOrEmpty(content));
+ payload.put("kind", trimToNull(kind) != null ? kind : "PAGE");
+ try {
+ JsonNode node = executeJson("POST", spaceUrl(space, "/pages"), payload);
+ return ToolResult.success("Created Brain page " + node.path("path").asText(""), nodeToData(node));
+ } catch (IOException | IllegalStateException exception) {
+ return executionFailure(exception.getMessage());
+ }
+ }
+
+ public ToolResult updatePage(String spaceSlug, String path, String title, String slug, String content,
+ String revision) {
+ requireWriteAllowed();
+ String pagePath = requireText(path, "path");
+ String space = resolveSpace(spaceSlug);
+ Map payload = new LinkedHashMap<>();
+ payload.put("title", trimToNull(title));
+ payload.put("slug", trimToNull(slug));
+ payload.put("content", valueOrEmpty(content));
+ payload.put("revision", trimToNull(revision));
+ try {
+ JsonNode node = executeJson("PUT", pageUrl(space, pagePath), payload);
+ return ToolResult.success("Updated Brain page " + node.path("path").asText(pagePath), nodeToData(node));
+ } catch (IOException | IllegalStateException exception) {
+ return executionFailure(exception.getMessage());
+ }
+ }
+
+ public ToolResult deletePage(String spaceSlug, String path) {
+ requireWriteAllowed();
+ String pagePath = requireText(path, "path");
+ String space = resolveSpace(spaceSlug);
+ try {
+ executeNoContent("DELETE", pageUrl(space, pagePath), null);
+ return ToolResult.success("Deleted Brain page " + pagePath, Map.of("path", pagePath));
+ } catch (IOException | IllegalStateException exception) {
+ return executionFailure(exception.getMessage());
+ }
+ }
+
+ public ToolResult ensurePage(String spaceSlug, String path, String targetTitle) {
+ requireWriteAllowed();
+ String pagePath = requireText(path, "path");
+ String space = resolveSpace(spaceSlug);
+ Map payload = new LinkedHashMap<>();
+ payload.put("path", pagePath);
+ payload.put("targetTitle", trimToNull(targetTitle));
+ try {
+ JsonNode node = executeJson("POST", spaceUrl(space, "/pages/ensure"), payload);
+ return ToolResult.success("Ensured Brain page " + node.path("path").asText(pagePath), nodeToData(node));
+ } catch (IOException | IllegalStateException exception) {
+ return executionFailure(exception.getMessage());
+ }
+ }
+
+ public ToolResult movePage(String spaceSlug, String path, String targetParentPath, String targetSlug,
+ String beforeSlug) {
+ requireWriteAllowed();
+ String pagePath = requireText(path, "path");
+ String space = resolveSpace(spaceSlug);
+ Map payload = new LinkedHashMap<>();
+ payload.put("targetParentPath", valueOrEmpty(targetParentPath));
+ payload.put("targetSlug", trimToNull(targetSlug));
+ payload.put("beforeSlug", trimToNull(beforeSlug));
+ try {
+ JsonNode node = executeJson("POST", pageActionUrl(space, pagePath, "/page/move"), payload);
+ return ToolResult.success("Moved Brain page " + pagePath, nodeToData(node));
+ } catch (IOException | IllegalStateException exception) {
+ return executionFailure(exception.getMessage());
+ }
+ }
+
+ public ToolResult copyPage(String spaceSlug, String path, String targetParentPath, String targetSlug,
+ String beforeSlug) {
+ requireWriteAllowed();
+ String pagePath = requireText(path, "path");
+ String space = resolveSpace(spaceSlug);
+ Map payload = new LinkedHashMap<>();
+ payload.put("targetParentPath", valueOrEmpty(targetParentPath));
+ payload.put("targetSlug", trimToNull(targetSlug));
+ payload.put("beforeSlug", trimToNull(beforeSlug));
+ try {
+ JsonNode node = executeJson("POST", pageActionUrl(space, pagePath, "/page/copy"), payload);
+ return ToolResult.success("Copied Brain page " + pagePath, nodeToData(node));
+ } catch (IOException | IllegalStateException exception) {
+ return executionFailure(exception.getMessage());
+ }
+ }
+
+ public ToolResult listAssets(String spaceSlug, String path) {
+ String pagePath = requireText(path, "path");
+ String space = resolveSpace(spaceSlug);
+ try {
+ JsonNode node = executeJson("GET", pageActionUrl(space, pagePath, "/pages/assets"), null);
+ return ToolResult.success("Listed Brain assets for " + pagePath, nodeToData(node));
+ } catch (IOException | IllegalStateException exception) {
+ return executionFailure(exception.getMessage());
+ }
+ }
+
+ private ToolResult runDynamicIntellisearch(String space, String dynamicApiSlug, String context, String query,
+ int limit) {
+ Map payload = new LinkedHashMap<>();
+ payload.put("context", context);
+ payload.put("query", query);
+ payload.put("limit", limit);
+ payload.put("task", "Find the most relevant Brain documents for the provided context.");
+ try {
+ JsonNode node = executeJson("POST", dynamicApiUrl(space, dynamicApiSlug), payload);
+ return ToolResult.success("Brain dynamic intellisearch via " + dynamicApiSlug + ": "
+ + compactJson(node), nodeToData(node));
+ } catch (IOException | IllegalStateException exception) {
+ return executionFailure(exception.getMessage());
+ }
+ }
+
+ private ToolResult fallbackIntellisearch(String space, String context, String query, int limit) {
+ try {
+ JsonNode hits = executeJson("GET", searchUrl(space, query, limit), null);
+ List