Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ matter):
- `@name`, `@description`, `@author`, `@version`, `@path` — required.
- `@mode` — `replace` (default) | `merge` | `merge-overwrite`.
- `@fetch: URL -> dest [sha256=hex]` — repeatable.
- `@prompt: name | type | help` — repeatable; type ∈ `text`/`secret`.
- `@prompt: name | type | help | default` — repeatable; type ∈
`text`/`secret`. Help and default are optional. Default is
forbidden when type is `secret`. When the user enters an empty
line, the default is used.

Body is JSONC. After parsing it must be valid JSON of the shape the
leaf at `@path` expects (object/array/scalar all allowed for `replace`;
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ each write — no auto-pruning, so they pile up.
| `jdtls-clean-workspace` | LSP | replace | Stops jdtls from writing `.project`/`.classpath`/etc. into your project root |
| `mcp-remote-add` | MCP | replace | Add a remote MCP server with bearer-token auth (prompts for id, URL, token) |
| `mcp-remote-add-noauth` | MCP | replace | Add a remote MCP server without auth (prompts for id, URL) |
| `mcp-intellij` | MCP | replace | Add the JetBrains IDE MCP server (loopback HTTP, default port 64342) |
| `permissions-git-safe` | Permissions | merge | Read-only git commands (status, diff, log, branch --list, fetch, etc.) |
| `permissions-shell-safe` | Permissions | merge | Low-risk shell commands (ls, cat, grep, rg, jq, yq, etc.) |
| `permissions-build-tools` | Permissions | merge | Build tools (node, npm, mvn, gradle, make, python, pip, cargo, go) |
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "opencode-presets",
"version": "0.2.2",
"version": "0.2.3",
"description": "Interactive CLI that patches opencode.json with curated config presets — LSP, MCP, permissions.",
"type": "module",
"bin": {
Expand Down
25 changes: 25 additions & 0 deletions presets/mcp-intellij.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// @name: mcp-intellij
// @description: Connects opencode to a running JetBrains IDE
// (IntelliJ IDEA, PyCharm, WebStorm, GoLand, RustRover, etc.) via
// the official "MCP Server" plugin. Once enabled, opencode can see
// what you're doing in the IDE in real time — the currently open
// file, caret position, selected text, list of open editors, recent
// files, project structure, and diagnostics — and can drive the IDE
// back: open files, jump to symbols, run/debug configurations, and
// apply edits through the IDE's own refactoring engine. This makes
// "fix the thing I'm looking at" prompts work without copy-paste.
// Wired up as a remote HTTP MCP server at
// http://127.0.0.1:<port>/stream. IMPORTANT: the "MCP Server"
// plugin MUST be installed AND switched on in the IDE — opencode
// cannot reach the endpoint otherwise. Press Enter to accept the
// default port 64342. To remove:
// `opencode-presets remove mcp-intellij`.
// @author: Jan <jan@trick77.com>
// @version: 0.1.0
// @path: mcp.intellij
// @prompt: port | text | IDE MCP server port | 64342
{
"type": "remote",
"url": "http://127.0.0.1:{{prompt:port}}/stream",
"enabled": true
}
15 changes: 11 additions & 4 deletions src/batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,18 @@ export async function runBatch({ resets, confPaths, target, cacheDir, backupDir
m.promptValues = {};
for (const p of m.meta.prompts) {
const label = ' ' + c.bold(p.name) +
(p.help ? c.meta(' (' + p.help + ')') : '') + ': ';
const val = p.type === 'secret' ? await promptSecret(label) : await promptText(label);
(p.help ? c.meta(' (' + p.help + ')') : '') +
(p.default !== undefined ? c.meta(' [default: ' + p.default + ']') : '') +
': ';
const raw = p.type === 'secret' ? await promptSecret(label) : await promptText(label);
let val = raw;
if (!val) {
console.error(c.err(`error: prompt "${p.name}" cannot be empty`));
process.exit(1);
if (p.default !== undefined) {
val = p.default;
} else {
console.error(c.err(`error: prompt "${p.name}" cannot be empty`));
process.exit(1);
}
}
if (p.name === 'name' && !ID_RE.test(val)) {
console.error(c.err(`error: invalid identifier "${val}" — must match ${ID_RE.source}`));
Expand Down
12 changes: 8 additions & 4 deletions src/parse-conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface PromptDirective {
name: string;
type: PromptType;
help: string;
default?: string;
}

export interface ConfMeta {
Expand Down Expand Up @@ -124,17 +125,20 @@ export function parseConfString(raw: string, filePath = '<inline>'): ParsedConf

function parsePrompt(value: string, filePath: string): PromptDirective {
const parts = value.split('|').map(s => s.trim());
if (parts.length < 2 || parts.length > 3) {
throw parseError(filePath, 0, `@prompt must be "name | type | help" (help optional), got "${value}"`);
if (parts.length < 2 || parts.length > 4) {
throw parseError(filePath, 0, `@prompt must be "name | type | help | default" (help and default optional), got "${value}"`);
}
const [name, type, help = ''] = parts;
const [name, type, help = '', def] = parts;
if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(name)) {
throw parseError(filePath, 0, `@prompt name must be alphanumeric/underscore/hyphen, got "${name}"`);
}
if (type !== 'text' && type !== 'secret') {
throw parseError(filePath, 0, `@prompt type must be "text" or "secret", got "${type}"`);
}
return { name, type, help };
if (def !== undefined && type === 'secret') {
throw parseError(filePath, 0, `@prompt default value is not allowed for type "secret" (got "${value}")`);
}
return def !== undefined ? { name, type, help, default: def } : { name, type, help };
}

function parseFetch(value: string, filePath: string): FetchDirective {
Expand Down
22 changes: 22 additions & 0 deletions test/parse-conf.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,28 @@ describe('parseConfString — @prompt', () => {
const src = minimalHeader + '// @prompt: x | bogus\n\n{}';
assert.throws(() => parseConfString(src), /type must be/);
});

test('parses name | type | help | default', () => {
const src = minimalHeader + '// @prompt: port | text | server port | 64342\n\n{}';
const { meta } = parseConfString(src);
assert.deepEqual(meta.prompts[0], { name: 'port', type: 'text', help: 'server port', default: '64342' });
});

test('parses default with empty help', () => {
const src = minimalHeader + '// @prompt: port | text | | 64342\n\n{}';
const { meta } = parseConfString(src);
assert.deepEqual(meta.prompts[0], { name: 'port', type: 'text', help: '', default: '64342' });
});

test('rejects default on secret', () => {
const src = minimalHeader + '// @prompt: token | secret | bearer | hunter2\n\n{}';
assert.throws(() => parseConfString(src), /not allowed for type "secret"/);
});

test('rejects more than 4 fields', () => {
const src = minimalHeader + '// @prompt: a | text | b | c | d\n\n{}';
assert.throws(() => parseConfString(src), /@prompt must be/);
});
});

describe('parseConfString — body and unknowns', () => {
Expand Down