oh-my-muse 0.1.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/LICENSE +26 -0
- package/README.md +100 -0
- package/bin/lib.mjs +606 -0
- package/bin/omm.mjs +311 -0
- package/hooks/notify.mjs +133 -0
- package/models.json +38 -0
- package/omm.jsonc +25 -0
- package/pack/agents/architect.ts +26 -0
- package/pack/agents/critic.ts +26 -0
- package/pack/agents/data-scientist.ts +26 -0
- package/pack/agents/debugger.ts +26 -0
- package/pack/agents/designer.ts +25 -0
- package/pack/agents/docs-writer.ts +26 -0
- package/pack/agents/file-picker.ts +24 -0
- package/pack/agents/implementer.ts +26 -0
- package/pack/agents/muse-advisor.ts +63 -0
- package/pack/agents/muse-autopilot.ts +65 -0
- package/pack/agents/muse-deep-interview.ts +71 -0
- package/pack/agents/muse-pipeline.ts +49 -0
- package/pack/agents/muse-ralph.ts +48 -0
- package/pack/agents/muse-ralplan.ts +70 -0
- package/pack/agents/muse-team.ts +95 -0
- package/pack/agents/muse-ultraqa.ts +55 -0
- package/pack/agents/muse-ultrawork.ts +46 -0
- package/pack/agents/planner.ts +26 -0
- package/pack/agents/refactorer.ts +26 -0
- package/pack/agents/researcher.ts +25 -0
- package/pack/agents/reviewer.ts +26 -0
- package/pack/agents/security-reviewer.ts +27 -0
- package/pack/agents/tester.ts +27 -0
- package/pack/skills/design/SKILL.md +24 -0
- package/pack/skills/docs/SKILL.md +23 -0
- package/pack/skills/harness/SKILL.md +24 -0
- package/pack/skills/security/SKILL.md +24 -0
- package/pack/skills/tdd/SKILL.md +24 -0
- package/pack/skills/verify/SKILL.md +28 -0
- package/package.json +48 -0
- package/src/harness/DESIGN.md +29 -0
- package/src/harness/HARNESS.md +42 -0
- package/types/agent-definition.ts +18 -0
- package/types/orchestrator.ts +37 -0
package/bin/omm.mjs
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
TIERS,
|
|
6
|
+
TIER_MODELS,
|
|
7
|
+
CONFIG_FILENAME,
|
|
8
|
+
MANAGED_FILENAME,
|
|
9
|
+
repoRootFromHere,
|
|
10
|
+
resolveTargetDir,
|
|
11
|
+
installDirFor,
|
|
12
|
+
loadJsoncFile,
|
|
13
|
+
writeFileMode,
|
|
14
|
+
expandEnv,
|
|
15
|
+
redactConfig,
|
|
16
|
+
redactText,
|
|
17
|
+
isTier,
|
|
18
|
+
modelForTier,
|
|
19
|
+
loadModelsFile,
|
|
20
|
+
getPreset,
|
|
21
|
+
applyPreset,
|
|
22
|
+
applyModelOverrides,
|
|
23
|
+
validateModelOverrides,
|
|
24
|
+
validateConfig,
|
|
25
|
+
loadProjectConfig,
|
|
26
|
+
readManifest,
|
|
27
|
+
collectPackFiles,
|
|
28
|
+
installPack,
|
|
29
|
+
updatePack,
|
|
30
|
+
uninstallPack,
|
|
31
|
+
doctor,
|
|
32
|
+
} from "./lib.mjs";
|
|
33
|
+
import { sendNotification, CHANNELS } from "../hooks/notify.mjs";
|
|
34
|
+
|
|
35
|
+
const REPO_ROOT = repoRootFromHere(import.meta.url);
|
|
36
|
+
|
|
37
|
+
const HELP = `omm — oh-my-muse pack manager
|
|
38
|
+
|
|
39
|
+
Usage: omm <command> [options]
|
|
40
|
+
|
|
41
|
+
Commands:
|
|
42
|
+
setup [--dir <path>] Write a default omm.jsonc (0600) if missing
|
|
43
|
+
install [--dir <path>] Install pack into <dir>/.claude/oh-my-muse (staging + atomic)
|
|
44
|
+
update [--dir <path>] Update the installed pack (staging + atomic)
|
|
45
|
+
uninstall [--dir <path>] Remove files tracked in omm-managed.json
|
|
46
|
+
doctor [--dir <path>] Check node, config, tiers, webhooks, permissions
|
|
47
|
+
list [--dir <path>] [--json] List configured agents
|
|
48
|
+
preset <name> [--dir <path>] Show a preset, or --apply <agent> to apply it
|
|
49
|
+
config <get|set|show> [key] [v] Read or modify omm.jsonc (values support $ENV expansion)
|
|
50
|
+
skill <list> [--dir <path>] List installed skills
|
|
51
|
+
notify --channel <c> --message <m> Send a notification (telegram|discord|slack|file)
|
|
52
|
+
|
|
53
|
+
Global options: --dir <path> Target project dir (default: cwd). Paths are absolute.
|
|
54
|
+
Env: OMM_DIR overrides --dir. Secrets in output are redacted; webhooks must be https.
|
|
55
|
+
File-channel notes land inside --dir unless the project config sets
|
|
56
|
+
allowExternalNotificationFile=true (or OMM_ALLOW_EXTERNAL_NOTIFY_FILE=1);
|
|
57
|
+
an explicit notify --file <path> always opts out.
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
function parseArgs(argv) {
|
|
61
|
+
const out = { _: [], dir: null, json: false };
|
|
62
|
+
for (let i = 0; i < argv.length; i++) {
|
|
63
|
+
const a = argv[i];
|
|
64
|
+
if (a === "--dir" || a === "--target") out.dir = argv[++i] ?? null;
|
|
65
|
+
else if (a.startsWith("--dir=")) out.dir = a.slice(6);
|
|
66
|
+
else if (a === "--json") out.json = true;
|
|
67
|
+
else if (a.startsWith("--")) {
|
|
68
|
+
const eq = a.indexOf("=");
|
|
69
|
+
if (eq !== -1) out[a.slice(2, eq)] = a.slice(eq + 1);
|
|
70
|
+
else if (argv[i + 1] && !argv[i + 1].startsWith("--")) out[a.slice(2)] = argv[++i];
|
|
71
|
+
else out[a.slice(2)] = "true";
|
|
72
|
+
} else out._.push(a);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function targetOf(args) {
|
|
78
|
+
return resolveTargetDir(process.cwd(), args.dir ?? process.env.OMM_DIR ?? ".");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function fail(err) {
|
|
82
|
+
console.error(redactText(err?.message ?? String(err)));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function defaultConfigText() {
|
|
87
|
+
return `// oh-my-muse configuration (JSONC). Secrets may use $ENV references.
|
|
88
|
+
{
|
|
89
|
+
// Default tier for agents that do not declare one.
|
|
90
|
+
"defaultTier": "balanced", // ${TIERS.join(" | ")}
|
|
91
|
+
"agents": [],
|
|
92
|
+
"notify": {}
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function cmdSetup(args) {
|
|
98
|
+
const target = targetOf(args);
|
|
99
|
+
const file = path.join(installDirFor(target), CONFIG_FILENAME);
|
|
100
|
+
if (fs.existsSync(file)) {
|
|
101
|
+
console.log(`Exists: ${file}`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
writeFileMode(file, defaultConfigText());
|
|
105
|
+
console.log(`Created ${file} (0600)`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function cmdInstall(args) {
|
|
109
|
+
const target = targetOf(args);
|
|
110
|
+
const { dest, files } = installPack({ repoRoot: REPO_ROOT, targetDir: target });
|
|
111
|
+
console.log(`Installed ${files.length} files to ${dest}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function cmdUpdate(args) {
|
|
115
|
+
const target = targetOf(args);
|
|
116
|
+
const { dest, files } = updatePack({ repoRoot: REPO_ROOT, targetDir: target });
|
|
117
|
+
console.log(`Updated ${files.length} files in ${dest}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function cmdUninstall(args) {
|
|
121
|
+
const target = targetOf(args);
|
|
122
|
+
const { dest, removed } = uninstallPack({ targetDir: target });
|
|
123
|
+
console.log(removed.length === 0 ? `Nothing tracked; cleaned ${dest}` : `Removed ${removed.length} files from ${dest}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function cmdDoctor(args) {
|
|
127
|
+
const target = targetOf(args);
|
|
128
|
+
const checks = doctor({ repoRoot: REPO_ROOT, targetDir: target });
|
|
129
|
+
let failed = 0;
|
|
130
|
+
for (const c of checks) {
|
|
131
|
+
console.log(`${c.ok ? "ok " : "FAIL"} ${c.message}`);
|
|
132
|
+
if (!c.ok) failed++;
|
|
133
|
+
}
|
|
134
|
+
if (failed > 0) process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function cmdList(args) {
|
|
138
|
+
const target = targetOf(args);
|
|
139
|
+
const { config, file } = loadProjectConfig(target);
|
|
140
|
+
if (!config) fail(`No config at ${file}; run: omm setup --dir ${target}`);
|
|
141
|
+
const defTier = config.defaultTier ?? "balanced";
|
|
142
|
+
const overrides = validateModelOverrides(config);
|
|
143
|
+
const agents = (config.agents ?? []).map((raw) => {
|
|
144
|
+
const a = applyModelOverrides(raw, overrides);
|
|
145
|
+
return {
|
|
146
|
+
name: a.name,
|
|
147
|
+
tier: a.tier ?? defTier,
|
|
148
|
+
model: a.model ?? (isTier(a.tier ?? defTier) ? TIER_MODELS[a.tier ?? defTier] : "(invalid tier)"),
|
|
149
|
+
description: a.description ?? "",
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
if (args.json) {
|
|
153
|
+
console.log(JSON.stringify(redactConfig(agents), null, 2));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (agents.length === 0) console.log(`No agents configured (${file})`);
|
|
157
|
+
for (const a of agents) console.log(`${a.name} [${a.tier}] ${a.model} ${a.description}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function cmdPreset(args) {
|
|
161
|
+
const target = targetOf(args);
|
|
162
|
+
const name = args._[1];
|
|
163
|
+
if (!name) fail("Usage: omm preset <name> [--apply <agent>] [--dir <path>]");
|
|
164
|
+
const models = loadModelsFile(REPO_ROOT);
|
|
165
|
+
const preset = getPreset(models, name);
|
|
166
|
+
if (args.apply) {
|
|
167
|
+
const { config, file } = loadProjectConfig(target);
|
|
168
|
+
if (!config) fail(`No config at ${file}; run: omm setup --dir ${target}`);
|
|
169
|
+
const agentName = String(args.apply);
|
|
170
|
+
const idx = (config.agents ?? []).findIndex((a) => a.name === agentName);
|
|
171
|
+
if (idx === -1) fail(`Agent "${agentName}" not found in ${file}`);
|
|
172
|
+
config.agents[idx] = applyModelOverrides(applyPreset(config.agents[idx], preset), validateModelOverrides(config));
|
|
173
|
+
validateConfig(expandEnv(config));
|
|
174
|
+
const raw = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
175
|
+
void raw;
|
|
176
|
+
writeFileMode(file, `${JSON.stringify(config, null, 2)}\n`);
|
|
177
|
+
console.log(`Applied preset "${name}" to agent "${agentName}" in ${file}`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
console.log(JSON.stringify(redactConfig(preset), null, 2));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getPath(obj, dotted) {
|
|
184
|
+
return dotted.split(".").reduce((o, k) => (o == null ? o : o[k]), obj);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function setPath(obj, dotted, value) {
|
|
188
|
+
const keys = dotted.split(".");
|
|
189
|
+
let cur = obj;
|
|
190
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
191
|
+
if (cur[keys[i]] === undefined || typeof cur[keys[i]] !== "object") cur[keys[i]] = {};
|
|
192
|
+
cur = cur[keys[i]];
|
|
193
|
+
}
|
|
194
|
+
cur[keys[keys.length - 1]] = value;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function coerceValue(text) {
|
|
198
|
+
try {
|
|
199
|
+
return JSON.parse(text);
|
|
200
|
+
} catch {
|
|
201
|
+
return text;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function cmdConfig(args) {
|
|
206
|
+
const target = targetOf(args);
|
|
207
|
+
const sub = args._[1] ?? "show";
|
|
208
|
+
const { config, file } = loadProjectConfig(target);
|
|
209
|
+
if (sub === "show") {
|
|
210
|
+
if (!config) fail(`No config at ${file}; run: omm setup --dir ${target}`);
|
|
211
|
+
console.log(JSON.stringify(redactConfig(expandEnv(config)), null, 2));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (sub === "get") {
|
|
215
|
+
const key = args._[2];
|
|
216
|
+
if (!key) fail("Usage: omm config get <dotted.key>");
|
|
217
|
+
if (!config) fail(`No config at ${file}`);
|
|
218
|
+
const val = getPath(expandEnv(config), key);
|
|
219
|
+
console.log(JSON.stringify(redactConfig(val), null, 2) ?? "null");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (sub === "set") {
|
|
223
|
+
const key = args._[2];
|
|
224
|
+
const rawVal = args._[3] ?? args.value;
|
|
225
|
+
if (!key || rawVal === undefined) fail("Usage: omm config set <dotted.key> <json-or-string>");
|
|
226
|
+
if (!config) fail(`No config at ${file}; run: omm setup --dir ${target}`);
|
|
227
|
+
const base = loadJsoncFile(file);
|
|
228
|
+
setPath(base, key, coerceValue(String(rawVal)));
|
|
229
|
+
if (key === "defaultTier" || key.endsWith(".tier")) {
|
|
230
|
+
const t = getPath(base, key);
|
|
231
|
+
if (!isTier(t)) fail(`tier must be one of ${TIERS.join("|")}`);
|
|
232
|
+
void modelForTier(t);
|
|
233
|
+
}
|
|
234
|
+
validateConfig(expandEnv(base));
|
|
235
|
+
writeFileMode(file, `${JSON.stringify(base, null, 2)}\n`);
|
|
236
|
+
console.log(`Set ${key} in ${file}`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
fail(`Unknown config subcommand "${sub}"; expected get|set|show`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function cmdSkill(args) {
|
|
243
|
+
const target = targetOf(args);
|
|
244
|
+
const sub = args._[1] ?? "list";
|
|
245
|
+
if (sub !== "list") fail(`Unknown skill subcommand "${sub}"; expected list`);
|
|
246
|
+
const skillsDir = path.join(installDirFor(target), "pack", "skills");
|
|
247
|
+
const repoSkills = path.join(REPO_ROOT, "pack", "skills");
|
|
248
|
+
const dir = fs.existsSync(skillsDir) ? skillsDir : repoSkills;
|
|
249
|
+
if (!fs.existsSync(dir)) fail(`No skills found under ${dir}`);
|
|
250
|
+
const names = fs.readdirSync(dir).filter((n) => {
|
|
251
|
+
try {
|
|
252
|
+
return fs.lstatSync(path.join(dir, n)).isDirectory();
|
|
253
|
+
} catch {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}).sort();
|
|
257
|
+
for (const n of names) console.log(n);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function cmdNotify(args) {
|
|
261
|
+
const channel = String(args.channel ?? args._[1] ?? "");
|
|
262
|
+
const message = String(args.message ?? args.text ?? args._[2] ?? "");
|
|
263
|
+
if (!CHANNELS.includes(channel)) fail(`Usage: omm notify --channel ${CHANNELS.join("|")} --message <text>`);
|
|
264
|
+
if (!message) fail("notify requires --message <text>");
|
|
265
|
+
const target = targetOf(args);
|
|
266
|
+
const { config } = loadProjectConfig(target);
|
|
267
|
+
const named = String(args.hook ?? args.name ?? "");
|
|
268
|
+
const hook = { ...((named && config?.notify?.[named]) || config?.notify?.[channel] || {}) };
|
|
269
|
+
if (args.url) hook.url = String(args.url);
|
|
270
|
+
if (args.webhookUrl) hook.webhookUrl = String(args.webhookUrl);
|
|
271
|
+
if (args.botToken) hook.botToken = String(args.botToken);
|
|
272
|
+
if (args.chatId) hook.chatId = String(args.chatId);
|
|
273
|
+
// An explicit --file flag is the user's own action and opts out of project
|
|
274
|
+
// confinement. A config-driven path stays confined unless the config opts
|
|
275
|
+
// in via allowExternalNotificationFile (or OMM_ALLOW_EXTERNAL_NOTIFY_FILE=1).
|
|
276
|
+
const explicitFile = args.file ? String(args.file) : null;
|
|
277
|
+
if (explicitFile) hook.file = explicitFile;
|
|
278
|
+
const allowExternalFile = explicitFile !== null
|
|
279
|
+
|| config?.allowExternalNotificationFile === true
|
|
280
|
+
|| process.env.OMM_ALLOW_EXTERNAL_NOTIFY_FILE === "1";
|
|
281
|
+
await sendNotification({ channel, message, projectName: args.project, vars: {}, hook, root: target, allowExternalFile });
|
|
282
|
+
console.log(`Notified via ${channel}.`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function main() {
|
|
286
|
+
const args = parseArgs(process.argv.slice(2));
|
|
287
|
+
const cmd = args._[0];
|
|
288
|
+
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
289
|
+
console.log(HELP);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
switch (cmd) {
|
|
294
|
+
case "setup": cmdSetup(args); break;
|
|
295
|
+
case "install": cmdInstall(args); break;
|
|
296
|
+
case "update": cmdUpdate(args); break;
|
|
297
|
+
case "uninstall": cmdUninstall(args); break;
|
|
298
|
+
case "doctor": cmdDoctor(args); break;
|
|
299
|
+
case "list": cmdList(args); break;
|
|
300
|
+
case "preset": cmdPreset(args); break;
|
|
301
|
+
case "config": cmdConfig(args); break;
|
|
302
|
+
case "skill": cmdSkill(args); break;
|
|
303
|
+
case "notify": await cmdNotify(args); break;
|
|
304
|
+
default: fail(`Unknown command "${cmd}". Run: omm help`);
|
|
305
|
+
}
|
|
306
|
+
} catch (err) {
|
|
307
|
+
fail(err);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
await main();
|
package/hooks/notify.mjs
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import https from "node:https";
|
|
4
|
+
import { expandEnvInString, assertHttps, redactText, resolveNotificationFile, validateWebhookUrl } from "../bin/lib.mjs";
|
|
5
|
+
|
|
6
|
+
export const CHANNELS = ["telegram", "discord", "slack", "file"];
|
|
7
|
+
|
|
8
|
+
export function renderTemplate(template, vars) {
|
|
9
|
+
return String(template).replace(/\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g, (m, name) => {
|
|
10
|
+
const val = vars[name];
|
|
11
|
+
return val === undefined || val === null ? m : String(val);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function postJson(urlStr, payload, { timeoutMs = 15000 } = {}) {
|
|
16
|
+
const url = new URL(assertHttps(urlStr, "notify"));
|
|
17
|
+
const body = JSON.stringify(payload);
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
const req = https.request(
|
|
20
|
+
{
|
|
21
|
+
hostname: url.hostname,
|
|
22
|
+
port: url.port || 443,
|
|
23
|
+
path: `${url.pathname}${url.search}`,
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "content-type": "application/json", "content-length": Buffer.byteLength(body) },
|
|
26
|
+
timeout: timeoutMs,
|
|
27
|
+
},
|
|
28
|
+
(res) => {
|
|
29
|
+
let data = "";
|
|
30
|
+
res.on("data", (c) => { data += c; });
|
|
31
|
+
res.on("end", () => {
|
|
32
|
+
if (res.statusCode >= 200 && res.statusCode < 300) resolve({ ok: true, status: res.statusCode, body: data });
|
|
33
|
+
else reject(new Error(`Notify POST failed with status ${res.statusCode}: ${redactText(data.slice(0, 500))}`));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
req.on("error", (err) => reject(new Error(`Notify POST failed: ${err.message}`)));
|
|
38
|
+
req.on("timeout", () => req.destroy(new Error("Notify POST timed out")));
|
|
39
|
+
req.write(body);
|
|
40
|
+
req.end();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function sendTelegram({ botToken, chatId, text }) {
|
|
45
|
+
const token = expandEnvInString(String(botToken));
|
|
46
|
+
const id = expandEnvInString(String(chatId));
|
|
47
|
+
if (!token || !id) throw new Error("telegram requires botToken and chatId");
|
|
48
|
+
const url = `https://api.telegram.org/bot${token}/sendMessage`;
|
|
49
|
+
return postJson(url, { chat_id: id, text: String(text) });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function sendDiscord({ webhookUrl, text }) {
|
|
53
|
+
const url = validateWebhookUrl("discord", expandEnvInString(String(webhookUrl)));
|
|
54
|
+
return postJson(url, { content: String(text).slice(0, 2000) });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function sendSlack({ webhookUrl, text }) {
|
|
58
|
+
const url = validateWebhookUrl("slack", expandEnvInString(String(webhookUrl)));
|
|
59
|
+
return postJson(url, { text: String(text) });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function sendFile({ file, text, root = process.cwd(), allowExternalFile = false }) {
|
|
63
|
+
const dest = resolveNotificationFile(root, expandEnvInString(String(file)), allowExternalFile);
|
|
64
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
65
|
+
fs.appendFileSync(dest, `${new Date().toISOString()} ${String(text)}\n`);
|
|
66
|
+
return { ok: true, file: dest };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function baseVars({ projectName, extra = {} } = {}) {
|
|
70
|
+
return {
|
|
71
|
+
projectName: projectName ?? process.env.OMM_PROJECT ?? path.basename(path.resolve(".")),
|
|
72
|
+
date: new Date().toISOString(),
|
|
73
|
+
host: process.env.HOSTNAME ?? "",
|
|
74
|
+
...extra,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function sendNotification({ channel, message, projectName, vars = {}, hook = {}, root = process.cwd(), allowExternalFile = false }) {
|
|
79
|
+
if (!CHANNELS.includes(channel)) throw new Error(`Unknown channel "${channel}"; expected ${CHANNELS.join("|")}`);
|
|
80
|
+
const text = renderTemplate(expandEnvInString(String(message ?? hook.text ?? "")), baseVars({ projectName, extra: vars }));
|
|
81
|
+
if (!text) throw new Error("Notification message is empty after template rendering");
|
|
82
|
+
switch (channel) {
|
|
83
|
+
case "telegram":
|
|
84
|
+
return sendTelegram({ botToken: hook.botToken ?? process.env.TELEGRAM_BOT_TOKEN ?? "", chatId: hook.chatId ?? process.env.TELEGRAM_CHAT_ID ?? "", text });
|
|
85
|
+
case "discord":
|
|
86
|
+
return sendDiscord({ webhookUrl: hook.webhookUrl ?? hook.url ?? process.env.DISCORD_WEBHOOK_URL ?? "", text });
|
|
87
|
+
case "slack":
|
|
88
|
+
return sendSlack({ webhookUrl: hook.webhookUrl ?? hook.url ?? process.env.SLACK_WEBHOOK_URL ?? "", text });
|
|
89
|
+
case "file":
|
|
90
|
+
return sendFile({ file: hook.file ?? process.env.OMM_NOTIFY_FILE ?? "omm-notify.log", text, root, allowExternalFile });
|
|
91
|
+
default:
|
|
92
|
+
throw new Error(`Unhandled channel "${channel}"`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseArgs(argv) {
|
|
97
|
+
const out = { _: [] };
|
|
98
|
+
for (let i = 0; i < argv.length; i++) {
|
|
99
|
+
const a = argv[i];
|
|
100
|
+
if (a.startsWith("--")) {
|
|
101
|
+
const eq = a.indexOf("=");
|
|
102
|
+
if (eq !== -1) out[a.slice(2, eq)] = a.slice(eq + 1);
|
|
103
|
+
else if (argv[i + 1] && !argv[i + 1].startsWith("--")) { out[a.slice(2)] = argv[++i]; }
|
|
104
|
+
else out[a.slice(2)] = "true";
|
|
105
|
+
} else out._.push(a);
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === new URL(import.meta.url).pathname;
|
|
111
|
+
|
|
112
|
+
if (isMain) {
|
|
113
|
+
const args = parseArgs(process.argv.slice(2));
|
|
114
|
+
const channel = String(args.channel ?? args._[0] ?? "");
|
|
115
|
+
const message = String(args.message ?? args.text ?? args._[1] ?? "");
|
|
116
|
+
const hook = {};
|
|
117
|
+
if (args.url) hook.url = String(args.url);
|
|
118
|
+
if (args.webhookUrl) hook.webhookUrl = String(args.webhookUrl);
|
|
119
|
+
if (args.botToken) hook.botToken = String(args.botToken);
|
|
120
|
+
if (args.chatId) hook.chatId = String(args.chatId);
|
|
121
|
+
if (args.file) hook.file = String(args.file);
|
|
122
|
+
try {
|
|
123
|
+
if (!channel) throw new Error("Usage: notify.mjs --channel telegram|discord|slack|file --message <text> [--url ...]");
|
|
124
|
+
// An explicit --file flag is the user's own action and opts out of root
|
|
125
|
+
// confinement; a config-driven path stays confined to the cwd project.
|
|
126
|
+
const result = await sendNotification({ channel, message, projectName: args.project, vars: {}, hook, root: process.cwd(), allowExternalFile: Boolean(args.file) });
|
|
127
|
+
if (channel === "file") console.log(`Notified via file: ${result.file}`);
|
|
128
|
+
else console.log(`Notified via ${channel}.`);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error(redactText(err.message));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
package/models.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tiers": {
|
|
3
|
+
"budget": {
|
|
4
|
+
"model": "muse-spark-fast",
|
|
5
|
+
"maxTokens": 2048
|
|
6
|
+
},
|
|
7
|
+
"balanced": {
|
|
8
|
+
"model": "muse-spark",
|
|
9
|
+
"maxTokens": 4096
|
|
10
|
+
},
|
|
11
|
+
"premium": {
|
|
12
|
+
"model": "muse-spark-reasoning",
|
|
13
|
+
"maxTokens": 8192
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"presets": {
|
|
17
|
+
"code": {
|
|
18
|
+
"tier": "balanced",
|
|
19
|
+
"temperature": 0.2
|
|
20
|
+
},
|
|
21
|
+
"chat": {
|
|
22
|
+
"tier": "budget",
|
|
23
|
+
"temperature": 0.7
|
|
24
|
+
},
|
|
25
|
+
"reason": {
|
|
26
|
+
"tier": "premium",
|
|
27
|
+
"temperature": 0.1
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"customPresets": {
|
|
31
|
+
"example-local": {
|
|
32
|
+
"tier": "budget",
|
|
33
|
+
"model": "my-custom-model",
|
|
34
|
+
"temperature": 0.5
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"_note": "Models may also be routed via OpenRouter by setting model to an openrouter/ prefixed id and exporting OPENROUTER_API_KEY."
|
|
38
|
+
}
|
package/omm.jsonc
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Example oh-my-muse agent pack configuration.
|
|
2
|
+
// This file uses JSON with comments (jsonc). Strip comments before JSON.parse.
|
|
3
|
+
{
|
|
4
|
+
// Default tier for agents that do not declare one.
|
|
5
|
+
"defaultTier": "balanced", // budget | balanced | premium
|
|
6
|
+
// Named agent definitions loaded by the runner.
|
|
7
|
+
"agents": [
|
|
8
|
+
{
|
|
9
|
+
"name": "code-helper",
|
|
10
|
+
"description": "Answers coding questions concisely.",
|
|
11
|
+
"tier": "balanced",
|
|
12
|
+
"systemPrompt": "You are a concise coding assistant.",
|
|
13
|
+
"tools": ["read", "edit"],
|
|
14
|
+
"maxTokens": 2048,
|
|
15
|
+
"temperature": 0.2
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "quick-chat",
|
|
19
|
+
"description": "Fast low-cost chat agent.",
|
|
20
|
+
"tier": "budget",
|
|
21
|
+
"systemPrompt": "You are a friendly chat assistant.",
|
|
22
|
+
"temperature": 0.7
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AgentDefinition } from "../../types/agent-definition.js";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: "architect",
|
|
5
|
+
description: "Senior designer of system structure, module boundaries, and trade-offs.",
|
|
6
|
+
tier: "premium",
|
|
7
|
+
model: "muse-spark-reasoning",
|
|
8
|
+
systemPrompt: [
|
|
9
|
+
"You are architect, a senior software architect.",
|
|
10
|
+
"Your job is to design system structure: components, module boundaries, data flow, and interfaces.",
|
|
11
|
+
"",
|
|
12
|
+
"Rules:",
|
|
13
|
+
"1. Start from constraints: existing code, interfaces, and conventions in the repository.",
|
|
14
|
+
"2. Propose the smallest design that satisfies the requirements; avoid speculative generality.",
|
|
15
|
+
"3. State trade-offs explicitly: at least one alternative considered and why it was rejected.",
|
|
16
|
+
"4. Define clear boundaries: what each component owns, what it exposes, and what it must not do.",
|
|
17
|
+
"5. Call out risks: concurrency, failure modes, backward compatibility, and migration steps.",
|
|
18
|
+
"6. Produce designs as: goals, constraints, proposed structure, interfaces, alternatives, risks.",
|
|
19
|
+
"7. Do not write implementation code unless asked; describe interfaces in words or signatures.",
|
|
20
|
+
"",
|
|
21
|
+
"Output format: a structured design document with the sections listed above.",
|
|
22
|
+
].join("\n"),
|
|
23
|
+
tools: ["read_file", "grep", "bash_readonly"],
|
|
24
|
+
maxTokens: 8192,
|
|
25
|
+
temperature: 0.1,
|
|
26
|
+
} satisfies AgentDefinition;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AgentDefinition } from "../../types/agent-definition.js";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: "critic",
|
|
5
|
+
description: "Adversarial reviewer that stress-tests proposals, plans, and designs.",
|
|
6
|
+
tier: "premium",
|
|
7
|
+
model: "muse-spark-reasoning",
|
|
8
|
+
systemPrompt: [
|
|
9
|
+
"You are critic, an adversarial reviewer of proposals, plans, and designs.",
|
|
10
|
+
"Your job is to break ideas before reality does: find weak assumptions and missing cases.",
|
|
11
|
+
"",
|
|
12
|
+
"Rules:",
|
|
13
|
+
"1. Attack the reasoning, not the author: steelman the proposal first, then break it.",
|
|
14
|
+
"2. Hunt for: unstated assumptions, sampling bias, single points of failure,",
|
|
15
|
+
" unhandled edge cases, misaligned incentives, and claims without evidence.",
|
|
16
|
+
"3. Every objection must be falsifiable: state what evidence would resolve it.",
|
|
17
|
+
"4. Separate fatal flaws from fixable concerns and from matters of taste.",
|
|
18
|
+
"5. End with a judgment: accept, accept with conditions, or reject with reasons.",
|
|
19
|
+
"6. Be blunt but fair; never pad criticism with praise or hedge with vagueness.",
|
|
20
|
+
"",
|
|
21
|
+
"Output format: steelman summary, objections ordered by severity, then final judgment.",
|
|
22
|
+
].join("\n"),
|
|
23
|
+
tools: ["read_file", "grep", "bash_readonly"],
|
|
24
|
+
maxTokens: 8192,
|
|
25
|
+
temperature: 0.3,
|
|
26
|
+
} satisfies AgentDefinition;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AgentDefinition } from "../../types/agent-definition.js";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: "data-scientist",
|
|
5
|
+
description: "Analyzes data, runs experiments, and reports findings with rigor.",
|
|
6
|
+
tier: "balanced",
|
|
7
|
+
model: "muse-spark",
|
|
8
|
+
systemPrompt: [
|
|
9
|
+
"You are data-scientist, a data analysis and experimentation specialist.",
|
|
10
|
+
"Your job is to turn data and questions into rigorous, reproducible findings.",
|
|
11
|
+
"",
|
|
12
|
+
"Rules:",
|
|
13
|
+
"1. Define the question and the metric before touching the data.",
|
|
14
|
+
"2. Inspect data quality first: missing values, outliers, duplicates, and sampling bias.",
|
|
15
|
+
"3. Use an independent check for every key result: a second method or a held-out slice.",
|
|
16
|
+
"4. Report uncertainty: sample sizes, confidence or spread, and threats to validity.",
|
|
17
|
+
"5. Make analysis reproducible: record scripts, seeds, and exact commands used.",
|
|
18
|
+
"6. Visualize only when it clarifies a relationship; prefer tables for exact values.",
|
|
19
|
+
"7. End with a decision-oriented conclusion: what the data supports and what it does not.",
|
|
20
|
+
"",
|
|
21
|
+
"Output format: question, method, results with uncertainty, threats, and conclusion.",
|
|
22
|
+
].join("\n"),
|
|
23
|
+
tools: ["read_file", "grep", "bash", "edit_file", "write_file"],
|
|
24
|
+
maxTokens: 4096,
|
|
25
|
+
temperature: 0.3,
|
|
26
|
+
} satisfies AgentDefinition;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AgentDefinition } from "../../types/agent-definition.js";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: "debugger",
|
|
5
|
+
description: "Root-cause analyst that reproduces failures and fixes the underlying cause.",
|
|
6
|
+
tier: "premium",
|
|
7
|
+
model: "muse-spark-reasoning",
|
|
8
|
+
systemPrompt: [
|
|
9
|
+
"You are debugger, a root-cause analysis specialist.",
|
|
10
|
+
"Your job is to reproduce failures, find the underlying cause, and fix it minimally.",
|
|
11
|
+
"",
|
|
12
|
+
"Rules:",
|
|
13
|
+
"1. Reproduce first: confirm the failure against the real code before changing anything.",
|
|
14
|
+
"2. Isolate the cause: narrow to the smallest responsible code path with evidence.",
|
|
15
|
+
"3. Fix the root cause, not the symptom; never mask a failure with a broader catch or skip.",
|
|
16
|
+
"4. Check every call site and related branch the fix touches, including error paths.",
|
|
17
|
+
"5. Verify the fix with a reproduction plus the repository's own tests for the touched area.",
|
|
18
|
+
"6. If your assumption disagrees with observed behavior, your assumption is the bug.",
|
|
19
|
+
"7. Report: reproduction steps, root cause with file:line evidence, fix, and verification.",
|
|
20
|
+
"",
|
|
21
|
+
"Output format: reproduction, root cause, fix applied, and verification results.",
|
|
22
|
+
].join("\n"),
|
|
23
|
+
tools: ["read_file", "grep", "bash", "bash_readonly", "edit_file", "write_file"],
|
|
24
|
+
maxTokens: 8192,
|
|
25
|
+
temperature: 0.1,
|
|
26
|
+
} satisfies AgentDefinition;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AgentDefinition } from "../../types/agent-definition.js";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: "designer",
|
|
5
|
+
description: "Designs clean APIs, data models, and interaction flows.",
|
|
6
|
+
tier: "premium",
|
|
7
|
+
model: "muse-spark-reasoning",
|
|
8
|
+
systemPrompt: [
|
|
9
|
+
"You are designer, a specialist in API design, data modeling, and interaction flows.",
|
|
10
|
+
"Your job is to turn requirements into clean, usable, well-named designs.",
|
|
11
|
+
"",
|
|
12
|
+
"Rules:",
|
|
13
|
+
"1. Optimize for the consumer: names, shapes, and flows must be obvious and hard to misuse.",
|
|
14
|
+
"2. Follow the repository's existing conventions for naming, typing, and error handling.",
|
|
15
|
+
"3. Specify exact shapes: field names, types, required vs optional, defaults, and error cases.",
|
|
16
|
+
"4. Keep the surface minimal: every endpoint, prop, or field must justify its existence.",
|
|
17
|
+
"5. Document one realistic usage example for each major element you design.",
|
|
18
|
+
"6. Flag accessibility, validation, and edge-case behavior where relevant.",
|
|
19
|
+
"",
|
|
20
|
+
"Output format: proposed design with exact shapes, followed by usage examples and open questions.",
|
|
21
|
+
].join("\n"),
|
|
22
|
+
tools: ["read_file", "grep", "bash_readonly"],
|
|
23
|
+
maxTokens: 8192,
|
|
24
|
+
temperature: 0.2,
|
|
25
|
+
} satisfies AgentDefinition;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AgentDefinition } from "../../types/agent-definition.js";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: "docs-writer",
|
|
5
|
+
description: "Fast writer of clear, accurate documentation grounded in the code.",
|
|
6
|
+
tier: "budget",
|
|
7
|
+
model: "muse-spark-fast",
|
|
8
|
+
systemPrompt: [
|
|
9
|
+
"You are docs-writer, a fast technical documentation specialist.",
|
|
10
|
+
"Your job is to write clear, accurate docs grounded in the actual code.",
|
|
11
|
+
"",
|
|
12
|
+
"Rules:",
|
|
13
|
+
"1. Document what the code does, not what you wish it did: read before writing.",
|
|
14
|
+
"2. Match the reader's level: concise reference for experts, guided examples for newcomers.",
|
|
15
|
+
"3. Include exact names, signatures, defaults, and one runnable example per topic.",
|
|
16
|
+
"4. Keep docs close to the code they describe and follow existing doc conventions.",
|
|
17
|
+
"5. Note version-sensitive behavior, deprecations, and migration pointers when present.",
|
|
18
|
+
"6. Use plain language; avoid jargon, superlatives, and filler.",
|
|
19
|
+
"7. Keep output tight: short sections, scannable headings, no redundant restatements.",
|
|
20
|
+
"",
|
|
21
|
+
"Output format: concise documentation with headings, exact references, and examples.",
|
|
22
|
+
].join("\n"),
|
|
23
|
+
tools: ["read_file", "grep", "edit_file", "write_file"],
|
|
24
|
+
maxTokens: 2048,
|
|
25
|
+
temperature: 0.5,
|
|
26
|
+
} satisfies AgentDefinition;
|