@nevermorelove/ompp 0.1.0 → 0.2.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/README.md +23 -11
- package/ompp.js +161 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,32 +8,44 @@ ompp gives each kind of work a name and a folder. Launch a session in a mode and
|
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
11
|
+
```sh
|
|
12
|
+
npm install -g @nevermorelove/ompp
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Node 18 or newer, that is the whole requirement. bun works too. The binary is
|
|
16
|
+
named `ompp` even though the package is scoped. You can also install from
|
|
17
|
+
source:
|
|
18
|
+
|
|
11
19
|
```sh
|
|
12
20
|
git clone git@github.com:ForeverInLaw/ompp.git
|
|
13
21
|
cd ompp
|
|
14
22
|
npm install -g .
|
|
15
23
|
```
|
|
16
24
|
|
|
17
|
-
Node 18 or newer, that is the whole requirement. bun works too. After install, `ompp` is on your PATH. To update, `git pull` and install again.
|
|
18
|
-
|
|
19
|
-
## Usage
|
|
20
|
-
|
|
21
25
|
```sh
|
|
22
26
|
ompp arrow-key picker, then launches omp
|
|
23
27
|
ompp pentest launch in a mode
|
|
24
28
|
ompp writing -p "..." mode plus any omp flags
|
|
29
|
+
ompp create pentest make a new mode with placeholder files,
|
|
30
|
+
then open its folder in your file manager
|
|
25
31
|
ompp list print mode names
|
|
26
32
|
ompp pentest --dry-run show the omp command line instead of running it
|
|
27
33
|
```
|
|
28
34
|
|
|
29
35
|
Before launch it prints one line to stderr, `[ompp] mode: pentest`, so you always know where you are.
|
|
30
36
|
|
|
31
|
-
|
|
37
|
+
Modes are read from up to three places, and same-named modes from earlier
|
|
38
|
+
places win:
|
|
32
39
|
|
|
33
|
-
|
|
40
|
+
1. `OMPP_MODES_DIR`, if you set it
|
|
41
|
+
2. `modes/` shipped next to the script (the repo checkout or the npm package)
|
|
42
|
+
3. `~/.omp/ompp/modes/`, the user-level home for your own modes
|
|
43
|
+
|
|
44
|
+
`ompp create` always writes to `~/.omp/ompp/modes/`, so you never edit files
|
|
45
|
+
inside an installed package. The first `ompp create` makes the folder for you.
|
|
34
46
|
|
|
35
47
|
```
|
|
36
|
-
modes/pentest/
|
|
48
|
+
~/.omp/ompp/modes/pentest/
|
|
37
49
|
config.yml settings overlay
|
|
38
50
|
system.md full system prompt replacement
|
|
39
51
|
append.md prompt addendum, used when system.md is absent
|
|
@@ -49,7 +61,9 @@ modes/pentest/
|
|
|
49
61
|
| `append.md` | `--append-system-prompt` | Rides on top of your normal prompt. This is what most modes want. |
|
|
50
62
|
| the folder itself | `--plugin-dir` | omp treats the mode folder as a plugin root, so `skills/` and `.mcp.json` inside it load too. |
|
|
51
63
|
|
|
52
|
-
A new mode is `
|
|
64
|
+
A new mode is `ompp create <name>` (or plain `mkdir` plus files). No manifest,
|
|
65
|
+
no code. The wrapper reads the folder, so it never needs to change when you
|
|
66
|
+
add modes. `modes/general` in the repo is a working example.
|
|
53
67
|
|
|
54
68
|
## Precedence
|
|
55
69
|
|
|
@@ -65,9 +79,7 @@ MCP servers can be added by a mode but never removed. omp has no launch-time MCP
|
|
|
65
79
|
|
|
66
80
|
Mode-local `skills/` are additive as well. To have fewer skills, set `skills.includeSkills` in the mode's `config.yml`. The allowlist filters every discovered skill, global ones included.
|
|
67
81
|
|
|
68
|
-
The picker needs a TTY. Piped or otherwise headless stdin gets a numbered prompt instead.
|
|
69
|
-
|
|
70
82
|
## Environment
|
|
71
83
|
|
|
72
|
-
- `OMPP_MODES_DIR`,
|
|
84
|
+
- `OMPP_MODES_DIR`, an extra modes directory that wins over the bundled one.
|
|
73
85
|
- `OMPP_OMP_BIN`, which omp to launch. Default is omp from PATH.
|
package/ompp.js
CHANGED
|
@@ -20,7 +20,20 @@ const fs = require("fs");
|
|
|
20
20
|
const path = require("path");
|
|
21
21
|
const { spawn } = require("child_process");
|
|
22
22
|
|
|
23
|
-
const
|
|
23
|
+
const os = require("os");
|
|
24
|
+
|
|
25
|
+
// Modes come from up to three places, in priority order:
|
|
26
|
+
// 1. OMPP_MODES_DIR (explicit override)
|
|
27
|
+
// 2. modes/ next to this script (repo checkout or npm package)
|
|
28
|
+
// 3. ~/.omp/ompp/modes (user-level default where `ompp create` writes)
|
|
29
|
+
// Same-named modes from a higher-priority source win.
|
|
30
|
+
const DEFAULT_MODES_DIR = path.join(os.homedir(), ".omp", "ompp", "modes");
|
|
31
|
+
const BUNDLED_MODES_DIR = path.join(__dirname, "modes");
|
|
32
|
+
const SOURCES = [];
|
|
33
|
+
if (process.env.OMPP_MODES_DIR) SOURCES.push(process.env.OMPP_MODES_DIR);
|
|
34
|
+
SOURCES.push(BUNDLED_MODES_DIR);
|
|
35
|
+
SOURCES.push(DEFAULT_MODES_DIR);
|
|
36
|
+
const RESERVED_NAMES = ["create", "list", "help", "version"];
|
|
24
37
|
const CONFIG_NAMES = ["config.yml", "config.yaml"];
|
|
25
38
|
const PLUGIN_ARTIFACTS = [
|
|
26
39
|
"skills",
|
|
@@ -32,6 +45,10 @@ const PLUGIN_ARTIFACTS = [
|
|
|
32
45
|
"tools",
|
|
33
46
|
];
|
|
34
47
|
|
|
48
|
+
function modeDirOf(name, source) {
|
|
49
|
+
return path.join(source, name);
|
|
50
|
+
}
|
|
51
|
+
|
|
35
52
|
function firstExisting(dir, names) {
|
|
36
53
|
for (const n of names) {
|
|
37
54
|
const p = path.join(dir, n);
|
|
@@ -40,27 +57,43 @@ function firstExisting(dir, names) {
|
|
|
40
57
|
return null;
|
|
41
58
|
}
|
|
42
59
|
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
const skipped = [];
|
|
46
|
-
if (!fs.existsSync(MODES_DIR)) return { modes, skipped };
|
|
60
|
+
function scanSource(source, modes, skipped, seen) {
|
|
61
|
+
if (!fs.existsSync(source)) return;
|
|
47
62
|
const entries = fs
|
|
48
|
-
.readdirSync(
|
|
63
|
+
.readdirSync(source, { withFileTypes: true })
|
|
49
64
|
.filter((e) => e.isDirectory())
|
|
50
65
|
.map((e) => e.name)
|
|
51
66
|
.sort();
|
|
52
67
|
for (const name of entries) {
|
|
53
68
|
if (name.startsWith(".") || name.startsWith("_")) continue;
|
|
54
|
-
const dir =
|
|
69
|
+
const dir = modeDirOf(name, source);
|
|
55
70
|
const hasSomething = firstExisting(dir, [
|
|
56
71
|
...CONFIG_NAMES,
|
|
57
72
|
"system.md",
|
|
58
73
|
"append.md",
|
|
59
74
|
...PLUGIN_ARTIFACTS,
|
|
60
75
|
]);
|
|
61
|
-
if (hasSomething)
|
|
62
|
-
|
|
76
|
+
if (!hasSomething) {
|
|
77
|
+
skipped.push(name);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (seen.has(name)) {
|
|
81
|
+
// A same-named mode from a higher-priority source already won.
|
|
82
|
+
console.error(
|
|
83
|
+
`[ompp] mode "${name}" exists in several mode folders; using the one in ${modeDirOf(name, SOURCES[0])}`,
|
|
84
|
+
);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
seen.add(name);
|
|
88
|
+
modes.push({ name, source });
|
|
63
89
|
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function discoverModes() {
|
|
93
|
+
const modes = [];
|
|
94
|
+
const skipped = [];
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
for (const source of SOURCES) scanSource(source, modes, skipped, seen);
|
|
64
97
|
return { modes, skipped };
|
|
65
98
|
}
|
|
66
99
|
|
|
@@ -130,9 +163,13 @@ Usage:
|
|
|
130
163
|
ompp pick a mode interactively, then launch omp
|
|
131
164
|
ompp <mode> [args...] launch omp in a mode; everything after <mode>
|
|
132
165
|
is passed to omp unchanged and wins over the mode
|
|
166
|
+
ompp create <name> create a new mode with placeholder files
|
|
167
|
+
and open its folder
|
|
133
168
|
ompp list list available modes
|
|
134
169
|
|
|
135
|
-
|
|
170
|
+
Modes live in ~/.omp/ompp/modes/ (created for you by "ompp create") and,
|
|
171
|
+
if OMPP_MODES_DIR is set, in that folder too. Same-named modes from
|
|
172
|
+
OMPP_MODES_DIR win. All files are optional:
|
|
136
173
|
config.yml settings overlay: model, thinking level,
|
|
137
174
|
skills.includeSkills allowlist, approval mode, ...
|
|
138
175
|
system.md replaces the system prompt entirely
|
|
@@ -147,10 +184,100 @@ Flags:
|
|
|
147
184
|
-v, --version version
|
|
148
185
|
|
|
149
186
|
Environment:
|
|
150
|
-
OMPP_MODES_DIR modes directory (
|
|
187
|
+
OMPP_MODES_DIR extra modes directory (repo checkout or custom)
|
|
151
188
|
OMPP_OMP_BIN omp executable to launch (default: omp from PATH)`);
|
|
152
189
|
}
|
|
153
190
|
|
|
191
|
+
const CONFIG_TEMPLATE = `# Settings overlay for this mode. Every line is optional:
|
|
192
|
+
# uncomment what you need. See "omp config list" for all keys.
|
|
193
|
+
|
|
194
|
+
# modelRoles:
|
|
195
|
+
# default: anthropic/claude-sonnet-4-5
|
|
196
|
+
# defaultThinkingLevel: high
|
|
197
|
+
# tools:
|
|
198
|
+
# approvalMode: write
|
|
199
|
+
|
|
200
|
+
# Allowlist of skills this mode can see. Globs are allowed.
|
|
201
|
+
# Empty (or absent) means every discovered skill loads.
|
|
202
|
+
# skills:
|
|
203
|
+
# includeSkills:
|
|
204
|
+
# - tdd
|
|
205
|
+
# - diagnosing-bugs
|
|
206
|
+
`;
|
|
207
|
+
|
|
208
|
+
const APPEND_TEMPLATE = `# Extra instructions added on top of your normal system prompt.
|
|
209
|
+
# This file is plain text: delete these lines and write your own.
|
|
210
|
+
|
|
211
|
+
Answer directly and briefly. No preamble, no restating the question.
|
|
212
|
+
`;
|
|
213
|
+
|
|
214
|
+
const SYSTEM_TEMPLATE = `# Rename this file to system.md to REPLACE the whole system prompt
|
|
215
|
+
# instead of appending. Replacing drops omp's default instructions and
|
|
216
|
+
# your global ~/.omp/agent/SYSTEM.md, including tool policy: write
|
|
217
|
+
# what you need into the file itself.
|
|
218
|
+
`;
|
|
219
|
+
|
|
220
|
+
const MCP_TEMPLATE = `{
|
|
221
|
+
"mcpServers": {
|
|
222
|
+
"example": {
|
|
223
|
+
"type": "http",
|
|
224
|
+
"url": "https://mcp.example.com/mcp"
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
`;
|
|
229
|
+
|
|
230
|
+
function revealInFileManager(dir) {
|
|
231
|
+
const cmd =
|
|
232
|
+
process.platform === "win32"
|
|
233
|
+
? { bin: "explorer", args: [dir] }
|
|
234
|
+
: process.platform === "darwin"
|
|
235
|
+
? { bin: "open", args: [dir] }
|
|
236
|
+
: { bin: "xdg-open", args: [dir] };
|
|
237
|
+
const child = spawn(cmd.bin, cmd.args, { stdio: "ignore", detached: true });
|
|
238
|
+
child.on("error", (err) => {
|
|
239
|
+
console.error(`[ompp] could not open a file manager: ${err.message}`);
|
|
240
|
+
});
|
|
241
|
+
child.unref();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function createMode(name) {
|
|
245
|
+
if (!name) {
|
|
246
|
+
console.error(`[ompp] usage: ompp create <name>`);
|
|
247
|
+
return 1;
|
|
248
|
+
}
|
|
249
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) {
|
|
250
|
+
console.error(
|
|
251
|
+
`[ompp] "${name}" is not a valid mode name. Use lowercase letters, digits, and dashes.`,
|
|
252
|
+
);
|
|
253
|
+
return 1;
|
|
254
|
+
}
|
|
255
|
+
if (RESERVED_NAMES.includes(name)) {
|
|
256
|
+
console.error(`[ompp] "${name}" is reserved for ompp commands.`);
|
|
257
|
+
return 1;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Always create in the user-level default, regardless of OMPP_MODES_DIR.
|
|
261
|
+
const dir = modeDirOf(name, DEFAULT_MODES_DIR);
|
|
262
|
+
if (fs.existsSync(dir)) {
|
|
263
|
+
console.error(`[ompp] mode "${name}" already exists, opening its folder.`);
|
|
264
|
+
revealInFileManager(dir);
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fs.mkdirSync(path.join(dir, "skills"), { recursive: true });
|
|
269
|
+
fs.writeFileSync(path.join(dir, "config.yml"), CONFIG_TEMPLATE);
|
|
270
|
+
fs.writeFileSync(path.join(dir, "append.md"), APPEND_TEMPLATE);
|
|
271
|
+
fs.writeFileSync(path.join(dir, "system.md.example"), SYSTEM_TEMPLATE);
|
|
272
|
+
fs.writeFileSync(path.join(dir, ".mcp.json.example"), MCP_TEMPLATE);
|
|
273
|
+
fs.writeFileSync(path.join(dir, "skills", ".gitkeep"), "");
|
|
274
|
+
|
|
275
|
+
console.error(`[ompp] created mode "${name}" in ${dir}`);
|
|
276
|
+
revealInFileManager(dir);
|
|
277
|
+
console.error(`[ompp] edit the placeholders, then launch it: ompp ${name}`);
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
154
281
|
function pickInteractive(modes) {
|
|
155
282
|
return new Promise((resolve, reject) => {
|
|
156
283
|
const stdin = process.stdin;
|
|
@@ -229,42 +356,27 @@ function pickInteractive(modes) {
|
|
|
229
356
|
|
|
230
357
|
stdin.setRawMode(true);
|
|
231
358
|
stdin.resume();
|
|
232
|
-
stdin.on("data", onData);
|
|
233
359
|
render(true);
|
|
234
360
|
});
|
|
235
361
|
}
|
|
236
362
|
|
|
237
|
-
function pickByNumber(modes) {
|
|
238
|
-
return new Promise((resolve, reject) => {
|
|
239
|
-
const readline = require("readline");
|
|
240
|
-
const rl = readline.createInterface({ input: process.stdin });
|
|
241
|
-
process.stdout.write("Select a mode:\n");
|
|
242
|
-
modes.forEach((m, i) => process.stdout.write(` ${i + 1}. ${m}\n`));
|
|
243
|
-
process.stdout.write(`Choice [1-${modes.length}]: `);
|
|
244
|
-
rl.once("line", (line) => {
|
|
245
|
-
rl.close();
|
|
246
|
-
const n = parseInt(line.trim(), 10);
|
|
247
|
-
if (n >= 1 && n <= modes.length) resolve(modes[n - 1]);
|
|
248
|
-
else reject(new Error(`"${line.trim()}" is not a valid choice`));
|
|
249
|
-
});
|
|
250
|
-
rl.once("close", () => {
|
|
251
|
-
reject(new Error("no mode selected"));
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
|
|
256
363
|
async function main() {
|
|
257
364
|
const argv = process.argv.slice(2);
|
|
258
365
|
|
|
366
|
+
if (argv[0] === "create") {
|
|
367
|
+
return createMode(argv[1]);
|
|
368
|
+
}
|
|
369
|
+
|
|
259
370
|
if (argv[0] === "list") {
|
|
260
371
|
const { modes, skipped } = discoverModes();
|
|
261
372
|
if (!modes.length) {
|
|
262
373
|
console.error(
|
|
263
|
-
`[ompp] no modes found
|
|
374
|
+
`[ompp] no modes found. Run "ompp create <name>" to make one.`,
|
|
264
375
|
);
|
|
376
|
+
for (const s of SOURCES) console.error(`[ompp] looked in: ${s}`);
|
|
265
377
|
return 1;
|
|
266
378
|
}
|
|
267
|
-
for (const m of modes) console.log(m);
|
|
379
|
+
for (const m of modes) console.log(m.name);
|
|
268
380
|
if (skipped.length) {
|
|
269
381
|
console.error(
|
|
270
382
|
`[ompp] skipped folders without recognized files: ${skipped.join(", ")}`,
|
|
@@ -293,46 +405,53 @@ async function main() {
|
|
|
293
405
|
}
|
|
294
406
|
if (!modes.length) {
|
|
295
407
|
console.error(
|
|
296
|
-
`[ompp] no modes found
|
|
408
|
+
`[ompp] no modes found. Run "ompp create <name>" to make one.`,
|
|
297
409
|
);
|
|
410
|
+
for (const s of SOURCES) console.error(`[ompp] looked in: ${s}`);
|
|
298
411
|
return 1;
|
|
299
412
|
}
|
|
300
413
|
|
|
414
|
+
const modeNames = modes.map((m) => m.name);
|
|
301
415
|
let mode = null;
|
|
302
416
|
let userArgs = rest;
|
|
303
417
|
if (rest.length && !rest[0].startsWith("-")) {
|
|
304
418
|
const candidate = rest[0];
|
|
305
|
-
|
|
306
|
-
|
|
419
|
+
const found = modes.find((m) => m.name === candidate);
|
|
420
|
+
if (found) {
|
|
421
|
+
mode = found;
|
|
307
422
|
userArgs = rest.slice(1);
|
|
308
423
|
} else {
|
|
309
424
|
console.error(`[ompp] unknown mode "${candidate}"`);
|
|
310
|
-
console.error(`[ompp] available: ${
|
|
425
|
+
console.error(`[ompp] available: ${modeNames.join(", ")}`);
|
|
311
426
|
return 1;
|
|
312
427
|
}
|
|
313
428
|
} else {
|
|
314
429
|
try {
|
|
315
430
|
mode = process.stdin.isTTY
|
|
316
|
-
? await pickInteractive(
|
|
317
|
-
|
|
431
|
+
? await pickInteractive(modeNames).then((n) =>
|
|
432
|
+
modes.find((m) => m.name === n),
|
|
433
|
+
)
|
|
434
|
+
: await pickByNumber(modeNames).then((n) =>
|
|
435
|
+
modes.find((m) => m.name === n),
|
|
436
|
+
);
|
|
318
437
|
} catch (err) {
|
|
319
438
|
console.error(`[ompp] ${err.message}`);
|
|
320
439
|
return 1;
|
|
321
440
|
}
|
|
322
441
|
}
|
|
323
442
|
|
|
324
|
-
const modeDir =
|
|
443
|
+
const modeDir = modeDirOf(mode.name, mode.source);
|
|
325
444
|
const args = buildArgv(modeDir, userArgs);
|
|
326
445
|
const { bin, shell } = resolveBin();
|
|
327
446
|
|
|
328
447
|
if (dryRun) {
|
|
329
|
-
console.error(`[ompp] mode: ${mode}`);
|
|
448
|
+
console.error(`[ompp] mode: ${mode.name}`);
|
|
330
449
|
console.error(`[ompp] omp: ${bin}${shell ? " (shell)" : ""}`);
|
|
331
450
|
console.error(`[ompp] argv: ${JSON.stringify(args)}`);
|
|
332
451
|
return 0;
|
|
333
452
|
}
|
|
334
453
|
|
|
335
|
-
console.error(`[ompp] mode: ${mode}`);
|
|
454
|
+
console.error(`[ompp] mode: ${mode.name}`);
|
|
336
455
|
const child = shell
|
|
337
456
|
? spawn([winQuote(bin), ...args.map(winQuote)].join(" "), {
|
|
338
457
|
stdio: "inherit",
|