regent-code 3.0.0 → 3.0.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.
@@ -1,11 +1,25 @@
1
1
  # Installation
2
2
 
3
+ ## One-command install
4
+
5
+ If the package is published to npm, both the plugin and the MCP server install
6
+ with a single command against any existing `opencode.json` / `opencode.jsonc`:
7
+
8
+ ```bash
9
+ npx -y regent-code@3.0.1 install
10
+ ```
11
+
12
+ Patches the project config (or the global `~/.config/opencode/` config) to add
13
+ `mcp.servers.regent` and the `plugins` entry. Idempotent and non-destructive;
14
+ `--global` forces the user config, `--file <path>` targets an exact file,
15
+ `--help` for options. Restart the OpenCode session afterwards.
16
+
3
17
  ## Add to opencode.jsonc
4
18
 
5
19
  ```jsonc
6
20
  {
7
21
  "$schema": "https://opencode.ai/config.json",
8
- "plugins": ["regent-code@git+https://github.com/nathwn12/regent-code.git#v2.7.1"],
22
+ "plugins": ["regent-code@git+https://github.com/nathwn12/regent-code.git#v3.0.1"],
9
23
  }
10
24
  ```
11
25
 
@@ -18,7 +32,7 @@
18
32
  }
19
33
  ```
20
34
 
21
- The pinned version is recommended. Use the unpinned branch only when you intentionally want the latest changes. The `v2.7.1` git tag must be pushed to GitHub before the pinned spec resolves.
35
+ The pinned version is recommended. Use the unpinned branch only when you intentionally want the latest changes. The `v3.0.1` git tag must be pushed to GitHub before the pinned spec resolves.
22
36
 
23
37
  ## Single-source rule (duplicate plugin ID)
24
38
 
package/README.md CHANGED
@@ -51,14 +51,31 @@ Add Regent to your OpenCode configuration:
51
51
  ```jsonc
52
52
  {
53
53
  "$schema": "https://opencode.ai/config.json",
54
- "plugins": ["regent-code@git+https://github.com/nathwn12/regent-code.git#v2.7.1"],
54
+ "plugins": ["regent-code@git+https://github.com/nathwn12/regent-code.git#v3.0.1"],
55
55
  }
56
56
  ```
57
57
 
58
- This release targets the OpenCode v2 beta plugin API (`@opencode-ai/plugin@0.0.0-beta-18155`); the beta API may change. The `v2.7.1` git tag must be pushed to GitHub before this pinned spec resolves.
58
+ This release targets the OpenCode v2 beta plugin API (`@opencode-ai/plugin@0.0.0-beta-18314`); the beta API may change. The `v3.0.1` git tag must be pushed to GitHub before this pinned spec resolves.
59
59
 
60
60
  > **Windows dev-machine warning (single-source rule):** when this repository is open as an OpenCode project, its own `.opencode/plugins/regent.js` is auto-loaded as a project plugin. Do NOT also pin regent in `opencode.jsonc` on the same machine — two active sources make host plugin reloads fail with `Duplicate plugin ID: regent`, leaving sessions with a torn tool surface and blocking live skill/plugin edits. Either develop unpinned (project plugin only) or pin the repo file directly: `"plugins": ["file:///Q:/PROJECTS/PERSONAL/regent-code/.opencode/plugins/regent.js"]`. One source of truth, always.
61
61
 
62
+ ## One-command install
63
+
64
+ The fastest way to get **both** the plugin and the MCP server on any machine — no cloning, no manual config edits, no local files:
65
+
66
+ ```bash
67
+ npx -y regent-code@3.0.1 install
68
+ ```
69
+
70
+ The installer finds an existing `opencode.json` / `opencode.jsonc` (project config in the current directory first, then the global `~/.config/opencode/` config) and adds both entries:
71
+
72
+ - **MCP server**: `mcp.servers.regent` → runs `["npx", "-y", "regent-code@3.0.1"]`
73
+ - **Plugin**: `plugins` → `regent-code@3.0.1`
74
+
75
+ It is **idempotent and non-destructive** — it only adds or updates regent entries, preserving comments, trailing commas, and every unrelated setting in the file. Re-run it to upgrade the pinned version. Flags: `--global` forces the user config, `--file <path>` targets an exact file, `--help` explains all options. Restart the OpenCode session afterwards — plugin load and MCP connection happen on config load.
76
+
77
+ > On the machine that develops regent-code itself, respect the single-source rule above: do not add a second pin when the repo is open as a project.
78
+
62
79
  ## MCP Server
63
80
 
64
81
  Since v3.0.0, regent-code ships a second distribution alongside the plugin: a Model Context Protocol (MCP) server that exposes the same six tools (`delegate`, `delegate_many`, `research`, `explore`, `changed-files`, `verify`) plus the Regent command and skill corpus as MCP prompts. It is out-of-process and works from any MCP client (Claude Desktop, Cursor, or OpenCode).
@@ -91,7 +108,7 @@ Configure it in OpenCode by adding a local MCP server:
91
108
  "servers": {
92
109
  "regent": {
93
110
  "type": "local",
94
- "command": ["npx", "-y", "regent-code@3.0.0"]
111
+ "command": ["npx", "-y", "regent-code@3.0.1"]
95
112
  }
96
113
  }
97
114
  }
package/mcp/cli.js CHANGED
@@ -1,9 +1,83 @@
1
- // Launch the Regent MCP server. Used as the package `bin` entry so that
2
- // `npx -y regent-code` starts the server over stdio without needing a local
3
- // clone. Keep this file dependency-free and side-effect-only by design.
1
+ // Regent CLI entry (package `bin`).
2
+ // no arguments -> run the MCP server over stdio (that is what a local
3
+ // `mcp.servers` entry in OpenCode invokes)
4
+ // install -> patch an existing opencode.json / opencode.jsonc to
5
+ // register the Regent MCP server AND plugin; idempotent and
6
+ // non-destructive, can create the config when missing
7
+ // anything else -> usage error
4
8
  import { main } from './index.js';
9
+ import { install, InstallError, PLUGIN_SPEC, SERVER_NAME } from './install.js';
5
10
 
6
- main().catch((err) => {
7
- console.error(err);
11
+ const USAGE = `Usage: npx -y ${PLUGIN_SPEC} install [--global] [--file <path>] [--help]
12
+
13
+ Patches an existing opencode.json / opencode.jsonc (V2 OpenCode config) to
14
+ register:
15
+ - MCP server "${SERVER_NAME}" running ${PLUGIN_SPEC}
16
+ - plugin ${PLUGIN_SPEC}
17
+
18
+ Without flags: patches ./opencode.json(c) or ./.opencode/opencode.json(c) when
19
+ present, otherwise the global ~/.config/opencode config, creating it if needed.
20
+
21
+ --global patch (or create) the user-global config instead
22
+ --file P patch the exact file path (wins over --global)
23
+ --help show this help
24
+
25
+ Non-destructive and idempotent: only adds or updates regent entries, leaving
26
+ comments, formatting, and unrelated settings untouched. Restart the OpenCode
27
+ session afterwards — plugin load and MCP connection happen on config load.`;
28
+
29
+ function runInstaller(args) {
30
+ const options = { cwd: process.cwd() };
31
+ for (let i = 0; i < args.length; i++) {
32
+ switch (args[i]) {
33
+ case '--global':
34
+ options.global = true;
35
+ break;
36
+ case '--file':
37
+ if (!args[i + 1]) throw new InstallError('--file requires a path argument');
38
+ options.file = args[++i];
39
+ break;
40
+ case '--help':
41
+ console.log(USAGE);
42
+ process.exit(0);
43
+ break;
44
+ default:
45
+ throw new InstallError(`unknown argument: ${args[i]}\n\n${USAGE}`);
46
+ }
47
+ }
48
+
49
+ const report = install(options);
50
+ const lines = [
51
+ `Installed into: ${report.path} (${report.scope}${report.created ? ', created' : ''})`,
52
+ ` MCP server "${SERVER_NAME}": ${report.mcp}`,
53
+ ` Plugin ${PLUGIN_SPEC}: ${report.plugin}`,
54
+ '',
55
+ ...report.warnings.map((warning) => ` warning: ${warning}`),
56
+ '',
57
+ 'Restart your OpenCode session — the plugin registers on load and the',
58
+ 'MCP server connects on the next config load.',
59
+ ];
60
+ console.log(lines.join('\n'));
61
+ }
62
+
63
+ const args = process.argv.slice(2);
64
+
65
+ if (args[0] === 'install') {
66
+ try {
67
+ runInstaller(args.slice(1));
68
+ } catch (err) {
69
+ if (err instanceof InstallError) {
70
+ console.error(err.message);
71
+ process.exit(1);
72
+ }
73
+ throw err;
74
+ }
75
+ } else if (args.length === 0) {
76
+ main().catch((err) => {
77
+ console.error(err);
78
+ process.exit(1);
79
+ });
80
+ } else {
81
+ console.error(`Unknown arguments: ${args.join(' ')}\n\n${USAGE}`);
8
82
  process.exit(1);
9
- });
83
+ }
package/mcp/index.js CHANGED
@@ -41,7 +41,7 @@ import {
41
41
 
42
42
  import { readPackagePrompts, renderPrompt } from './prompts.js';
43
43
 
44
- const version = '3.0.0';
44
+ const version = '3.0.1';
45
45
 
46
46
  // ── OpenCode client (lazy singleton) ─────────────────────────
47
47
  let clientPromise = null;
package/mcp/install.js CHANGED
@@ -9,13 +9,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
9
9
  import { join, dirname, resolve } from 'node:path';
10
10
  import { homedir } from 'node:os';
11
11
  import { createRequire } from 'node:module';
12
- import {
13
- parseTree,
14
- findNodeAtLocation,
15
- getNodeValue,
16
- modify,
17
- applyEdits,
18
- } from 'jsonc-parser';
12
+ import { parseTree, findNodeAtLocation, getNodeValue, modify, applyEdits } from 'jsonc-parser';
19
13
 
20
14
  const require = createRequire(import.meta.url);
21
15
  const { version } = require('../package.json');
@@ -58,10 +52,20 @@ function findExisting(candidates) {
58
52
  return null;
59
53
  }
60
54
 
55
+ /**
56
+ * @typedef {Object} InstallOptions
57
+ * @property {string} [cwd] Working directory for resolving relative paths and project configs.
58
+ * @property {string} [file] Explicit config path (wins over every other mode).
59
+ * @property {boolean} [global] Force the user-global config.
60
+ */
61
+
61
62
  // Decide which file to patch. Never writes; the installer writes after
62
63
  // resolving. Explicit --file wins, then forced global, then: an existing
63
64
  // project config in cwd, else an existing global config, else the global
64
65
  // location (created on demand).
66
+ /**
67
+ * @param {InstallOptions} [options]
68
+ */
65
69
  export function resolveTarget({ cwd = process.cwd(), file, global = false } = {}) {
66
70
  if (file) return { path: resolve(cwd, file), scope: 'explicit' };
67
71
  if (global) {
@@ -91,7 +95,9 @@ export function patchText(
91
95
  { pluginSpec = PLUGIN_SPEC, serverName = SERVER_NAME, serverConfig = SERVER_CONFIG } = {},
92
96
  ) {
93
97
  const errors = [];
94
- const root = parseTree(text, errors);
98
+ // JSONC by contract: comments are allowed by default, trailing commas must
99
+ // be opted into — both are common in real opencode.json(c) files.
100
+ const root = parseTree(text, errors, { allowTrailingComma: true });
95
101
  if (!root || errors.length) {
96
102
  throw new InstallError(`invalid JSONC: ${errors[0]?.error ?? 'could not parse'}`);
97
103
  }
@@ -101,8 +107,17 @@ export function patchText(
101
107
 
102
108
  const eol = detectEol(text);
103
109
  const formattingOptions = { insertSpaces: true, tabSize: 2, eol };
110
+ /** @type {{ mcp: string, plugin: string, warnings: string[] }} */
104
111
  const report = { mcp: 'unchanged', plugin: 'unchanged', warnings: [] };
105
- let edits = [];
112
+
113
+ // Apply each edit batch against the CURRENT text, never the original:
114
+ // two insertions at the same location (e.g. missing mcp + missing plugins,
115
+ // both inserted before the closing brace) overlap when computed against one
116
+ // shared base text and make jsonc-parser throw "Overlapping edit".
117
+ let current = text;
118
+ const apply = (edits) => {
119
+ if (edits.length) current = applyEdits(current, edits);
120
+ };
106
121
 
107
122
  // ---- MCP server entry ----
108
123
  const mcpNode = findNodeAtLocation(root, ['mcp']);
@@ -115,24 +130,18 @@ export function patchText(
115
130
  if (deepEqual(existing, serverConfig)) {
116
131
  report.mcp = 'unchanged';
117
132
  } else {
118
- edits = edits.concat(
119
- modify(text, ['mcp', 'servers', serverName], serverConfig, { formattingOptions }),
120
- );
133
+ apply(modify(current, ['mcp', 'servers', serverName], serverConfig, { formattingOptions }));
121
134
  report.mcp = 'updated';
122
135
  }
123
136
  } else {
124
- report.warnings.push(
125
- `mcp.servers.${serverName} exists but is not an object; left untouched`,
126
- );
137
+ report.warnings.push(`mcp.servers.${serverName} exists but is not an object; left untouched`);
127
138
  }
128
139
  } else if (mcpNode && mcpNode.type !== 'object') {
129
140
  report.warnings.push('existing "mcp" key is not an object; left untouched');
130
141
  } else if (serversNode && serversNode.type !== 'object') {
131
142
  report.warnings.push('existing "mcp.servers" is not an object; left untouched');
132
143
  } else {
133
- edits = edits.concat(
134
- modify(text, ['mcp', 'servers', serverName], serverConfig, { formattingOptions }),
135
- );
144
+ apply(modify(current, ['mcp', 'servers', serverName], serverConfig, { formattingOptions }));
136
145
  report.mcp = 'added';
137
146
  }
138
147
 
@@ -150,12 +159,11 @@ export function patchText(
150
159
  (entry) => /^file:/.test(entry) && entry.toLowerCase().includes('regent'),
151
160
  );
152
161
  const hasNpmPin =
153
- entries.some((entry) => entry.startsWith('regent-code@') && !/^regent-code@git/.test(entry)) ||
154
- entries.includes('regent-code');
162
+ entries.some(
163
+ (entry) => entry.startsWith('regent-code@') && !/^regent-code@git/.test(entry),
164
+ ) || entries.includes('regent-code');
155
165
 
156
- const next = entries
157
- .filter((entry) => !gitEntries.includes(entry))
158
- .concat(nonString);
166
+ const next = entries.filter((entry) => !gitEntries.includes(entry)).concat(nonString);
159
167
  let reportPlugin;
160
168
  if (hasNpmPin && gitEntries.length === 0) {
161
169
  reportPlugin = 'unchanged';
@@ -164,31 +172,32 @@ export function patchText(
164
172
  }
165
173
  if (!hasNpmPin) next.push(pluginSpec);
166
174
  if (filePin) {
167
- report.warnings.push(
168
- 'regent plugin pinned via file:// (dev loop); entry left untouched',
169
- );
175
+ report.warnings.push('regent plugin pinned via file:// (dev loop); entry left untouched');
170
176
  }
171
177
  if (!deepEqual(next, existing)) {
172
- edits = edits.concat(modify(text, ['plugins'], next, { formattingOptions }));
178
+ apply(modify(current, ['plugins'], next, { formattingOptions }));
173
179
  report.plugin = reportPlugin;
174
180
  }
175
181
  }
176
182
  } else {
177
- edits = edits.concat(modify(text, ['plugins'], [pluginSpec], { formattingOptions }));
183
+ apply(modify(current, ['plugins'], [pluginSpec], { formattingOptions }));
178
184
  report.plugin = 'added';
179
185
  }
180
186
 
181
- return { text: applyEdits(text, edits), report };
187
+ return { text: current, report };
182
188
  }
183
189
 
184
190
  // Full install against the filesystem: resolves the target config, patches it
185
191
  // (creating the file with only the regent entries when none exists), and
186
192
  // returns the report plus target info.
193
+ /**
194
+ * @param {InstallOptions} [options]
195
+ */
187
196
  export function install(options = {}) {
188
197
  const target = resolveTarget(options);
189
198
  const existingText = existsSync(target.path) ? readFileSync(target.path, 'utf8') : null;
190
199
  const base = existingText ?? `{\n}\n`;
191
- const { text, report } = patchText(base, options);
200
+ const { text, report } = patchText(base);
192
201
 
193
202
  if (text !== existingText) {
194
203
  mkdirSync(dirname(target.path), { recursive: true });
@@ -201,4 +210,4 @@ export function install(options = {}) {
201
210
  scope: target.scope,
202
211
  created: existingText === null,
203
212
  };
204
- }
213
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "regent-code",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Agent orchestration for OpenCode. From idea to shipped — zero ceremony. Plugin + MCP server.",
5
5
  "type": "module",
6
6
  "main": ".opencode/plugins/regent.js",
@@ -30,7 +30,7 @@
30
30
  "format": "prettier --write .opencode/plugins/ skills/ mcp/ --no-error-on-unmatched-pattern",
31
31
  "format:check": "prettier --check .opencode/plugins/ skills/ mcp/ --no-error-on-unmatched-pattern",
32
32
  "typecheck": "tsc --noEmit",
33
- "test": "node --test .opencode/tests/regent.test.js .opencode/tests/regent.live-test.js .opencode/tests/regent.v2.test.js .opencode/tests/regent.hybrid.v2.6.1.test.js .opencode/tests/regent.runtime.v2.6.1.test.js mcp/tests/mcp.test.js",
33
+ "test": "node --test .opencode/tests/regent.test.js .opencode/tests/regent.live-test.js .opencode/tests/regent.v2.test.js .opencode/tests/regent.hybrid.v2.6.1.test.js .opencode/tests/regent.runtime.v2.6.1.test.js mcp/tests/mcp.test.js mcp/tests/install.test.js",
34
34
  "verify": "npm run format:check && npm run lint && npm run typecheck && npm test"
35
35
  },
36
36
  "devDependencies": {