opencoding-agent 1.0.2 → 1.0.4

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/README.md CHANGED
@@ -5,8 +5,31 @@ opencoding-agent is an OpenCode plugin that provides specialized agents and a su
5
5
  ## Features
6
6
 
7
7
  - **Custom Agents**: Replaces default agents with specialized `plan` and `build` modes.
8
+ - **Dynamic MCP Injection**: Injects powerful Model Context Protocol (MCP) servers into your environment.
8
9
  - **Subagent Catalog**: Browse and install subagents from the [awesome-opencode-subagents](https://github.com/j5hjun/awesome-opencode-subagents) repository.
9
10
 
11
+ ## Built-in MCPs
12
+
13
+ The following MCP servers are automatically injected and granted full access for `opencoding-plan` and `opencoding-build` agents:
14
+
15
+ - **websearch**: Real-time web search powered by Exa AI.
16
+ - **context7**: Official documentation lookup for libraries.
17
+ - **grep_app**: Ultra-fast code search across GitHub repositories.
18
+
19
+ ## Configuration
20
+
21
+ You can customize the plugin by creating a configuration file at `~/.config/opencode/opencoding-agent.json` or `.opencoding-agent.json` in your project root.
22
+
23
+ ### Disable specific MCPs
24
+
25
+ If you want to disable specific MCP servers, add them to the `disabled_mcps` list:
26
+
27
+ ```json
28
+ {
29
+ "disabled_mcps": ["grep_app"]
30
+ }
31
+ ```
32
+
10
33
  ## Installation
11
34
 
12
35
  Add this plugin to your `opencode.json`:
@@ -1 +1 @@
1
- export declare const injectAgents: (config: any) => Promise<void>;
1
+ export declare const injectAgents: (opencodeConfig: any) => Promise<void>;
@@ -1,20 +1,22 @@
1
1
  import { planAgent } from "./plan";
2
2
  import { buildAgent } from "./build";
3
- export const injectAgents = async (config) => {
4
- config.agent = {
5
- ...config.agent,
3
+ import { deepMerge } from "../config";
4
+ export const injectAgents = async (opencodeConfig) => {
5
+ const existingAgents = (opencodeConfig.agent ?? {});
6
+ opencodeConfig.agent = {
7
+ ...existingAgents,
6
8
  // Disable default agents
7
9
  "build": { disable: true },
8
10
  "plan": { disable: true },
9
11
  // Inject our opencoding- prefixed agents
10
- "opencoding-plan": {
11
- ...planAgent,
12
+ "opencoding-plan": deepMerge(planAgent, {
13
+ ...(existingAgents["opencoding-plan"] ?? {}),
12
14
  disable: false
13
- },
14
- "opencoding-build": {
15
- ...buildAgent,
15
+ }),
16
+ "opencoding-build": deepMerge(buildAgent, {
17
+ ...(existingAgents["opencoding-build"] ?? {}),
16
18
  disable: false
17
- }
19
+ })
18
20
  };
19
- config.default_agent = "opencoding-plan";
21
+ opencodeConfig.default_agent = "opencoding-plan";
20
22
  };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Parse a list with wildcard and exclusion syntax.
3
+ * Validates against allAvailable and removes duplicates.
4
+ */
5
+ export declare function parseList(items: string[], allAvailable: string[]): string[];
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Parse a list with wildcard and exclusion syntax.
3
+ * Validates against allAvailable and removes duplicates.
4
+ */
5
+ export function parseList(items, allAvailable) {
6
+ if (!items || items.length === 0) {
7
+ return [];
8
+ }
9
+ const allow = items.filter((i) => !i.startsWith("!"));
10
+ const deny = items.filter((i) => i.startsWith("!")).map((i) => i.slice(1));
11
+ if (deny.includes("*")) {
12
+ return [];
13
+ }
14
+ let result;
15
+ if (allow.includes("*")) {
16
+ result = allAvailable.filter((item) => !deny.includes(item));
17
+ }
18
+ else {
19
+ result = allow.filter((item) => !deny.includes(item) && allAvailable.includes(item));
20
+ }
21
+ return [...new Set(result)];
22
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import { expect, test, describe } from "bun:test";
2
+ import { parseList } from "./mcp-parser";
3
+ describe("parseList", () => {
4
+ const allAvailable = ["a", "b", "c"];
5
+ test("returns empty list for empty items", () => {
6
+ expect(parseList([], allAvailable)).toEqual([]);
7
+ });
8
+ test("returns specific items", () => {
9
+ expect(parseList(["a", "b"], allAvailable)).toEqual(["a", "b"]);
10
+ });
11
+ test("returns all items with *", () => {
12
+ expect(parseList(["*"], allAvailable)).toEqual(["a", "b", "c"]);
13
+ });
14
+ test("excludes specific items", () => {
15
+ expect(parseList(["*", "!b"], allAvailable)).toEqual(["a", "c"]);
16
+ });
17
+ test("excludes all with !*", () => {
18
+ expect(parseList(["a", "!*"], allAvailable)).toEqual([]);
19
+ });
20
+ test("handles only exclusions", () => {
21
+ expect(parseList(["!b"], allAvailable)).toEqual([]);
22
+ });
23
+ });
@@ -1,3 +1,4 @@
1
+ import { z } from "zod";
1
2
  export type AgentInfo = {
2
3
  name: string;
3
4
  description: string;
@@ -6,4 +7,20 @@ export type AgentInfo = {
6
7
  permission: Record<string, string>;
7
8
  prompt?: string;
8
9
  };
10
+ /**
11
+ * opencoding-agent Configuration Schema
12
+ */
13
+ export declare const PluginConfigSchema: z.ZodObject<{
14
+ disabled_mcps: z.ZodOptional<z.ZodArray<z.ZodString>>;
15
+ }, z.core.$strip>;
16
+ export type PluginConfig = z.infer<typeof PluginConfigSchema>;
9
17
  export declare const PLUGIN_NAME = "opencoding-agent";
18
+ /**
19
+ * Loads the plugin configuration from the home directory or project directory.
20
+ * Path: ~/.config/opencode/opencoding-agent.json or <project_dir>/.opencoding-agent.json
21
+ */
22
+ export declare function loadPluginConfig(projectDir?: string): PluginConfig;
23
+ /**
24
+ * Basic deep merge for configuration objects.
25
+ */
26
+ export declare function deepMerge(target: any, source: any): any;
@@ -1 +1,54 @@
1
+ import { z } from "zod";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import * as os from "node:os";
5
+ /**
6
+ * opencoding-agent Configuration Schema
7
+ */
8
+ export const PluginConfigSchema = z.object({
9
+ disabled_mcps: z.array(z.string()).optional(),
10
+ // Add other config items as needed
11
+ });
1
12
  export const PLUGIN_NAME = "opencoding-agent";
13
+ /**
14
+ * Loads the plugin configuration from the home directory or project directory.
15
+ * Path: ~/.config/opencode/opencoding-agent.json or <project_dir>/.opencoding-agent.json
16
+ */
17
+ export function loadPluginConfig(projectDir) {
18
+ const globalConfigPath = path.join(os.homedir(), ".config", "opencode", `${PLUGIN_NAME}.json`);
19
+ const projectConfigPath = projectDir ? path.join(projectDir, `.${PLUGIN_NAME}.json`) : null;
20
+ const configPaths = [projectConfigPath, globalConfigPath].filter(Boolean);
21
+ for (const configPath of configPaths) {
22
+ if (fs.existsSync(configPath)) {
23
+ try {
24
+ const rawContent = fs.readFileSync(configPath, "utf-8");
25
+ const json = JSON.parse(rawContent);
26
+ return PluginConfigSchema.parse(json);
27
+ }
28
+ catch (error) {
29
+ console.error(`[${PLUGIN_NAME}] Failed to load config from ${configPath}:`, error);
30
+ }
31
+ }
32
+ }
33
+ return {};
34
+ }
35
+ /**
36
+ * Basic deep merge for configuration objects.
37
+ */
38
+ export function deepMerge(target, source) {
39
+ if (typeof target !== "object" || target === null || typeof source !== "object" || source === null) {
40
+ return source;
41
+ }
42
+ const result = { ...target };
43
+ for (const key in source) {
44
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
45
+ if (typeof source[key] === "object" && source[key] !== null && !Array.isArray(source[key])) {
46
+ result[key] = deepMerge(target[key] || {}, source[key]);
47
+ }
48
+ else {
49
+ result[key] = source[key];
50
+ }
51
+ }
52
+ }
53
+ return result;
54
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Invalidates the current package by removing its directory from OpenCode's cache.
3
+ * This forces a clean state so OpenCode re-downloads the plugin on next run.
4
+ * @param packageName The name of the package to invalidate.
5
+ */
6
+ export declare function invalidatePackage(packageName?: string): boolean;
@@ -0,0 +1,24 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { CACHE_DIR, PACKAGE_NAME } from './constants';
4
+ /**
5
+ * Invalidates the current package by removing its directory from OpenCode's cache.
6
+ * This forces a clean state so OpenCode re-downloads the plugin on next run.
7
+ * @param packageName The name of the package to invalidate.
8
+ */
9
+ export function invalidatePackage(packageName = PACKAGE_NAME) {
10
+ try {
11
+ const pkgDir = path.join(CACHE_DIR, 'node_modules', packageName);
12
+ if (fs.existsSync(pkgDir)) {
13
+ fs.rmSync(pkgDir, { recursive: true, force: true });
14
+ console.log(`[auto-update] Package cache removed: ${pkgDir}`);
15
+ return true;
16
+ }
17
+ console.log(`[auto-update] Package not found in cache: ${packageName}`);
18
+ return false;
19
+ }
20
+ catch (err) {
21
+ console.error('[auto-update] Failed to invalidate package:', err);
22
+ return false;
23
+ }
24
+ }
@@ -0,0 +1,18 @@
1
+ export interface PackageJson {
2
+ name?: string;
3
+ version?: string;
4
+ [key: string]: unknown;
5
+ }
6
+ export interface NpmDistTags {
7
+ latest: string;
8
+ [key: string]: string;
9
+ }
10
+ export declare function getCachedVersion(): string | null;
11
+ /**
12
+ * Fetches the latest version for a specific channel from the NPM registry.
13
+ */
14
+ export declare function getLatestVersion(channel?: string): Promise<string | null>;
15
+ /**
16
+ * Checks if the plugin is running in local development mode (e.g. from a file path).
17
+ */
18
+ export declare function isLocalDevMode(directory: string): boolean;
@@ -0,0 +1,128 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { INSTALLED_PACKAGE_JSON, NPM_FETCH_TIMEOUT, NPM_REGISTRY_URL, PACKAGE_NAME, USER_OPENCODE_CONFIG, USER_OPENCODE_CONFIG_JSONC } from './constants';
5
+ /**
6
+ * Strips single and multi-line comments from a JSON string.
7
+ */
8
+ function stripJsonComments(text) {
9
+ return text.replace(/\/\/.*|\/\*[\s\S]*?\*\//g, "");
10
+ }
11
+ /**
12
+ * Resolves the installed version from node_modules, with memoization.
13
+ */
14
+ let cachedPackageVersion = null;
15
+ export function getCachedVersion() {
16
+ if (cachedPackageVersion)
17
+ return cachedPackageVersion;
18
+ try {
19
+ if (fs.existsSync(INSTALLED_PACKAGE_JSON)) {
20
+ const content = fs.readFileSync(INSTALLED_PACKAGE_JSON, 'utf-8');
21
+ const pkg = JSON.parse(content);
22
+ if (pkg.version) {
23
+ cachedPackageVersion = pkg.version;
24
+ return pkg.version;
25
+ }
26
+ }
27
+ }
28
+ catch {
29
+ /* empty */
30
+ }
31
+ try {
32
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
33
+ const pkgPath = findPackageJsonUp(currentDir);
34
+ if (pkgPath) {
35
+ const content = fs.readFileSync(pkgPath, 'utf-8');
36
+ const pkg = JSON.parse(content);
37
+ if (pkg.version) {
38
+ cachedPackageVersion = pkg.version;
39
+ return pkg.version;
40
+ }
41
+ }
42
+ }
43
+ catch (err) {
44
+ console.warn('[auto-update] Failed to resolve version from current directory:', err);
45
+ }
46
+ return null;
47
+ }
48
+ /**
49
+ * Recursively searches upwards for a package.json belonging to this plugin.
50
+ */
51
+ function findPackageJsonUp(startPath) {
52
+ try {
53
+ const stat = fs.statSync(startPath);
54
+ let dir = stat.isDirectory() ? startPath : path.dirname(startPath);
55
+ for (let i = 0; i < 10; i++) {
56
+ const pkgPath = path.join(dir, 'package.json');
57
+ if (fs.existsSync(pkgPath)) {
58
+ try {
59
+ const content = fs.readFileSync(pkgPath, 'utf-8');
60
+ const pkg = JSON.parse(content);
61
+ if (pkg.name === PACKAGE_NAME)
62
+ return pkgPath;
63
+ }
64
+ catch {
65
+ /* empty */
66
+ }
67
+ }
68
+ const parent = path.dirname(dir);
69
+ if (parent === dir)
70
+ break;
71
+ dir = parent;
72
+ }
73
+ }
74
+ catch {
75
+ /* empty */
76
+ }
77
+ return null;
78
+ }
79
+ /**
80
+ * Fetches the latest version for a specific channel from the NPM registry.
81
+ */
82
+ export async function getLatestVersion(channel = 'latest') {
83
+ const controller = new AbortController();
84
+ const timeoutId = setTimeout(() => controller.abort(), NPM_FETCH_TIMEOUT);
85
+ try {
86
+ const response = await fetch(NPM_REGISTRY_URL, {
87
+ signal: controller.signal,
88
+ headers: { Accept: 'application/json' },
89
+ });
90
+ if (!response.ok)
91
+ return null;
92
+ const data = (await response.json());
93
+ return data[channel] ?? data.latest ?? null;
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ finally {
99
+ clearTimeout(timeoutId);
100
+ }
101
+ }
102
+ /**
103
+ * Checks if the plugin is running in local development mode (e.g. from a file path).
104
+ */
105
+ export function isLocalDevMode(directory) {
106
+ const configPaths = [
107
+ path.join(directory, '.opencode', 'opencode.json'),
108
+ path.join(directory, '.opencode', 'opencode.jsonc'),
109
+ USER_OPENCODE_CONFIG,
110
+ USER_OPENCODE_CONFIG_JSONC,
111
+ ];
112
+ for (const configPath of configPaths) {
113
+ try {
114
+ if (fs.existsSync(configPath)) {
115
+ const content = fs.readFileSync(configPath, 'utf-8');
116
+ const json = JSON.parse(stripJsonComments(content));
117
+ const plugins = json.plugin ?? [];
118
+ if (plugins.some((p) => p.startsWith('/') || p.startsWith('file://'))) {
119
+ return true;
120
+ }
121
+ }
122
+ }
123
+ catch {
124
+ /* ignore */
125
+ }
126
+ }
127
+ return false;
128
+ }
@@ -0,0 +1,9 @@
1
+ export declare const PACKAGE_NAME = "opencoding-agent";
2
+ export declare const NPM_REGISTRY_URL = "https://registry.npmjs.org/-/package/opencoding-agent/dist-tags";
3
+ export declare const NPM_FETCH_TIMEOUT = 3000;
4
+ /** The directory used by OpenCode to cache node_modules for plugins. */
5
+ export declare const CACHE_DIR: string;
6
+ /** Path to this plugin's package.json within the OpenCode cache. */
7
+ export declare const INSTALLED_PACKAGE_JSON: string;
8
+ export declare const USER_OPENCODE_CONFIG: string;
9
+ export declare const USER_OPENCODE_CONFIG_JSONC: string;
@@ -0,0 +1,23 @@
1
+ import * as os from 'node:os';
2
+ import * as path from 'node:path';
3
+ export const PACKAGE_NAME = 'opencoding-agent';
4
+ export const NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`;
5
+ export const NPM_FETCH_TIMEOUT = 3000;
6
+ function getCacheDir() {
7
+ if (process.platform === 'win32') {
8
+ return path.join(process.env.LOCALAPPDATA ?? os.homedir(), 'opencode');
9
+ }
10
+ return path.join(os.homedir(), '.cache', 'opencode');
11
+ }
12
+ /** The directory used by OpenCode to cache node_modules for plugins. */
13
+ export const CACHE_DIR = getCacheDir();
14
+ /** Path to this plugin's package.json within the OpenCode cache. */
15
+ export const INSTALLED_PACKAGE_JSON = path.join(CACHE_DIR, 'node_modules', PACKAGE_NAME, 'package.json');
16
+ function getConfigDir() {
17
+ const userConfigDir = process.env.XDG_CONFIG_HOME
18
+ ? process.env.XDG_CONFIG_HOME
19
+ : path.join(os.homedir(), '.config');
20
+ return path.join(userConfigDir, 'opencode');
21
+ }
22
+ export const USER_OPENCODE_CONFIG = path.join(getConfigDir(), 'opencode.json');
23
+ export const USER_OPENCODE_CONFIG_JSONC = path.join(getConfigDir(), 'opencode.jsonc');
@@ -0,0 +1,15 @@
1
+ import type { PluginInput } from '@opencode-ai/plugin';
2
+ /**
3
+ * Creates an OpenCode hook that checks for plugin updates and invalidates cache if available.
4
+ *
5
+ * @param ctx The plugin input context.
6
+ * @returns A hook object for the session.created event.
7
+ */
8
+ export declare function createAutoUpdateHook(ctx: PluginInput): {
9
+ event: ({ event }: {
10
+ event: {
11
+ type: string;
12
+ properties?: unknown;
13
+ };
14
+ }) => void;
15
+ };
@@ -0,0 +1,52 @@
1
+ import { getCachedVersion, getLatestVersion, isLocalDevMode } from './checker';
2
+ import { invalidatePackage } from './cache';
3
+ /**
4
+ * Creates an OpenCode hook that checks for plugin updates and invalidates cache if available.
5
+ *
6
+ * @param ctx The plugin input context.
7
+ * @returns A hook object for the session.created event.
8
+ */
9
+ export function createAutoUpdateHook(ctx) {
10
+ let hasChecked = false;
11
+ return {
12
+ event: ({ event }) => {
13
+ // Only run once per process when a session is created
14
+ if (event.type !== 'session.created')
15
+ return;
16
+ if (hasChecked)
17
+ return;
18
+ hasChecked = true;
19
+ // Run check in the background
20
+ setTimeout(async () => {
21
+ // Skip check if in local development mode
22
+ if (isLocalDevMode(ctx.directory)) {
23
+ console.log('[auto-update] Local development mode detected. Skipping check.');
24
+ return;
25
+ }
26
+ const currentVersion = getCachedVersion();
27
+ const latestVersion = await getLatestVersion();
28
+ if (!currentVersion || !latestVersion) {
29
+ console.log('[auto-update] Could not determine versions. Skipping.');
30
+ return;
31
+ }
32
+ if (currentVersion !== latestVersion) {
33
+ console.log(`[auto-update] Update available: ${currentVersion} → ${latestVersion}`);
34
+ // Show toast notification
35
+ ctx.client.tui.showToast({
36
+ body: {
37
+ title: 'opencoding-agent Update!',
38
+ message: `v${latestVersion} is available. Restart OpenCode to apply.`,
39
+ variant: 'info',
40
+ duration: 8000,
41
+ },
42
+ }).catch(() => { });
43
+ // Invalidate cache
44
+ invalidatePackage();
45
+ }
46
+ else {
47
+ console.log(`[auto-update] Already on latest version: ${currentVersion}`);
48
+ }
49
+ }, 0);
50
+ },
51
+ };
52
+ }
@@ -3,6 +3,7 @@ import type { Plugin } from "@opencode-ai/plugin";
3
3
  * opencoding-agent Plugin
4
4
  *
5
5
  * Replaces default OpenCode agents with core-identical 'plan' and 'build' modes.
6
+ * Now with dynamic MCP injection!
6
7
  */
7
8
  declare const OpencodingAgentPlugin: Plugin;
8
9
  export default OpencodingAgentPlugin;
package/dist/src/index.js CHANGED
@@ -1,20 +1,70 @@
1
1
  import { injectAgents } from "./agents";
2
2
  import { catalogTools } from "./tools/catalog";
3
+ import { createBuiltinMcps } from "./mcp";
4
+ import { loadPluginConfig } from "./config";
5
+ import { createAutoUpdateHook } from "./hooks/auto-update";
3
6
  /**
4
7
  * opencoding-agent Plugin
5
8
  *
6
9
  * Replaces default OpenCode agents with core-identical 'plan' and 'build' modes.
10
+ * Now with dynamic MCP injection!
7
11
  */
8
12
  const OpencodingAgentPlugin = async (ctx) => {
13
+ const pluginConfig = loadPluginConfig(ctx.directory);
14
+ const mcps = createBuiltinMcps(pluginConfig.disabled_mcps);
15
+ const mcpNames = Object.keys(mcps);
16
+ // Initialize auto-update hook
17
+ const autoUpdateHook = createAutoUpdateHook(ctx);
9
18
  return {
19
+ name: "opencoding-agent",
10
20
  // Config hook: Injected once during initialization
11
- config: async (config) => {
12
- await injectAgents(config);
21
+ // Note: 'any' is used because the Config type is not exported by the plugin SDK
22
+ config: async (opencodeConfig) => {
23
+ // 1. Inject specialized agents (opencoding-plan, opencoding-build)
24
+ await injectAgents(opencodeConfig);
25
+ // 2. Merge MCP configs (careful not to overwrite user settings)
26
+ if (!opencodeConfig.mcp) {
27
+ opencodeConfig.mcp = { ...mcps };
28
+ }
29
+ else {
30
+ const existingMcp = opencodeConfig.mcp;
31
+ for (const [name, config] of Object.entries(mcps)) {
32
+ if (!(name in existingMcp)) {
33
+ existingMcp[name] = config;
34
+ }
35
+ }
36
+ }
37
+ // 3. Grant full permissions to opencoding- agents for these MCPs
38
+ const agentsToGrant = ["opencoding-plan", "opencoding-build"];
39
+ const agentConfig = opencodeConfig.agent;
40
+ agentsToGrant.forEach((agentName) => {
41
+ const agent = agentConfig[agentName];
42
+ if (!agent)
43
+ return;
44
+ if (!agent.permission) {
45
+ agent.permission = {};
46
+ }
47
+ for (const mcpName of mcpNames) {
48
+ // MCP tools are prefixed with sanitized mcp server name
49
+ const sanitizedMcpName = mcpName.replace(/[^a-zA-Z0-9_-]/g, "_");
50
+ const permissionKey = `${sanitizedMcpName}_*`;
51
+ // Force allow unless already defined
52
+ if (!(permissionKey in agent.permission)) {
53
+ agent.permission[permissionKey] = "allow";
54
+ }
55
+ }
56
+ });
57
+ },
58
+ // Session events
59
+ event: async (input) => {
60
+ await autoUpdateHook.event(input);
13
61
  },
14
62
  // Register custom tools
15
63
  tool: {
16
- ...catalogTools
17
- }
64
+ ...catalogTools,
65
+ },
66
+ // Register MCPs
67
+ mcp: mcps,
18
68
  };
19
69
  };
20
70
  export default OpencodingAgentPlugin;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { describe, expect, test, mock } from "bun:test";
2
+ import OpencodingAgentPlugin from "./index";
3
+ // Mock config load for test
4
+ mock.module("./config", () => {
5
+ return {
6
+ loadPluginConfig: () => ({ disabled_mcps: [] }),
7
+ PLUGIN_NAME: "opencoding-agent"
8
+ };
9
+ });
10
+ describe("OpencodingAgentPlugin", () => {
11
+ const mockCtx = {
12
+ directory: "/mock/dir"
13
+ };
14
+ test("config hook injects agents and mcps and permissions", async () => {
15
+ const plugin = await OpencodingAgentPlugin(mockCtx);
16
+ const opencodeConfig = {
17
+ agent: {},
18
+ mcp: {}
19
+ };
20
+ if (plugin.config) {
21
+ await plugin.config(opencodeConfig);
22
+ }
23
+ // Agents should be injected
24
+ expect(opencodeConfig.agent["opencoding-plan"]).toBeDefined();
25
+ expect(opencodeConfig.agent["opencoding-build"]).toBeDefined();
26
+ // MCPs should be merged
27
+ expect(opencodeConfig.mcp["websearch"]).toBeDefined();
28
+ expect(opencodeConfig.mcp["context7"]).toBeDefined();
29
+ expect(opencodeConfig.mcp["grep_app"]).toBeDefined();
30
+ // Permissions should be granted to opencoding- agents
31
+ expect(opencodeConfig.agent["opencoding-plan"].permission["websearch_*"]).toBe("allow");
32
+ expect(opencodeConfig.agent["opencoding-build"].permission["websearch_*"]).toBe("allow");
33
+ });
34
+ });
@@ -0,0 +1,6 @@
1
+ import type { RemoteMcpConfig } from './types';
2
+ /**
3
+ * Context7 - official documentation lookup for libraries
4
+ * @see https://context7.com
5
+ */
6
+ export declare const context7: RemoteMcpConfig;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Context7 - official documentation lookup for libraries
3
+ * @see https://context7.com
4
+ */
5
+ export const context7 = {
6
+ type: 'remote',
7
+ url: 'https://mcp.context7.com/mcp',
8
+ headers: process.env.CONTEXT7_API_KEY
9
+ ? { CONTEXT7_API_KEY: process.env.CONTEXT7_API_KEY }
10
+ : undefined,
11
+ oauth: false,
12
+ };
@@ -0,0 +1,6 @@
1
+ import type { RemoteMcpConfig } from './types';
2
+ /**
3
+ * grep.app - ultra-fast code search across GitHub repositories
4
+ * @see https://grep.app
5
+ */
6
+ export declare const grep_app: RemoteMcpConfig;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * grep.app - ultra-fast code search across GitHub repositories
3
+ * @see https://grep.app
4
+ */
5
+ export const grep_app = {
6
+ type: 'remote',
7
+ url: 'https://mcp.grep.app',
8
+ oauth: false,
9
+ };
@@ -0,0 +1,7 @@
1
+ import type { McpConfig } from './types';
2
+ export type { LocalMcpConfig, McpConfig, RemoteMcpConfig } from './types';
3
+ /**
4
+ * Creates MCP configurations, excluding disabled ones.
5
+ * Supports wildcard (*) and exclusion (!) syntax via parseList.
6
+ */
7
+ export declare function createBuiltinMcps(disabledMcps?: readonly string[]): Record<string, McpConfig>;
@@ -0,0 +1,21 @@
1
+ import { context7 } from './context7';
2
+ import { grep_app } from './grep-app';
3
+ import { websearch } from './websearch';
4
+ import { parseList } from '../config/mcp-parser';
5
+ const allBuiltinMcps = {
6
+ websearch,
7
+ context7,
8
+ grep_app,
9
+ };
10
+ /**
11
+ * Creates MCP configurations, excluding disabled ones.
12
+ * Supports wildcard (*) and exclusion (!) syntax via parseList.
13
+ */
14
+ export function createBuiltinMcps(disabledMcps = []) {
15
+ const allNames = Object.keys(allBuiltinMcps);
16
+ // By default, all builtin MCPs are allowed.
17
+ // We prepend '*' to the list and then apply the disabled list as exclusions.
18
+ const items = ["*", ...disabledMcps.map(name => `!${name}`)];
19
+ const enabledNames = parseList(items, allNames);
20
+ return Object.fromEntries(Object.entries(allBuiltinMcps).filter(([name]) => enabledNames.includes(name)));
21
+ }
@@ -0,0 +1,12 @@
1
+ export type RemoteMcpConfig = {
2
+ type: 'remote';
3
+ url: string;
4
+ headers?: Record<string, string>;
5
+ oauth?: false;
6
+ };
7
+ export type LocalMcpConfig = {
8
+ type: 'local';
9
+ command: string[];
10
+ environment?: Record<string, string>;
11
+ };
12
+ export type McpConfig = RemoteMcpConfig | LocalMcpConfig;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { RemoteMcpConfig } from './types';
2
+ /**
3
+ * Exa AI web search - real-time web search
4
+ * @see https://exa.ai
5
+ */
6
+ export declare const websearch: RemoteMcpConfig;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Exa AI web search - real-time web search
3
+ * @see https://exa.ai
4
+ */
5
+ export const websearch = {
6
+ type: 'remote',
7
+ url: 'https://mcp.exa.ai/mcp?tools=web_search_exa',
8
+ headers: process.env.EXA_API_KEY
9
+ ? { 'x-api-key': process.env.EXA_API_KEY }
10
+ : undefined,
11
+ oauth: false,
12
+ };
@@ -1,33 +1,2 @@
1
- import { z } from "zod";
2
- export declare const catalogTools: {
3
- "subagent-catalog:list": {
4
- description: string;
5
- args: {};
6
- execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
7
- };
8
- "subagent-catalog:search": {
9
- description: string;
10
- args: {
11
- query: z.ZodString;
12
- category: z.ZodOptional<z.ZodString>;
13
- };
14
- execute(args: {
15
- query: string;
16
- category?: string | undefined;
17
- }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
18
- };
19
- "subagent-catalog:fetch": {
20
- description: string;
21
- args: {
22
- name: z.ZodString;
23
- scope: z.ZodDefault<z.ZodEnum<{
24
- global: "global";
25
- local: "local";
26
- }>>;
27
- };
28
- execute(args: {
29
- name: string;
30
- scope: "global" | "local";
31
- }, context: import("@opencode-ai/plugin").ToolContext): Promise<string>;
32
- };
33
- };
1
+ import { type ToolDefinition } from "@opencode-ai/plugin";
2
+ export declare const catalogTools: Record<string, ToolDefinition>;
@@ -40,7 +40,8 @@ export const catalogTools = {
40
40
  query: z.string().describe("Search term"),
41
41
  category: z.string().optional().describe("Filter by category")
42
42
  },
43
- execute: async ({ query, category }) => {
43
+ execute: async (args) => {
44
+ const { query, category } = args;
44
45
  try {
45
46
  const url = `${REPO_URL}/catalog.json?t=${Date.now()}`;
46
47
  const response = await fetch(url);
@@ -75,7 +76,8 @@ export const catalogTools = {
75
76
  name: z.string().describe("Name of the agent to fetch"),
76
77
  scope: z.enum(["global", "local"]).default("global").describe("Installation scope (global: all projects, local: current project only)")
77
78
  },
78
- execute: async ({ name, scope }, { directory }) => {
79
+ execute: async (args, { directory }) => {
80
+ const { name, scope } = args;
79
81
  try {
80
82
  const catalogUrl = `${REPO_URL}/catalog.json?t=${Date.now()}`;
81
83
  const catalogResponse = await fetch(catalogUrl);
package/package.json CHANGED
@@ -1,10 +1,17 @@
1
1
  {
2
2
  "name": "opencoding-agent",
3
- "version": "1.0.2",
4
- "description": "OpenCode specialized agents and subagent catalog manager",
5
- "type": "module",
3
+ "version": "1.0.4",
6
4
  "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
5
+ "devDependencies": {
6
+ "@opencode-ai/plugin": "^1.0.0",
7
+ "@types/bun": "^1.2.0",
8
+ "@types/node": "^22.0.0",
9
+ "typescript": "^5.7.0"
10
+ },
11
+ "peerDependencies": {
12
+ "@opencode-ai/plugin": "^1.0.0"
13
+ },
14
+ "description": "OpenCode specialized agents and subagent catalog manager",
8
15
  "files": [
9
16
  "dist",
10
17
  "README.md",
@@ -16,21 +23,14 @@
16
23
  "plugin"
17
24
  ],
18
25
  "license": "MIT",
19
- "peerDependencies": {
20
- "@opencode-ai/plugin": "^1.0.0"
21
- },
22
- "devDependencies": {
23
- "@opencode-ai/plugin": "^1.0.0",
24
- "@types/bun": "^1.2.0",
25
- "@types/node": "^22.0.0",
26
- "typescript": "^5.7.0"
26
+ "publishConfig": {
27
+ "access": "public"
27
28
  },
28
29
  "scripts": {
29
30
  "clean": "rm -rf dist",
30
31
  "build": "npm run clean && tsc",
31
32
  "prepublishOnly": "npm run build"
32
33
  },
33
- "publishConfig": {
34
- "access": "public"
35
- }
34
+ "type": "module",
35
+ "types": "dist/index.d.ts"
36
36
  }