regent-code 3.0.0
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/.github/workflows/ci.yml +38 -0
- package/.opencode/INSTALL.md +82 -0
- package/.opencode/agents/regent-explore.md +10 -0
- package/.opencode/agents/regent-general.md +8 -0
- package/.opencode/commands/accept.md +15 -0
- package/.opencode/commands/delegate.md +16 -0
- package/.opencode/commands/diagnose.md +19 -0
- package/.opencode/commands/orchestrate.md +22 -0
- package/.opencode/commands/plan.md +20 -0
- package/.opencode/commands/research.md +12 -0
- package/.opencode/commands/review.md +23 -0
- package/.opencode/commands/ship.md +18 -0
- package/.opencode/commands/spec.md +18 -0
- package/.opencode/commands/status.md +13 -0
- package/.opencode/commands/tdd.md +18 -0
- package/.opencode/commands/verify.md +18 -0
- package/.opencode/package.json +6 -0
- package/.opencode/plugins/regent.js +1623 -0
- package/.opencode/skills/code-review/SKILL.md +89 -0
- package/.opencode/skills/diagnose/SKILL.md +118 -0
- package/.opencode/skills/grilling/SKILL.md +59 -0
- package/.opencode/skills/handoff/SKILL.md +61 -0
- package/.opencode/skills/merge-conflicts/SKILL.md +39 -0
- package/.opencode/skills/orchestrator/SKILL.md +206 -0
- package/.opencode/skills/prototype/SKILL.md +40 -0
- package/.opencode/skills/ship/SKILL.md +42 -0
- package/.opencode/skills/spec/SKILL.md +61 -0
- package/.opencode/skills/tdd/SKILL.md +102 -0
- package/.opencode/skills/tickets/SKILL.md +71 -0
- package/.opencode/skills/using-regent/SKILL.md +71 -0
- package/.opencode/skills/verification-before-completion/SKILL.md +82 -0
- package/.opencode/skills/wizard/SKILL.md +45 -0
- package/.opencode/skills/worktrees/SKILL.md +39 -0
- package/.opencode/skills/zoom-out/SKILL.md +38 -0
- package/.prettierignore +2 -0
- package/.prettierrc +7 -0
- package/AGENTS.md +38 -0
- package/CONSTITUTION.md +101 -0
- package/LICENSE +21 -0
- package/README.md +264 -0
- package/docs/contributing.md +86 -0
- package/docs/superpowers/plans/windows-guardrail/plan.md +49 -0
- package/docs/superpowers/plans/windows-guardrail/tasks.md +58 -0
- package/docs/superpowers/specs/2026-06-12-regent-health-audit-design.md +49 -0
- package/docs/superpowers/specs/2026-08-26-windows-guardrail.md +66 -0
- package/eslint.config.js +23 -0
- package/handoff.md +100 -0
- package/mcp/cli.js +9 -0
- package/mcp/index.js +805 -0
- package/mcp/install.js +204 -0
- package/mcp/prompts.js +99 -0
- package/mcp/shared.js +428 -0
- package/package.json +52 -0
- package/tsconfig.json +17 -0
package/mcp/install.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// One-command installer: patches an existing OpenCode config (opencode.json or
|
|
2
|
+
// opencode.jsonc) to register the Regent MCP server AND the Regent plugin.
|
|
3
|
+
//
|
|
4
|
+
// Non-destructive and idempotent: only adds or updates regent entries, and
|
|
5
|
+
// preserves comments, formatting, trailing commas, and every unrelated
|
|
6
|
+
// setting. Handles both bare JSON and JSONC (comments, trailing commas)
|
|
7
|
+
// through jsonc-parser's modify/applyEdits, which edit the text in place.
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
9
|
+
import { join, dirname, resolve } from 'node:path';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import {
|
|
13
|
+
parseTree,
|
|
14
|
+
findNodeAtLocation,
|
|
15
|
+
getNodeValue,
|
|
16
|
+
modify,
|
|
17
|
+
applyEdits,
|
|
18
|
+
} from 'jsonc-parser';
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const { version } = require('../package.json');
|
|
22
|
+
|
|
23
|
+
export const PLUGIN_SPEC = `regent-code@${version}`;
|
|
24
|
+
export const SERVER_NAME = 'regent';
|
|
25
|
+
export const SERVER_CONFIG = Object.freeze({
|
|
26
|
+
type: 'local',
|
|
27
|
+
command: ['npx', '-y', `regent-code@${version}`],
|
|
28
|
+
cwd: '.',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const CONFIG_NAMES = ['opencode.jsonc', 'opencode.json'];
|
|
32
|
+
|
|
33
|
+
export class InstallError extends Error {}
|
|
34
|
+
|
|
35
|
+
export function globalConfigDir() {
|
|
36
|
+
return process.env.XDG_CONFIG_HOME
|
|
37
|
+
? join(process.env.XDG_CONFIG_HOME, 'opencode')
|
|
38
|
+
: join(homedir(), '.config', 'opencode');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function projectCandidates(cwd) {
|
|
42
|
+
return [
|
|
43
|
+
join(cwd, '.opencode', 'opencode.jsonc'),
|
|
44
|
+
join(cwd, '.opencode', 'opencode.json'),
|
|
45
|
+
join(cwd, 'opencode.jsonc'),
|
|
46
|
+
join(cwd, 'opencode.json'),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function globalCandidates() {
|
|
51
|
+
return CONFIG_NAMES.map((name) => join(globalConfigDir(), name));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findExisting(candidates) {
|
|
55
|
+
for (const candidate of candidates) {
|
|
56
|
+
if (existsSync(candidate)) return candidate;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Decide which file to patch. Never writes; the installer writes after
|
|
62
|
+
// resolving. Explicit --file wins, then forced global, then: an existing
|
|
63
|
+
// project config in cwd, else an existing global config, else the global
|
|
64
|
+
// location (created on demand).
|
|
65
|
+
export function resolveTarget({ cwd = process.cwd(), file, global = false } = {}) {
|
|
66
|
+
if (file) return { path: resolve(cwd, file), scope: 'explicit' };
|
|
67
|
+
if (global) {
|
|
68
|
+
const hit = findExisting(globalCandidates());
|
|
69
|
+
return { path: hit ?? join(globalConfigDir(), 'opencode.jsonc'), scope: 'global' };
|
|
70
|
+
}
|
|
71
|
+
const projectHit = findExisting(projectCandidates(cwd));
|
|
72
|
+
if (projectHit) return { path: projectHit, scope: 'project' };
|
|
73
|
+
const globalHit = findExisting(globalCandidates());
|
|
74
|
+
if (globalHit) return { path: globalHit, scope: 'global' };
|
|
75
|
+
return { path: join(globalConfigDir(), 'opencode.jsonc'), scope: 'global' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function detectEol(text) {
|
|
79
|
+
return text.includes('\r\n') ? '\r\n' : '\n';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function deepEqual(a, b) {
|
|
83
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Pure text transform: returns { text, report } with report describing what
|
|
87
|
+
// changed (mcp: added|updated|unchanged, plugin: added|updated|unchanged,
|
|
88
|
+
// warnings: string[]).
|
|
89
|
+
export function patchText(
|
|
90
|
+
text,
|
|
91
|
+
{ pluginSpec = PLUGIN_SPEC, serverName = SERVER_NAME, serverConfig = SERVER_CONFIG } = {},
|
|
92
|
+
) {
|
|
93
|
+
const errors = [];
|
|
94
|
+
const root = parseTree(text, errors);
|
|
95
|
+
if (!root || errors.length) {
|
|
96
|
+
throw new InstallError(`invalid JSONC: ${errors[0]?.error ?? 'could not parse'}`);
|
|
97
|
+
}
|
|
98
|
+
if (root.type !== 'object') {
|
|
99
|
+
throw new InstallError('config root must be an object');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const eol = detectEol(text);
|
|
103
|
+
const formattingOptions = { insertSpaces: true, tabSize: 2, eol };
|
|
104
|
+
const report = { mcp: 'unchanged', plugin: 'unchanged', warnings: [] };
|
|
105
|
+
let edits = [];
|
|
106
|
+
|
|
107
|
+
// ---- MCP server entry ----
|
|
108
|
+
const mcpNode = findNodeAtLocation(root, ['mcp']);
|
|
109
|
+
const serversNode = mcpNode ? findNodeAtLocation(mcpNode, ['servers']) : null;
|
|
110
|
+
const regentNode = serversNode ? findNodeAtLocation(serversNode, [serverName]) : null;
|
|
111
|
+
|
|
112
|
+
if (regentNode) {
|
|
113
|
+
const existing = getNodeValue(regentNode);
|
|
114
|
+
if (existing && typeof existing === 'object' && !Array.isArray(existing)) {
|
|
115
|
+
if (deepEqual(existing, serverConfig)) {
|
|
116
|
+
report.mcp = 'unchanged';
|
|
117
|
+
} else {
|
|
118
|
+
edits = edits.concat(
|
|
119
|
+
modify(text, ['mcp', 'servers', serverName], serverConfig, { formattingOptions }),
|
|
120
|
+
);
|
|
121
|
+
report.mcp = 'updated';
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
report.warnings.push(
|
|
125
|
+
`mcp.servers.${serverName} exists but is not an object; left untouched`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
} else if (mcpNode && mcpNode.type !== 'object') {
|
|
129
|
+
report.warnings.push('existing "mcp" key is not an object; left untouched');
|
|
130
|
+
} else if (serversNode && serversNode.type !== 'object') {
|
|
131
|
+
report.warnings.push('existing "mcp.servers" is not an object; left untouched');
|
|
132
|
+
} else {
|
|
133
|
+
edits = edits.concat(
|
|
134
|
+
modify(text, ['mcp', 'servers', serverName], serverConfig, { formattingOptions }),
|
|
135
|
+
);
|
|
136
|
+
report.mcp = 'added';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---- Plugin entry ----
|
|
140
|
+
const pluginsNode = findNodeAtLocation(root, ['plugins']);
|
|
141
|
+
if (pluginsNode) {
|
|
142
|
+
const existing = getNodeValue(pluginsNode);
|
|
143
|
+
if (!Array.isArray(existing)) {
|
|
144
|
+
report.warnings.push('existing "plugins" is not an array; left untouched');
|
|
145
|
+
} else {
|
|
146
|
+
const entries = existing.filter((entry) => typeof entry === 'string');
|
|
147
|
+
const nonString = existing.filter((entry) => typeof entry !== 'string');
|
|
148
|
+
const gitEntries = entries.filter((entry) => /^regent-code@(git\+|git )/.test(entry));
|
|
149
|
+
const filePin = entries.find(
|
|
150
|
+
(entry) => /^file:/.test(entry) && entry.toLowerCase().includes('regent'),
|
|
151
|
+
);
|
|
152
|
+
const hasNpmPin =
|
|
153
|
+
entries.some((entry) => entry.startsWith('regent-code@') && !/^regent-code@git/.test(entry)) ||
|
|
154
|
+
entries.includes('regent-code');
|
|
155
|
+
|
|
156
|
+
const next = entries
|
|
157
|
+
.filter((entry) => !gitEntries.includes(entry))
|
|
158
|
+
.concat(nonString);
|
|
159
|
+
let reportPlugin;
|
|
160
|
+
if (hasNpmPin && gitEntries.length === 0) {
|
|
161
|
+
reportPlugin = 'unchanged';
|
|
162
|
+
} else {
|
|
163
|
+
reportPlugin = gitEntries.length ? 'updated' : 'added';
|
|
164
|
+
}
|
|
165
|
+
if (!hasNpmPin) next.push(pluginSpec);
|
|
166
|
+
if (filePin) {
|
|
167
|
+
report.warnings.push(
|
|
168
|
+
'regent plugin pinned via file:// (dev loop); entry left untouched',
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
if (!deepEqual(next, existing)) {
|
|
172
|
+
edits = edits.concat(modify(text, ['plugins'], next, { formattingOptions }));
|
|
173
|
+
report.plugin = reportPlugin;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
edits = edits.concat(modify(text, ['plugins'], [pluginSpec], { formattingOptions }));
|
|
178
|
+
report.plugin = 'added';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { text: applyEdits(text, edits), report };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Full install against the filesystem: resolves the target config, patches it
|
|
185
|
+
// (creating the file with only the regent entries when none exists), and
|
|
186
|
+
// returns the report plus target info.
|
|
187
|
+
export function install(options = {}) {
|
|
188
|
+
const target = resolveTarget(options);
|
|
189
|
+
const existingText = existsSync(target.path) ? readFileSync(target.path, 'utf8') : null;
|
|
190
|
+
const base = existingText ?? `{\n}\n`;
|
|
191
|
+
const { text, report } = patchText(base, options);
|
|
192
|
+
|
|
193
|
+
if (text !== existingText) {
|
|
194
|
+
mkdirSync(dirname(target.path), { recursive: true });
|
|
195
|
+
writeFileSync(target.path, text);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
...report,
|
|
200
|
+
path: target.path,
|
|
201
|
+
scope: target.scope,
|
|
202
|
+
created: existingText === null,
|
|
203
|
+
};
|
|
204
|
+
}
|
package/mcp/prompts.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extract the body of a Markdown file after its YAML-ish frontmatter block.
|
|
10
|
+
* Mirrors the plugin's `extractContent` helper.
|
|
11
|
+
* @param {string} content
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
const extractContent = (content) => {
|
|
15
|
+
const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);
|
|
16
|
+
return match ? match[1] : content;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** @param {string} content @param {string} key @returns {string} */
|
|
20
|
+
function frontmatterValue(content, key) {
|
|
21
|
+
return content.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'))?.[1]?.trim() || '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Read a SKILL.md/command.md file into an MCP prompt definition.
|
|
26
|
+
* @param {string} name
|
|
27
|
+
* @param {string} description
|
|
28
|
+
* @param {string} location
|
|
29
|
+
* @returns {{ name: string, description: string, arguments: Array<{ name: string, description: string, required: boolean }>, template: string }}
|
|
30
|
+
*/
|
|
31
|
+
function toPrompt(name, description, location) {
|
|
32
|
+
let content = '';
|
|
33
|
+
let sourceDescription = description;
|
|
34
|
+
try {
|
|
35
|
+
if (fs.existsSync(location)) {
|
|
36
|
+
const raw = fs.readFileSync(location, 'utf8');
|
|
37
|
+
sourceDescription = frontmatterValue(raw, 'description') || description;
|
|
38
|
+
content = extractContent(raw).trim();
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
/* non-fatal */
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
name,
|
|
45
|
+
description: sourceDescription,
|
|
46
|
+
arguments: [
|
|
47
|
+
{
|
|
48
|
+
name: 'arguments',
|
|
49
|
+
description: 'Optional free-form arguments to append to the template',
|
|
50
|
+
required: false,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
template: content,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @returns {Array<{ name: string, description: string, arguments: Array<{ name: string, description: string, required: boolean }>, template: string }>} */
|
|
58
|
+
export function readPackagePrompts() {
|
|
59
|
+
const prompts = [];
|
|
60
|
+
|
|
61
|
+
// Commands -> command.<name> prompts
|
|
62
|
+
const commandsDir = path.join(rootDir, '.opencode', 'commands');
|
|
63
|
+
if (fs.existsSync(commandsDir)) {
|
|
64
|
+
for (const entry of fs.readdirSync(commandsDir, { withFileTypes: true })) {
|
|
65
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
66
|
+
const name = entry.name.replace(/\.md$/, '');
|
|
67
|
+
prompts.push(
|
|
68
|
+
toPrompt(`command.${name}`, `Regent command: ${name}`, path.join(commandsDir, entry.name)),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Skills -> skill.<id> prompts
|
|
74
|
+
const skillsDir = path.join(rootDir, '.opencode', 'skills');
|
|
75
|
+
if (fs.existsSync(skillsDir)) {
|
|
76
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
77
|
+
if (!entry.isDirectory()) continue;
|
|
78
|
+
const location = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
79
|
+
if (!fs.existsSync(location)) continue;
|
|
80
|
+
prompts.push(toPrompt(`skill.${entry.name}`, `Regent skill: ${entry.name}`, location));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return prompts;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Render a prompt template, substituting $ARGUMENTS with the provided value.
|
|
89
|
+
* @param {{ template: string }} prompt
|
|
90
|
+
* @param {Record<string, string> | undefined} args
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
93
|
+
export function renderPrompt(prompt, args = {}) {
|
|
94
|
+
const argumentsText = typeof args?.arguments === 'string' ? args.arguments : '';
|
|
95
|
+
if (prompt.template.includes('$ARGUMENTS')) {
|
|
96
|
+
return prompt.template.replaceAll('$ARGUMENTS', () => argumentsText);
|
|
97
|
+
}
|
|
98
|
+
return argumentsText.trim() ? `${prompt.template}\n\n${argumentsText}` : prompt.template;
|
|
99
|
+
}
|