coding-friend-cli 1.38.4 → 1.39.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/dist/{chunk-32UAW3NX.js → chunk-2EY7VVFT.js} +1 -1
- package/dist/{chunk-RCE4OWNX.js → chunk-463QKRBN.js} +1 -1
- package/dist/{chunk-ETTZRJ55.js → chunk-4CPUWHIN.js} +1 -1
- package/dist/{chunk-6NXZKAOV.js → chunk-7JQZRMOG.js} +37 -4
- package/dist/{chunk-5KHZQCZB.js → chunk-FQUIO5BW.js} +1 -1
- package/dist/chunk-GTOEIGDM.js +510 -0
- package/dist/{chunk-PLSJW7QJ.js → chunk-IV5OPG34.js} +47 -1
- package/dist/{chunk-5QCGEPQO.js → chunk-JSH6BMVZ.js} +1 -1
- package/dist/{chunk-S4ENKXXC.js → chunk-ODW27NAC.js} +8 -8
- package/dist/chunk-OFEC6OVI.js +99 -0
- package/dist/{chunk-GIH6E7GJ.js → chunk-VMODQCSL.js} +4 -53
- package/dist/{chunk-I2DQYPKT.js → chunk-WJHQUCOF.js} +162 -17
- package/dist/{chunk-VHUU3QDC.js → chunk-YPW67OGP.js} +1 -1
- package/dist/{chunk-BXSCYGWE.js → chunk-ZGIUKUL2.js} +51 -6
- package/dist/chunk-ZZNTNIM4.js +63 -0
- package/dist/{clean-MSRZSOSO.js → clean-LPWEBJJ5.js} +2 -2
- package/dist/{config-EX3B3HWO.js → config-VNSC2NAY.js} +15 -13
- package/dist/{dev-PTBNMKZT.js → dev-NRTDUQAR.js} +3 -3
- package/dist/{disable-MO7AXYYF.js → disable-NROSQUHR.js} +19 -4
- package/dist/{enable-4GXLQEHD.js → enable-26FFJGX5.js} +24 -4
- package/dist/{guide-HCR43LJN.js → guide-HCLEOAGR.js} +1 -1
- package/dist/{host-VIQ5YI3V.js → host-2JOPHLN3.js} +4 -3
- package/dist/index.js +46 -46
- package/dist/{init-S2K4CQO2.js → init-MHHII5EP.js} +54 -18
- package/dist/{install-LUPE3VTC.js → install-OEFJLAWA.js} +52 -9
- package/dist/{learn-A3TP6PLL.js → learn-SSNBBDY3.js} +2 -2
- package/dist/{mcp-QNJTCZMS.js → mcp-J5VO3OH7.js} +13 -11
- package/dist/{mcp-serve-4VP4KDYG.js → mcp-serve-55DSE2J4.js} +4 -3
- package/dist/{mcp-serve-learn-UVXIDOEZ.js → mcp-serve-learn-EDBWNL4I.js} +3 -2
- package/dist/{memory-7GKTQSIW.js → memory-CDZKWOFL.js} +9 -7
- package/dist/{permission-VISPGMUK.js → permission-DPKPA2HU.js} +11 -3
- package/dist/{permissions-2SBACQHV.js → permissions-HZYUVKCY.js} +2 -2
- package/dist/{session-VDGNTICO.js → session-YWJDLNBP.js} +2 -2
- package/dist/{status-RHSL5MBU.js → status-4WMIQL7L.js} +18 -16
- package/dist/{statusline-AOZHEQSV.js → statusline-R5BNYK3H.js} +2 -2
- package/dist/{uninstall-QN6527IR.js → uninstall-EV7362NK.js} +52 -8
- package/dist/{update-ZQG25VOJ.js → update-YQBBAIIF.js} +10 -7
- package/dist/{update-check-7LKJLJPM.js → update-check-EJSGVGO6.js} +1 -1
- package/package.json +1 -1
- package/dist/chunk-RZRT7NGT.js +0 -18
|
@@ -1,17 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readOmpMcpJson,
|
|
3
|
+
removeOmpMcpEntry,
|
|
4
|
+
writeOmpMcpEntry
|
|
5
|
+
} from "./chunk-GTOEIGDM.js";
|
|
1
6
|
import {
|
|
2
7
|
runWithStderr
|
|
3
8
|
} from "./chunk-EVGXUDX4.js";
|
|
4
9
|
import {
|
|
5
10
|
resolvePath
|
|
6
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-IV5OPG34.js";
|
|
7
12
|
import {
|
|
8
13
|
log
|
|
9
14
|
} from "./chunk-NREZK463.js";
|
|
10
15
|
|
|
11
16
|
// src/lib/learn-prompts.ts
|
|
12
17
|
var MCP_NAME = "coding-friend-learn";
|
|
13
|
-
function
|
|
18
|
+
function hasOmpLearnEntry() {
|
|
19
|
+
const data = readOmpMcpJson();
|
|
20
|
+
return data !== null && MCP_NAME in data.mcpServers;
|
|
21
|
+
}
|
|
22
|
+
function registerLearnMcp(learnDir, host = "claude") {
|
|
14
23
|
const resolved = resolvePath(learnDir);
|
|
24
|
+
if (host === "omp") {
|
|
25
|
+
try {
|
|
26
|
+
writeOmpMcpEntry(MCP_NAME, {
|
|
27
|
+
command: "npx",
|
|
28
|
+
args: ["-y", "coding-friend-cli", "mcp-serve-learn", resolved]
|
|
29
|
+
});
|
|
30
|
+
return true;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
const detail = error instanceof Error ? error.message : "unknown error";
|
|
33
|
+
log.warn(`Could not register MCP: ${detail}`);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
15
37
|
const result = runWithStderr("claude", [
|
|
16
38
|
"mcp",
|
|
17
39
|
"add",
|
|
@@ -39,11 +61,22 @@ function registerLearnMcp(learnDir) {
|
|
|
39
61
|
}
|
|
40
62
|
return true;
|
|
41
63
|
}
|
|
42
|
-
function isLearnMcpRegistered() {
|
|
64
|
+
function isLearnMcpRegistered(host = "claude") {
|
|
65
|
+
if (host === "omp") return hasOmpLearnEntry();
|
|
43
66
|
const result = runWithStderr("claude", ["mcp", "get", MCP_NAME]);
|
|
44
67
|
return result.exitCode === 0;
|
|
45
68
|
}
|
|
46
|
-
function unregisterLearnMcp() {
|
|
69
|
+
function unregisterLearnMcp(host = "claude") {
|
|
70
|
+
if (host === "omp") {
|
|
71
|
+
try {
|
|
72
|
+
removeOmpMcpEntry(MCP_NAME);
|
|
73
|
+
return !hasOmpLearnEntry();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const detail = error instanceof Error ? error.message : "unknown error";
|
|
76
|
+
log.warn(`Could not unregister MCP: ${detail}`);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
47
80
|
const result = runWithStderr("claude", [
|
|
48
81
|
"mcp",
|
|
49
82
|
"remove",
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getOmpExtensionPath
|
|
3
|
+
} from "./chunk-ZZNTNIM4.js";
|
|
4
|
+
import {
|
|
5
|
+
compareVersions
|
|
6
|
+
} from "./chunk-OFEC6OVI.js";
|
|
7
|
+
import {
|
|
8
|
+
readJson,
|
|
9
|
+
writeJson
|
|
10
|
+
} from "./chunk-5UVDWG5L.js";
|
|
11
|
+
import {
|
|
12
|
+
ompConfigYmlPath,
|
|
13
|
+
ompExtensionsDir,
|
|
14
|
+
ompMcpJsonPath,
|
|
15
|
+
ompProjectAgentsDir,
|
|
16
|
+
ompProjectConfigYmlPath,
|
|
17
|
+
ompProjectExtensionsDir,
|
|
18
|
+
ompUserAgentsDir,
|
|
19
|
+
pluginCachePath
|
|
20
|
+
} from "./chunk-IV5OPG34.js";
|
|
21
|
+
|
|
22
|
+
// src/lib/omp-config.ts
|
|
23
|
+
import {
|
|
24
|
+
existsSync,
|
|
25
|
+
mkdirSync,
|
|
26
|
+
readdirSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
rmSync,
|
|
29
|
+
writeFileSync
|
|
30
|
+
} from "fs";
|
|
31
|
+
import { dirname, join, resolve } from "path";
|
|
32
|
+
var EXTENSION_SHIM = "coding-friend.ts";
|
|
33
|
+
var MANAGED_START = "# coding-friend-managed";
|
|
34
|
+
var MANAGED_END = "# end coding-friend-managed";
|
|
35
|
+
var KEPT_MODELS = /* @__PURE__ */ new Set(["haiku", "sonnet", "opus"]);
|
|
36
|
+
var FALLBACK_CF_AGENT_NAMES = [
|
|
37
|
+
"cf-explorer",
|
|
38
|
+
"cf-implementer",
|
|
39
|
+
"cf-planner",
|
|
40
|
+
"cf-reviewer",
|
|
41
|
+
"cf-reviewer-plan",
|
|
42
|
+
"cf-reviewer-quality",
|
|
43
|
+
"cf-reviewer-reducer",
|
|
44
|
+
"cf-reviewer-rules",
|
|
45
|
+
"cf-reviewer-security",
|
|
46
|
+
"cf-reviewer-tests",
|
|
47
|
+
"cf-writer",
|
|
48
|
+
"cf-writer-deep"
|
|
49
|
+
];
|
|
50
|
+
function isPlainObject(value) {
|
|
51
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
function agentsDirForScope(scope) {
|
|
54
|
+
return scope === "user" ? ompUserAgentsDir() : ompProjectAgentsDir();
|
|
55
|
+
}
|
|
56
|
+
function extensionsDirForScope(scope) {
|
|
57
|
+
return scope === "user" ? ompExtensionsDir() : ompProjectExtensionsDir();
|
|
58
|
+
}
|
|
59
|
+
function configYmlForScope(scope) {
|
|
60
|
+
return scope === "user" ? ompConfigYmlPath() : ompProjectConfigYmlPath();
|
|
61
|
+
}
|
|
62
|
+
function parseFrontmatter(markdown) {
|
|
63
|
+
const normalized = markdown.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
64
|
+
const match = normalized.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
65
|
+
if (!match) {
|
|
66
|
+
return { frontmatter: {}, body: normalized };
|
|
67
|
+
}
|
|
68
|
+
const frontmatter = {};
|
|
69
|
+
const lines = match[1].split("\n");
|
|
70
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
71
|
+
const line = lines[index];
|
|
72
|
+
const simple = line.match(/^([A-Za-z0-9_-]+):(?:\s*(.*))?$/);
|
|
73
|
+
if (!simple) continue;
|
|
74
|
+
const [, key, rawValue = ""] = simple;
|
|
75
|
+
if (rawValue === ">") {
|
|
76
|
+
const folded = [];
|
|
77
|
+
while (index + 1 < lines.length && /^\s+/.test(lines[index + 1])) {
|
|
78
|
+
index += 1;
|
|
79
|
+
folded.push(lines[index].trim());
|
|
80
|
+
}
|
|
81
|
+
frontmatter[key] = folded.join(" ").replace(/\s+/g, " ").trim();
|
|
82
|
+
} else {
|
|
83
|
+
frontmatter[key] = rawValue.trim();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
frontmatter,
|
|
88
|
+
body: normalized.slice(match[0].length)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function yamlScalar(value) {
|
|
92
|
+
if (/[:#\n]/.test(value) || value === "" || /["']/.test(value)) {
|
|
93
|
+
return JSON.stringify(value);
|
|
94
|
+
}
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
function normalizeAgentBody(body) {
|
|
98
|
+
let text = body.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
99
|
+
if (text.startsWith("\n")) text = text.slice(1);
|
|
100
|
+
if (text.endsWith("\n\n")) text = text.slice(0, -1);
|
|
101
|
+
if (!text.endsWith("\n")) text += "\n";
|
|
102
|
+
return text;
|
|
103
|
+
}
|
|
104
|
+
function isCfAgentFile(name) {
|
|
105
|
+
return /^cf-.*\.md$/.test(name);
|
|
106
|
+
}
|
|
107
|
+
function listMdFiles(dir) {
|
|
108
|
+
if (!existsSync(dir)) return [];
|
|
109
|
+
try {
|
|
110
|
+
return readdirSync(dir, { withFileTypes: true }).filter(
|
|
111
|
+
(entry) => (entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(".md")
|
|
112
|
+
).map((entry) => entry.name).sort();
|
|
113
|
+
} catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function addCfAgentNames(dir, names) {
|
|
118
|
+
for (const filename of listMdFiles(dir)) {
|
|
119
|
+
if (!isCfAgentFile(filename)) continue;
|
|
120
|
+
names.add(filename.slice(0, -3));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function ourAgentNames(scope) {
|
|
124
|
+
const names = new Set(FALLBACK_CF_AGENT_NAMES);
|
|
125
|
+
const sourceDir = findOmpAgentSourceDir();
|
|
126
|
+
if (sourceDir) addCfAgentNames(sourceDir, names);
|
|
127
|
+
addCfAgentNames(agentsDirForScope(scope), names);
|
|
128
|
+
return [...names].sort();
|
|
129
|
+
}
|
|
130
|
+
function repoPluginCandidate(repoRoot, ...parts) {
|
|
131
|
+
const direct = resolve(repoRoot, ...parts);
|
|
132
|
+
const parent = resolve(repoRoot, "..", ...parts);
|
|
133
|
+
if (existsSync(direct)) return direct;
|
|
134
|
+
if (existsSync(parent)) return parent;
|
|
135
|
+
if (existsSync(resolve(repoRoot, "..", "plugin", "agents"))) return parent;
|
|
136
|
+
return direct;
|
|
137
|
+
}
|
|
138
|
+
function latestCachedPluginDir(cacheRoot = pluginCachePath()) {
|
|
139
|
+
if (!existsSync(cacheRoot)) return null;
|
|
140
|
+
if (existsSync(join(cacheRoot, "agents"))) return cacheRoot;
|
|
141
|
+
let dirs = [];
|
|
142
|
+
try {
|
|
143
|
+
dirs = readdirSync(cacheRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const versions = dirs.filter((name) => /^\d/.test(name)).sort(compareVersions).reverse();
|
|
148
|
+
if (versions[0]) return join(cacheRoot, versions[0]);
|
|
149
|
+
const rest = [...dirs].sort(compareVersions).reverse();
|
|
150
|
+
return rest[0] ? join(cacheRoot, rest[0]) : null;
|
|
151
|
+
}
|
|
152
|
+
function ensureNl(content) {
|
|
153
|
+
return content === "" || content.endsWith("\n") ? content : `${content}
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
156
|
+
function normalizeYml(content) {
|
|
157
|
+
return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
158
|
+
}
|
|
159
|
+
function findManagedRange(content) {
|
|
160
|
+
const markerStart = content.indexOf(MANAGED_START);
|
|
161
|
+
const markerEnd = content.indexOf(MANAGED_END);
|
|
162
|
+
if (markerStart === -1 || markerEnd === -1 || markerEnd < markerStart) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
let start = markerStart;
|
|
166
|
+
while (start > 0 && (content[start - 1] === " " || content[start - 1] === " ")) {
|
|
167
|
+
start -= 1;
|
|
168
|
+
}
|
|
169
|
+
let end = markerEnd + MANAGED_END.length;
|
|
170
|
+
if (content[end] === "\n") end += 1;
|
|
171
|
+
return { start, end };
|
|
172
|
+
}
|
|
173
|
+
function stripManagedBlock(content) {
|
|
174
|
+
const normalized = normalizeYml(content);
|
|
175
|
+
const range = findManagedRange(normalized);
|
|
176
|
+
if (!range) return ensureNl(normalized);
|
|
177
|
+
const before = normalized.slice(0, range.start).replace(/\n+$/, "");
|
|
178
|
+
const after = normalized.slice(range.end).replace(/^\n+/, "");
|
|
179
|
+
if (!before && !after) return "";
|
|
180
|
+
if (!after) return `${before}
|
|
181
|
+
`;
|
|
182
|
+
if (!before) return ensureNl(after);
|
|
183
|
+
return ensureNl(`${before}
|
|
184
|
+
|
|
185
|
+
${after}`);
|
|
186
|
+
}
|
|
187
|
+
function stripManagedBlocks(content) {
|
|
188
|
+
let current = normalizeYml(content);
|
|
189
|
+
for (let i = 0; i < 16; i += 1) {
|
|
190
|
+
const next = stripManagedBlock(current);
|
|
191
|
+
if (next === current) return current;
|
|
192
|
+
current = next;
|
|
193
|
+
}
|
|
194
|
+
return current;
|
|
195
|
+
}
|
|
196
|
+
function isBlockTaskKey(line) {
|
|
197
|
+
return /^task:\s*(#.*)?$/.test(line);
|
|
198
|
+
}
|
|
199
|
+
function isTopLevelKey(line) {
|
|
200
|
+
return /^[A-Za-z0-9_-]+:(\s|$)/.test(line);
|
|
201
|
+
}
|
|
202
|
+
function mappingEnd(lines, start) {
|
|
203
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
204
|
+
if (isTopLevelKey(lines[i])) return i;
|
|
205
|
+
}
|
|
206
|
+
return lines.length;
|
|
207
|
+
}
|
|
208
|
+
function mappingChildIndent(lines, start, end) {
|
|
209
|
+
for (let i = start + 1; i < end; i += 1) {
|
|
210
|
+
const match = lines[i].match(/^([ \t]+)\S/);
|
|
211
|
+
if (match) return match[1];
|
|
212
|
+
}
|
|
213
|
+
return " ";
|
|
214
|
+
}
|
|
215
|
+
function findChildKeyRange(lines, mappingStart, mappingEndIdx, indent, key) {
|
|
216
|
+
const prefix = `${indent}${key}:`;
|
|
217
|
+
for (let i = mappingStart + 1; i < mappingEndIdx; i += 1) {
|
|
218
|
+
const line = lines[i];
|
|
219
|
+
if (line !== prefix && !line.startsWith(`${prefix} `) && !line.startsWith(`${prefix} `)) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
let end = i + 1;
|
|
223
|
+
while (end < mappingEndIdx) {
|
|
224
|
+
const next = lines[end];
|
|
225
|
+
if (next.trim() === "") break;
|
|
226
|
+
const nextIndent = next.match(/^[ \t]*/)?.[0] ?? "";
|
|
227
|
+
if (nextIndent.length <= indent.length) break;
|
|
228
|
+
end += 1;
|
|
229
|
+
}
|
|
230
|
+
return { start: i, end };
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
function managedDisabledAgentsLine(names) {
|
|
235
|
+
return `disabledAgents: [${names.join(", ")}]`;
|
|
236
|
+
}
|
|
237
|
+
function managedDisabledAgentsBlock(names) {
|
|
238
|
+
return [
|
|
239
|
+
MANAGED_START,
|
|
240
|
+
"task:",
|
|
241
|
+
` ${managedDisabledAgentsLine(names)}`,
|
|
242
|
+
MANAGED_END
|
|
243
|
+
].join("\n");
|
|
244
|
+
}
|
|
245
|
+
function disabledAgentsLines(names, indent, managed) {
|
|
246
|
+
const line = `${indent}${managedDisabledAgentsLine(names)}`;
|
|
247
|
+
if (!managed) return [line];
|
|
248
|
+
return [`${indent}${MANAGED_START}`, line, `${indent}${MANAGED_END}`];
|
|
249
|
+
}
|
|
250
|
+
function hasTopLevelTaskKey(lines) {
|
|
251
|
+
return lines.some((line) => /^task:(\s|$)/.test(line));
|
|
252
|
+
}
|
|
253
|
+
function parseYamlName(raw) {
|
|
254
|
+
const trimmed = raw.trim();
|
|
255
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
256
|
+
return trimmed.slice(1, -1);
|
|
257
|
+
}
|
|
258
|
+
return trimmed;
|
|
259
|
+
}
|
|
260
|
+
function parseFlowNameList(inside) {
|
|
261
|
+
if (!inside.trim()) return [];
|
|
262
|
+
return inside.split(",").map(parseYamlName).filter(Boolean);
|
|
263
|
+
}
|
|
264
|
+
function parseDisabledAgentsAt(lines, mappingStart, mappingEndIdx, indent) {
|
|
265
|
+
const range = findChildKeyRange(
|
|
266
|
+
lines,
|
|
267
|
+
mappingStart,
|
|
268
|
+
mappingEndIdx,
|
|
269
|
+
indent,
|
|
270
|
+
"disabledAgents"
|
|
271
|
+
);
|
|
272
|
+
if (!range) return [];
|
|
273
|
+
const flow = lines[range.start].match(/disabledAgents:\s*\[(.*)\]\s*(#.*)?$/);
|
|
274
|
+
if (flow) return parseFlowNameList(flow[1]);
|
|
275
|
+
const names = [];
|
|
276
|
+
for (let i = range.start + 1; i < range.end; i += 1) {
|
|
277
|
+
const item = lines[i].match(/^\s*-\s+(.+?)\s*$/);
|
|
278
|
+
if (item) names.push(parseYamlName(item[1]));
|
|
279
|
+
}
|
|
280
|
+
return names;
|
|
281
|
+
}
|
|
282
|
+
function readDisabledAgentNames(content) {
|
|
283
|
+
const lines = normalizeYml(content).split("\n");
|
|
284
|
+
const taskIdx = lines.findIndex(isBlockTaskKey);
|
|
285
|
+
if (taskIdx === -1) return [];
|
|
286
|
+
const end = mappingEnd(lines, taskIdx);
|
|
287
|
+
const indent = mappingChildIndent(lines, taskIdx, end);
|
|
288
|
+
return parseDisabledAgentsAt(lines, taskIdx, end, indent);
|
|
289
|
+
}
|
|
290
|
+
function mergeAgentNames(existing, ours) {
|
|
291
|
+
const seen = /* @__PURE__ */ new Set();
|
|
292
|
+
const merged = [];
|
|
293
|
+
for (const name of [...existing, ...ours]) {
|
|
294
|
+
if (!name || seen.has(name)) continue;
|
|
295
|
+
seen.add(name);
|
|
296
|
+
merged.push(name);
|
|
297
|
+
}
|
|
298
|
+
return merged;
|
|
299
|
+
}
|
|
300
|
+
function writeDisabledAgents(content, names, managed) {
|
|
301
|
+
const stripped = stripManagedBlocks(content);
|
|
302
|
+
const trimmed = stripped.replace(/\n+$/, "");
|
|
303
|
+
const lines = trimmed === "" ? [] : trimmed.split("\n");
|
|
304
|
+
const taskIdx = lines.findIndex(isBlockTaskKey);
|
|
305
|
+
if (taskIdx === -1) {
|
|
306
|
+
if (hasTopLevelTaskKey(lines) || names.length === 0) {
|
|
307
|
+
return ensureNl(stripped);
|
|
308
|
+
}
|
|
309
|
+
return `${trimmed}${trimmed ? "\n\n" : ""}${managedDisabledAgentsBlock(names)}
|
|
310
|
+
`;
|
|
311
|
+
}
|
|
312
|
+
const end = mappingEnd(lines, taskIdx);
|
|
313
|
+
const indent = mappingChildIndent(lines, taskIdx, end);
|
|
314
|
+
const existing = findChildKeyRange(
|
|
315
|
+
lines,
|
|
316
|
+
taskIdx,
|
|
317
|
+
end,
|
|
318
|
+
indent,
|
|
319
|
+
"disabledAgents"
|
|
320
|
+
);
|
|
321
|
+
if (names.length === 0) {
|
|
322
|
+
if (existing) {
|
|
323
|
+
lines.splice(existing.start, existing.end - existing.start);
|
|
324
|
+
}
|
|
325
|
+
return ensureNl(lines.join("\n"));
|
|
326
|
+
}
|
|
327
|
+
const next = disabledAgentsLines(names, indent, managed);
|
|
328
|
+
if (existing) {
|
|
329
|
+
lines.splice(existing.start, existing.end - existing.start, ...next);
|
|
330
|
+
} else {
|
|
331
|
+
lines.splice(taskIdx + 1, 0, ...next);
|
|
332
|
+
}
|
|
333
|
+
return ensureNl(lines.join("\n"));
|
|
334
|
+
}
|
|
335
|
+
function upsertDisabledAgents(content, ours) {
|
|
336
|
+
return writeDisabledAgents(
|
|
337
|
+
content,
|
|
338
|
+
mergeAgentNames(readDisabledAgentNames(content), ours),
|
|
339
|
+
true
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
function removeOurDisabledAgents(content, ours) {
|
|
343
|
+
const oursSet = new Set(ours);
|
|
344
|
+
const remaining = readDisabledAgentNames(content).filter(
|
|
345
|
+
(name) => !oursSet.has(name)
|
|
346
|
+
);
|
|
347
|
+
return writeDisabledAgents(content, remaining, false);
|
|
348
|
+
}
|
|
349
|
+
function writeTextFile(filePath, content) {
|
|
350
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
351
|
+
writeFileSync(filePath, ensureNl(content), "utf8");
|
|
352
|
+
}
|
|
353
|
+
function readOmpMcpJson(filePath = ompMcpJsonPath()) {
|
|
354
|
+
const data = readJson(filePath);
|
|
355
|
+
if (!data || !isPlainObject(data.mcpServers)) return null;
|
|
356
|
+
return data;
|
|
357
|
+
}
|
|
358
|
+
function writeOmpMcpEntry(name, server, filePath = ompMcpJsonPath()) {
|
|
359
|
+
const existing = readJson(filePath);
|
|
360
|
+
const data = isPlainObject(existing) ? { ...existing } : {};
|
|
361
|
+
const mcpServers = isPlainObject(
|
|
362
|
+
data.mcpServers
|
|
363
|
+
) ? { ...data.mcpServers } : {};
|
|
364
|
+
mcpServers[name] = server;
|
|
365
|
+
data.mcpServers = mcpServers;
|
|
366
|
+
writeJson(filePath, data);
|
|
367
|
+
}
|
|
368
|
+
function removeOmpMcpEntry(name, filePath = ompMcpJsonPath()) {
|
|
369
|
+
const existing = readJson(filePath);
|
|
370
|
+
if (!existing || !isPlainObject(existing.mcpServers)) return;
|
|
371
|
+
const mcpServers = {
|
|
372
|
+
...existing.mcpServers
|
|
373
|
+
};
|
|
374
|
+
if (!(name in mcpServers)) return;
|
|
375
|
+
delete mcpServers[name];
|
|
376
|
+
writeJson(filePath, { ...existing, mcpServers });
|
|
377
|
+
}
|
|
378
|
+
function convertClaudeAgentToOmp(sourceMd) {
|
|
379
|
+
const { frontmatter, body } = parseFrontmatter(sourceMd);
|
|
380
|
+
const name = frontmatter.name?.trim();
|
|
381
|
+
const description = frontmatter.description ?? "";
|
|
382
|
+
if (!name || !description.trim()) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
"Agent markdown is missing required frontmatter name or description"
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
const lines = [
|
|
388
|
+
"---",
|
|
389
|
+
`name: ${yamlScalar(name)}`,
|
|
390
|
+
`description: ${JSON.stringify(description)}`
|
|
391
|
+
];
|
|
392
|
+
const model = frontmatter.model?.trim();
|
|
393
|
+
if (model && KEPT_MODELS.has(model)) {
|
|
394
|
+
lines.push(`model: ${model}`);
|
|
395
|
+
}
|
|
396
|
+
lines.push("---");
|
|
397
|
+
return `${lines.join("\n")}
|
|
398
|
+
|
|
399
|
+
${normalizeAgentBody(body)}`;
|
|
400
|
+
}
|
|
401
|
+
function findOmpAgentSourceDir(repoRoot = process.cwd()) {
|
|
402
|
+
const repoAgents = repoPluginCandidate(repoRoot, "plugin", "agents");
|
|
403
|
+
if (existsSync(repoAgents)) return repoAgents;
|
|
404
|
+
const cached = latestCachedPluginDir();
|
|
405
|
+
if (!cached) return null;
|
|
406
|
+
const cachedAgents = join(cached, "agents");
|
|
407
|
+
if (existsSync(cachedAgents)) return cachedAgents;
|
|
408
|
+
const nested = join(cached, "plugin", "agents");
|
|
409
|
+
return existsSync(nested) ? nested : null;
|
|
410
|
+
}
|
|
411
|
+
function deployOmpAgents(scope, options) {
|
|
412
|
+
const sourceDir = findOmpAgentSourceDir();
|
|
413
|
+
const deployed = [];
|
|
414
|
+
const skipped = [];
|
|
415
|
+
if (!sourceDir) return { deployed, skipped };
|
|
416
|
+
const files = listMdFiles(sourceDir);
|
|
417
|
+
const dryRun = options?.dryRun === true;
|
|
418
|
+
const targetDir = agentsDirForScope(scope);
|
|
419
|
+
let printedFirst = false;
|
|
420
|
+
if (!dryRun) mkdirSync(targetDir, { recursive: true });
|
|
421
|
+
for (const filename of files) {
|
|
422
|
+
let converted;
|
|
423
|
+
try {
|
|
424
|
+
converted = convertClaudeAgentToOmp(
|
|
425
|
+
readFileSync(join(sourceDir, filename), "utf8")
|
|
426
|
+
);
|
|
427
|
+
} catch {
|
|
428
|
+
skipped.push(filename);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (dryRun) {
|
|
432
|
+
if (!printedFirst) {
|
|
433
|
+
console.log(converted);
|
|
434
|
+
printedFirst = true;
|
|
435
|
+
}
|
|
436
|
+
skipped.push(filename);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
writeFileSync(join(targetDir, filename), converted, "utf8");
|
|
440
|
+
deployed.push(filename);
|
|
441
|
+
}
|
|
442
|
+
return { deployed, skipped };
|
|
443
|
+
}
|
|
444
|
+
function removeOmpAgents(scope) {
|
|
445
|
+
const targetDir = agentsDirForScope(scope);
|
|
446
|
+
if (!existsSync(targetDir)) return;
|
|
447
|
+
for (const entry of readdirSync(targetDir, { withFileTypes: true })) {
|
|
448
|
+
if (!(entry.isFile() || entry.isSymbolicLink()) || !isCfAgentFile(entry.name)) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
rmSync(join(targetDir, entry.name));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function writeOmpExtensionEntry(scope, repoRoot = process.cwd()) {
|
|
455
|
+
const extensionPath = getOmpExtensionPath(repoRoot);
|
|
456
|
+
if (!extensionPath || !existsSync(extensionPath)) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
extensionPath ? `omp extension not found: ${extensionPath}. Run from the coding-friend repo or install the plugin first.` : "omp extension not found. Run from the coding-friend repo or install the plugin first."
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
const pluginRoot = dirname(dirname(extensionPath));
|
|
462
|
+
const shim = [
|
|
463
|
+
`// CODING_FRIEND_PLUGIN_ROOT=${pluginRoot}`,
|
|
464
|
+
`export { default } from ${JSON.stringify(extensionPath)};`,
|
|
465
|
+
""
|
|
466
|
+
].join("\n");
|
|
467
|
+
writeTextFile(join(extensionsDirForScope(scope), EXTENSION_SHIM), shim);
|
|
468
|
+
}
|
|
469
|
+
function removeOmpExtensionEntry(scope) {
|
|
470
|
+
const filePath = join(extensionsDirForScope(scope), EXTENSION_SHIM);
|
|
471
|
+
if (existsSync(filePath)) rmSync(filePath);
|
|
472
|
+
}
|
|
473
|
+
function setOmpAgentEnabled(scope) {
|
|
474
|
+
const filePath = configYmlForScope(scope);
|
|
475
|
+
if (!existsSync(filePath)) return false;
|
|
476
|
+
const current = readFileSync(filePath, "utf8");
|
|
477
|
+
const next = removeOurDisabledAgents(current, ourAgentNames(scope));
|
|
478
|
+
if (next === current) return false;
|
|
479
|
+
writeFileSync(filePath, next, "utf8");
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
function setOmpAgentDisabled(scope) {
|
|
483
|
+
const filePath = configYmlForScope(scope);
|
|
484
|
+
const current = existsSync(filePath) ? readFileSync(filePath, "utf8") : "";
|
|
485
|
+
writeTextFile(filePath, upsertDisabledAgents(current, ourAgentNames(scope)));
|
|
486
|
+
}
|
|
487
|
+
function isOmpAgentInstalled(scope) {
|
|
488
|
+
const dir = agentsDirForScope(scope);
|
|
489
|
+
if (!existsSync(dir)) return false;
|
|
490
|
+
try {
|
|
491
|
+
return readdirSync(dir, { withFileTypes: true }).some(
|
|
492
|
+
(entry) => (entry.isFile() || entry.isSymbolicLink()) && isCfAgentFile(entry.name)
|
|
493
|
+
);
|
|
494
|
+
} catch {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export {
|
|
500
|
+
readOmpMcpJson,
|
|
501
|
+
writeOmpMcpEntry,
|
|
502
|
+
removeOmpMcpEntry,
|
|
503
|
+
deployOmpAgents,
|
|
504
|
+
removeOmpAgents,
|
|
505
|
+
writeOmpExtensionEntry,
|
|
506
|
+
removeOmpExtensionEntry,
|
|
507
|
+
setOmpAgentEnabled,
|
|
508
|
+
setOmpAgentDisabled,
|
|
509
|
+
isOmpAgentInstalled
|
|
510
|
+
};
|
|
@@ -102,6 +102,43 @@ function codexInstalledPluginsPath() {
|
|
|
102
102
|
function codexAgentsDir() {
|
|
103
103
|
return join(codexConfigDir(), "agents");
|
|
104
104
|
}
|
|
105
|
+
function ompHome() {
|
|
106
|
+
const env = process.env.OMP_HOME?.trim();
|
|
107
|
+
if (!env) return join(homedir(), ".omp");
|
|
108
|
+
if (env === "~") return homedir();
|
|
109
|
+
if (env.startsWith("~/")) return join(homedir(), env.slice(2));
|
|
110
|
+
return env;
|
|
111
|
+
}
|
|
112
|
+
function ompAgentDir() {
|
|
113
|
+
return join(ompHome(), "agent");
|
|
114
|
+
}
|
|
115
|
+
function ompUserAgentsDir() {
|
|
116
|
+
return join(ompAgentDir(), "agents");
|
|
117
|
+
}
|
|
118
|
+
function ompMcpJsonPath() {
|
|
119
|
+
return join(ompAgentDir(), "mcp.json");
|
|
120
|
+
}
|
|
121
|
+
function ompConfigYmlPath() {
|
|
122
|
+
return join(ompAgentDir(), "config.yml");
|
|
123
|
+
}
|
|
124
|
+
function ompExtensionsDir() {
|
|
125
|
+
return join(ompAgentDir(), "extensions");
|
|
126
|
+
}
|
|
127
|
+
function ompProjectDir() {
|
|
128
|
+
return resolve(process.cwd(), ".omp");
|
|
129
|
+
}
|
|
130
|
+
function ompProjectMcpJsonPath() {
|
|
131
|
+
return resolve(ompProjectDir(), "mcp.json");
|
|
132
|
+
}
|
|
133
|
+
function ompProjectConfigYmlPath() {
|
|
134
|
+
return resolve(ompProjectDir(), "config.yml");
|
|
135
|
+
}
|
|
136
|
+
function ompProjectAgentsDir() {
|
|
137
|
+
return resolve(ompProjectDir(), "agents");
|
|
138
|
+
}
|
|
139
|
+
function ompProjectExtensionsDir() {
|
|
140
|
+
return resolve(ompProjectDir(), "extensions");
|
|
141
|
+
}
|
|
105
142
|
|
|
106
143
|
export {
|
|
107
144
|
resolvePath,
|
|
@@ -123,5 +160,14 @@ export {
|
|
|
123
160
|
claudeSessionDir,
|
|
124
161
|
codexConfigTomlPath,
|
|
125
162
|
codexInstalledPluginsPath,
|
|
126
|
-
codexAgentsDir
|
|
163
|
+
codexAgentsDir,
|
|
164
|
+
ompUserAgentsDir,
|
|
165
|
+
ompMcpJsonPath,
|
|
166
|
+
ompConfigYmlPath,
|
|
167
|
+
ompExtensionsDir,
|
|
168
|
+
ompProjectDir,
|
|
169
|
+
ompProjectMcpJsonPath,
|
|
170
|
+
ompProjectConfigYmlPath,
|
|
171
|
+
ompProjectAgentsDir,
|
|
172
|
+
ompProjectExtensionsDir
|
|
127
173
|
};
|