mister-wolf 2.20.1 → 2.21.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 +1 -1
- package/README.ru.md +1 -1
- package/dist/adapters/platforms/opencode-adapter.d.ts +36 -3
- package/dist/adapters/platforms/opencode-adapter.d.ts.map +1 -1
- package/dist/adapters/platforms/opencode-adapter.js +380 -165
- package/dist/adapters/platforms/opencode-adapter.js.map +1 -1
- package/dist/adapters/platforms/opencode-config-keys.d.ts +43 -11
- package/dist/adapters/platforms/opencode-config-keys.d.ts.map +1 -1
- package/dist/adapters/platforms/opencode-config-keys.js +64 -19
- package/dist/adapters/platforms/opencode-config-keys.js.map +1 -1
- package/dist/app/use-cases/init-project.d.ts.map +1 -1
- package/dist/app/use-cases/init-project.js +3 -1
- package/dist/app/use-cases/init-project.js.map +1 -1
- package/dist/app/use-cases/uninstall.js +1 -1
- package/dist/app/use-cases/uninstall.js.map +1 -1
- package/dist/ports/platform-adapter.port.d.ts +11 -2
- package/dist/ports/platform-adapter.port.d.ts.map +1 -1
- package/package.json +1 -1
- package/templates/.wolf/router.log +1 -1
- package/update-map.json +2 -2
|
@@ -2,13 +2,13 @@ import * as fs from 'fs/promises';
|
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
3
|
import { execFile } from 'child_process';
|
|
4
4
|
import { promisify } from 'util';
|
|
5
|
-
import { basename, join } from 'path';
|
|
5
|
+
import { basename, join, relative } from 'path';
|
|
6
6
|
import { parseJsonc } from './jsonc.js';
|
|
7
7
|
import { writeFileAtomic } from '../fs/markdown-memory-store.js';
|
|
8
8
|
import { UserFacingError } from '../../domain/errors.js';
|
|
9
9
|
import { parseStamp } from '../render/stamp.js';
|
|
10
10
|
import yaml from 'js-yaml';
|
|
11
|
-
import { DEFAULT_AGENT, DEFAULT_AGENT_KEY, MCP_KEY, MCP_ROOT, MCP_SERVERS, SUBAGENT_DEPTH, SUBAGENT_DEPTH_KEY, WOLF_MCP_COMMAND, deletePath, getPath, getRecordAt, isWolfMcpEntry, legacyPathOf, blockedSegment, mcpEntryNative, pathWritable,
|
|
11
|
+
import { CONFIG_LOCATIONS, DEFAULT_AGENT, DEFAULT_AGENT_KEY, MCP_KEY, MCP_ROOT, MCP_SERVERS, SUBAGENT_DEPTH, SUBAGENT_DEPTH_KEY, WOLF_MCP_COMMAND, WRITE_LOCATION, WRITE_LOCATION_ID, deletePath, getPath, getRecordAt, isWolfMcpEntry, legacyPathOf, blockedSegment, locatePath, mcpEntryNative, mergeSurface, pathWritable, relPathOf, setPath, } from './opencode-config-keys.js';
|
|
12
12
|
export { DEFAULT_AGENT, SUBAGENT_DEPTH };
|
|
13
13
|
/** Каноническая команда wolf: removeWolf/configIssues не получают cmd, ownership проверяем ею. */
|
|
14
14
|
const WOLF_CMD = WOLF_MCP_COMMAND;
|
|
@@ -61,253 +61,464 @@ export class OpencodeAdapter {
|
|
|
61
61
|
return { status: 'absent', message: err instanceof Error ? err.message : String(err) };
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Файл локации: jsonc — только если json отсутствует (правило прежнее),
|
|
66
|
+
* иначе json. Если файла нет — возвращается дефолтный путь (цель записи).
|
|
67
|
+
*/
|
|
68
|
+
locationFile(projectRoot, location) {
|
|
69
|
+
const dir = location.dir === '' ? projectRoot : join(projectRoot, location.dir);
|
|
70
|
+
const jsonc = join(dir, `${location.fileBase}.jsonc`);
|
|
71
|
+
const json = join(dir, `${location.fileBase}.json`);
|
|
72
|
+
return existsSync(jsonc) && !existsSync(json) ? jsonc : json;
|
|
73
|
+
}
|
|
74
|
+
/** Корневой конфиг — единственное место, куда wolf пишет свои ключи. */
|
|
75
|
+
rootFile(projectRoot) {
|
|
76
|
+
return this.locationFile(projectRoot, WRITE_LOCATION);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Поверхность конфига (6.7): обе локации + эффективный (слейдженный) вид.
|
|
80
|
+
* Битый/нечитаемый файл ИЗОЛИРУЕТСЯ: он попадает в `errors`, а остальные локации
|
|
81
|
+
* читаются как обычно (прежнее поведение «всё или ничего» чинило в обратную сторону).
|
|
82
|
+
*/
|
|
83
|
+
async loadSurface(projectRoot) {
|
|
84
|
+
const files = [];
|
|
85
|
+
const errors = [];
|
|
86
|
+
for (const location of CONFIG_LOCATIONS) {
|
|
87
|
+
const file = this.locationFile(projectRoot, location);
|
|
88
|
+
if (!existsSync(file))
|
|
89
|
+
continue;
|
|
90
|
+
const rel = relative(projectRoot, file);
|
|
91
|
+
let raw;
|
|
92
|
+
try {
|
|
93
|
+
raw = await fs.readFile(file, 'utf-8');
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
errors.push({
|
|
97
|
+
location: location.id,
|
|
98
|
+
file,
|
|
99
|
+
rel,
|
|
100
|
+
kind: 'not readable',
|
|
101
|
+
message: `${file} is not readable: ${err instanceof Error ? err.message : err}`,
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = parseJsonc(raw);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
// паритет с ClaudeAdapter: ошибки парсинга — UserFacingError, не сырой SyntaxError
|
|
111
|
+
errors.push({
|
|
112
|
+
location: location.id,
|
|
113
|
+
file,
|
|
114
|
+
rel,
|
|
115
|
+
kind: 'not valid JSONC',
|
|
116
|
+
message: `${file} is not valid JSONC: ${err instanceof Error ? err.message : err}`,
|
|
117
|
+
});
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const cfg = asRecord(parsed);
|
|
121
|
+
if (cfg === undefined) {
|
|
122
|
+
errors.push({
|
|
123
|
+
location: location.id,
|
|
124
|
+
file,
|
|
125
|
+
rel,
|
|
126
|
+
kind: 'not a JSON object',
|
|
127
|
+
message: `${file} is not a JSON object — refusing to touch it`,
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
files.push({ location: location.id, file, rel, cfg });
|
|
68
132
|
}
|
|
69
|
-
return
|
|
133
|
+
return { files, errors, effective: mergeSurface(files) };
|
|
134
|
+
}
|
|
135
|
+
/** Битая/нечитаемая корневая локация (та, куда wolf пишет) — прежний контракт: ошибка наружу. */
|
|
136
|
+
static rootError(surface) {
|
|
137
|
+
return surface.errors.find((error) => error.location === WRITE_LOCATION_ID);
|
|
70
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Эффективный конфиг обеих локаций; null — конфиг-файлов нет ни в одной локации.
|
|
141
|
+
* Битый корневой файл — UserFacingError (прежний контракт для корня);
|
|
142
|
+
* битый `.opencode` — не мешает: возвращаем вид читаемых файлов.
|
|
143
|
+
*/
|
|
71
144
|
async readConfig(projectRoot) {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
catch {
|
|
145
|
+
const surface = await this.loadSurface(projectRoot);
|
|
146
|
+
const rootError = OpencodeAdapter.rootError(surface);
|
|
147
|
+
if (rootError !== undefined)
|
|
148
|
+
throw new UserFacingError(rootError.message);
|
|
149
|
+
if (surface.files.length === 0)
|
|
78
150
|
return null;
|
|
79
|
-
|
|
80
|
-
let parsed;
|
|
81
|
-
try {
|
|
82
|
-
parsed = parseJsonc(raw);
|
|
83
|
-
}
|
|
84
|
-
catch (err) {
|
|
85
|
-
// паритет с ClaudeAdapter: ошибки парсинга — UserFacingError, не сырой SyntaxError
|
|
86
|
-
throw new UserFacingError(`${file} is not valid JSONC: ${err instanceof Error ? err.message : err}`);
|
|
87
|
-
}
|
|
88
|
-
return asConfig(parsed, file);
|
|
151
|
+
return surface.effective;
|
|
89
152
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Запись wolf-ключей (6.7): решения принимаются по эффективному виду обеих
|
|
155
|
+
* локаций, запись — только в корневой конфиг (в .opencode wolf не пишет).
|
|
156
|
+
*
|
|
157
|
+
* opts.createFile=false — режим repair-only: файл конфига не создаётся, если его нет
|
|
158
|
+
* (минимальный init чинит существующий конфиг и никогда не создаёт платформенный).
|
|
159
|
+
*/
|
|
160
|
+
async writeConfig(projectRoot, cmd, opts) {
|
|
161
|
+
const surface = await this.loadSurface(projectRoot);
|
|
162
|
+
// битый/нечитаемый корень — прежнее поведение: файл не трогаем, ошибка наружу
|
|
163
|
+
const rootError = OpencodeAdapter.rootError(surface);
|
|
164
|
+
if (rootError !== undefined)
|
|
165
|
+
throw new UserFacingError(rootError.message);
|
|
166
|
+
const file = this.rootFile(projectRoot);
|
|
167
|
+
const rootExists = surface.files.some((f) => f.location === WRITE_LOCATION_ID);
|
|
168
|
+
if (!rootExists && opts?.createFile === false) {
|
|
169
|
+
// минимальный init: конфига нет — не создаём (документированное обещание)
|
|
170
|
+
const other = surface.files.find((f) => f.location !== WRITE_LOCATION_ID);
|
|
171
|
+
const reason = other !== undefined
|
|
172
|
+
? `no root ${relPathOf(WRITE_LOCATION)} to repair — the project config is ${other.rel}; a minimal init never creates a platform config (use wolf init --platform ${this.id})`
|
|
173
|
+
: `no root ${relPathOf(WRITE_LOCATION)} to repair — a minimal init never creates a platform config (use wolf init --platform ${this.id})`;
|
|
174
|
+
return { action: 'skipped', reason };
|
|
175
|
+
}
|
|
176
|
+
const rootCfg = rootConfigOf(surface);
|
|
93
177
|
// 6.3: пишем нативную форму mcp.servers.wolf, легаси mcp.wolf снимаем только если он наш
|
|
94
178
|
const desired = mcpEntryNative(cmd);
|
|
95
|
-
const mcpWritable = pathWritable(cfg, MCP_KEY.writePath);
|
|
96
|
-
const nativeEntry = getPath(cfg, MCP_KEY.writePath);
|
|
97
|
-
const mcpOk = mcpWritable && JSON.stringify(nativeEntry) === JSON.stringify(desired);
|
|
98
179
|
const legacyMcpPath = legacyPathOf(MCP_KEY);
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
const
|
|
180
|
+
const nativeAt = locatePath(surface.files, MCP_KEY.writePath);
|
|
181
|
+
const legacyAt = locatePath(surface.files, legacyMcpPath);
|
|
182
|
+
const legacyMcpOurs = legacyAt !== undefined && isWolfMcpEntry(legacyAt.value, cmd);
|
|
183
|
+
const mcpWritable = pathWritable(rootCfg, MCP_KEY.writePath);
|
|
184
|
+
const blockedReasons = [];
|
|
185
|
+
const agentReasons = [];
|
|
186
|
+
const depthReasons = [];
|
|
187
|
+
const mcpBlocked = blockedSegment(rootCfg, MCP_KEY.writePath);
|
|
103
188
|
if (mcpBlocked !== undefined) {
|
|
104
|
-
|
|
189
|
+
blockedReasons.push(`${mcpBlocked} is not an object; ${MCP_KEY.label} is not written — fix the conflicting key manually`);
|
|
105
190
|
}
|
|
106
|
-
// §6.1: default_agent
|
|
107
|
-
const da =
|
|
191
|
+
// §6.1: default_agent — по эффективному виду (корень + .opencode)
|
|
192
|
+
const da = locatePath(surface.files, DEFAULT_AGENT_KEY.writePath)?.value;
|
|
108
193
|
if (da !== undefined && da !== DEFAULT_AGENT) {
|
|
109
|
-
|
|
194
|
+
agentReasons.push(`${DEFAULT_AGENT_KEY.label}=${da} is taken; ${DEFAULT_AGENT} not assigned`);
|
|
110
195
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
196
|
+
// 6.7: объявленное в .opencode не перекрываем вторым ключом в корне
|
|
197
|
+
let writeMcp = false;
|
|
198
|
+
let replaceMcp = false;
|
|
199
|
+
let mcpNativeReason;
|
|
200
|
+
if (nativeAt !== undefined) {
|
|
201
|
+
if (JSON.stringify(nativeAt.value) === JSON.stringify(desired)) {
|
|
202
|
+
// уже канонично — писать нечего
|
|
203
|
+
}
|
|
204
|
+
else if (nativeAt.location === WRITE_LOCATION_ID) {
|
|
205
|
+
if (mcpWritable) {
|
|
206
|
+
writeMcp = true;
|
|
207
|
+
replaceMcp = true;
|
|
208
|
+
mcpNativeReason = `replaced non-canonical ${MCP_KEY.label} entry with the canonical Wolf entry`;
|
|
209
|
+
}
|
|
114
210
|
}
|
|
115
211
|
else {
|
|
116
|
-
|
|
212
|
+
mcpNativeReason = `${MCP_KEY.label} is declared in ${relOf(surface, nativeAt.location)} — the .opencode value overrides the root one, so a root entry would have no effect; wolf leaves it untouched (wolf writes the root config)`;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else if (legacyAt === undefined || !legacyMcpOurs || legacyAt.location === WRITE_LOCATION_ID) {
|
|
216
|
+
writeMcp = mcpWritable;
|
|
217
|
+
}
|
|
218
|
+
// легаси-форма: снимаем только свою и только в корне (писать можно лишь туда)
|
|
219
|
+
let dropLegacyMcp = false;
|
|
220
|
+
let mcpLegacyReason;
|
|
221
|
+
if (legacyAt !== undefined) {
|
|
222
|
+
if (!legacyMcpOurs) {
|
|
223
|
+
mcpLegacyReason = `top-level ${legacyMcpPath.join('.')} is deprecated and foreign (command differs); ${MCP_KEY.label} is used`;
|
|
224
|
+
}
|
|
225
|
+
else if (legacyAt.location === WRITE_LOCATION_ID) {
|
|
226
|
+
dropLegacyMcp = true;
|
|
227
|
+
mcpLegacyReason = `removed deprecated top-level ${legacyMcpPath.join('.')} (migrated to ${MCP_KEY.label})`;
|
|
228
|
+
}
|
|
229
|
+
else if (nativeAt === undefined) {
|
|
230
|
+
mcpLegacyReason = `wolf entry is in the deprecated top-level ${legacyMcpPath.join('.')} form in ${relOf(surface, legacyAt.location)} — the .opencode value overrides the root one, so a root entry would have no effect; migrate it to ${MCP_KEY.label} manually`;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
mcpLegacyReason = `deprecated top-level ${legacyMcpPath.join('.')} in ${relOf(surface, legacyAt.location)} is ignored by opencode v2 — the .opencode value overrides the root one; remove it manually`;
|
|
117
234
|
}
|
|
118
235
|
}
|
|
119
|
-
const migrateLegacyMcp = legacyMcpOurs;
|
|
120
|
-
const replaced = nativeEntry !== undefined && !mcpOk;
|
|
121
|
-
if (replaced)
|
|
122
|
-
reasons.push(`replaced non-canonical ${MCP_KEY.label} entry with the canonical Wolf entry`);
|
|
123
236
|
// opencode v2: действует только experimental.subagent_depth. Верхнеуровневый ключ —
|
|
124
237
|
// deprecated: наше точное значение мигрируем, чужое не трогаем и сообщаем.
|
|
125
238
|
const depthPath = SUBAGENT_DEPTH_KEY.writePath;
|
|
126
239
|
const depthLeaf = leaf(SUBAGENT_DEPTH_KEY);
|
|
127
240
|
const depthLabel = dotted(SUBAGENT_DEPTH_KEY);
|
|
128
241
|
const legacyDepthPath = legacyPathOf(SUBAGENT_DEPTH_KEY);
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
const
|
|
242
|
+
const depthAt = locatePath(surface.files, depthPath);
|
|
243
|
+
const legacyDepthAt = locatePath(surface.files, legacyDepthPath);
|
|
244
|
+
const depth = depthAt?.value;
|
|
245
|
+
const legacyDepth = legacyDepthAt?.value;
|
|
246
|
+
const depthOk = depthIsOurs(depth);
|
|
247
|
+
const legacyOurs = legacyDepth === SUBAGENT_DEPTH;
|
|
134
248
|
let setDepth = false;
|
|
135
249
|
let dropLegacy = false;
|
|
136
|
-
const depthWritable = pathWritable(
|
|
250
|
+
const depthWritable = pathWritable(rootCfg, depthPath);
|
|
137
251
|
if (!depthWritable) {
|
|
138
|
-
|
|
252
|
+
depthReasons.push(`experimental is not an object; ${depthLeaf} is not written — set ${depthLabel}: ${SUBAGENT_DEPTH} manually`);
|
|
139
253
|
}
|
|
140
254
|
else if (depthOk) {
|
|
141
255
|
if (legacyOurs) {
|
|
142
|
-
|
|
143
|
-
|
|
256
|
+
if (legacyDepthAt.location === WRITE_LOCATION_ID) {
|
|
257
|
+
dropLegacy = true; // оба ключа наши: легаси — только шум, убираем
|
|
258
|
+
depthReasons.push(`removed deprecated top-level ${depthLeaf}=${SUBAGENT_DEPTH} (ignored by opencode v2)`);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
depthReasons.push(`top-level ${depthLeaf}=${SUBAGENT_DEPTH} in ${relOf(surface, legacyDepthAt.location)} is deprecated and ignored by opencode v2 — the .opencode value overrides the root one; remove it manually`);
|
|
262
|
+
}
|
|
144
263
|
}
|
|
145
|
-
else if (
|
|
146
|
-
|
|
264
|
+
else if (legacyDepth !== undefined) {
|
|
265
|
+
depthReasons.push(`top-level ${depthLeaf}=${legacyDepth} is deprecated and ignored by opencode v2; ${depthLabel} is used`);
|
|
147
266
|
}
|
|
148
267
|
}
|
|
149
|
-
else if (
|
|
150
|
-
|
|
268
|
+
else if (depth !== undefined) {
|
|
269
|
+
depthReasons.push(`${depthLabel}=${depth} is taken; the three-level scheme will not work, set >=2`);
|
|
151
270
|
}
|
|
152
271
|
else if (legacyOurs) {
|
|
153
272
|
setDepth = true;
|
|
154
|
-
|
|
155
|
-
|
|
273
|
+
if (legacyDepthAt.location === WRITE_LOCATION_ID) {
|
|
274
|
+
dropLegacy = true;
|
|
275
|
+
depthReasons.push(`migrated ${depthLeaf} to ${depthLabel}`);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
// легаси в .opencode удалить не можем (wolf пишет только в корень) — но и второй
|
|
279
|
+
// ключ не нужен: каноническую форму wolf записывает в корневой конфиг сам.
|
|
280
|
+
depthReasons.push(`top-level ${depthLeaf}=${SUBAGENT_DEPTH} in ${relOf(surface, legacyDepthAt.location)} is deprecated and ignored by opencode v2 — wolf wrote ${depthLabel}=${SUBAGENT_DEPTH} in the root config; remove the deprecated key manually`);
|
|
281
|
+
}
|
|
156
282
|
}
|
|
157
|
-
else if (
|
|
158
|
-
|
|
283
|
+
else if (legacyDepth !== undefined) {
|
|
284
|
+
depthReasons.push(`top-level ${depthLeaf}=${legacyDepth} is taken; the key is deprecated and ignored by opencode v2 — set ${depthLabel} >=${SUBAGENT_DEPTH}`);
|
|
159
285
|
}
|
|
160
286
|
else {
|
|
161
287
|
setDepth = true;
|
|
162
288
|
}
|
|
289
|
+
const reasons = [
|
|
290
|
+
...blockedReasons,
|
|
291
|
+
...agentReasons,
|
|
292
|
+
...(mcpLegacyReason !== undefined ? [mcpLegacyReason] : []),
|
|
293
|
+
...(mcpNativeReason !== undefined ? [mcpNativeReason] : []),
|
|
294
|
+
...depthReasons,
|
|
295
|
+
// битая/нечитаемая локация не мешает починке корня — но называем её
|
|
296
|
+
...surface.errors.map((error) => `skipped: ${error.rel} is ${error.kind}`),
|
|
297
|
+
];
|
|
163
298
|
const reason = reasons.length > 0 ? reasons.join('; ') : undefined;
|
|
164
|
-
// F6 (спека 2.1.0 §2.4): фактические wolf-ключи после ensured —
|
|
299
|
+
// F6 (спека 2.1.0 §2.4): фактические wolf-ключи после ensured — по эффективному виду
|
|
165
300
|
// (mcp.servers.wolf каноничен; default_agent/subagent_depth — если ключ наш, не занят чужим значением)
|
|
166
301
|
const ours = () => {
|
|
167
|
-
const
|
|
168
|
-
|
|
302
|
+
const effective = effectiveAfter(surface, rootCfg);
|
|
303
|
+
const keys = [];
|
|
304
|
+
if (isWolfMcpEntry(getPath(effective, MCP_KEY.writePath), cmd))
|
|
305
|
+
keys.push(MCP_KEY.label);
|
|
306
|
+
if (getPath(effective, DEFAULT_AGENT_KEY.writePath) === DEFAULT_AGENT)
|
|
169
307
|
keys.push(`${DEFAULT_AGENT_KEY.label}=${DEFAULT_AGENT}`);
|
|
170
|
-
const sd =
|
|
308
|
+
const sd = getPath(effective, depthPath);
|
|
171
309
|
if (depthIsOurs(sd))
|
|
172
310
|
keys.push(`${depthLeaf}=${sd}`);
|
|
173
311
|
return keys;
|
|
174
312
|
};
|
|
313
|
+
// F-10: отчёт init называет файл, где реально лежат wolf-ключи (при отсутствии
|
|
314
|
+
// записи — файл, где объявлен эффективный wolf-ключ; иначе — файл записи).
|
|
315
|
+
const wolfKeyFile = () => {
|
|
316
|
+
const mcp = locatePath(surface.files, MCP_KEY.writePath);
|
|
317
|
+
if (mcp !== undefined)
|
|
318
|
+
return relOf(surface, mcp.location);
|
|
319
|
+
const agent = locatePath(surface.files, DEFAULT_AGENT_KEY.writePath);
|
|
320
|
+
if (agent !== undefined)
|
|
321
|
+
return relOf(surface, agent.location);
|
|
322
|
+
const depth = locatePath(surface.files, SUBAGENT_DEPTH_KEY.writePath);
|
|
323
|
+
if (depth !== undefined)
|
|
324
|
+
return relOf(surface, depth.location);
|
|
325
|
+
return undefined;
|
|
326
|
+
};
|
|
175
327
|
// unchanged — по ВСЕМ ключам: корректный mcp.servers.wolf не маскирует отсутствие default_agent/subagent_depth;
|
|
176
328
|
// занятое чужим значением место depth тоже settled (не перезаписываем — только reason);
|
|
177
329
|
// наш легаси mcp.wolf — не settled: его надо снять записью.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
330
|
+
const setAgent = da === undefined;
|
|
331
|
+
if (!writeMcp && !dropLegacyMcp && !setAgent && !setDepth && !dropLegacy)
|
|
332
|
+
return { action: 'unchanged', reason, configFile: wolfKeyFile() ?? basename(file), keys: ours() };
|
|
333
|
+
if (writeMcp)
|
|
334
|
+
setPath(rootCfg, MCP_KEY.writePath, desired);
|
|
335
|
+
if (dropLegacyMcp)
|
|
336
|
+
deletePath(rootCfg, legacyMcpPath);
|
|
337
|
+
if (setAgent)
|
|
338
|
+
setPath(rootCfg, DEFAULT_AGENT_KEY.writePath, DEFAULT_AGENT); // конфликтный ключ не трогаем
|
|
186
339
|
if (setDepth)
|
|
187
|
-
setPath(
|
|
340
|
+
setPath(rootCfg, depthPath, SUBAGENT_DEPTH);
|
|
188
341
|
if (dropLegacy)
|
|
189
|
-
deletePath(
|
|
190
|
-
await writeFileAtomic(file, JSON.stringify(
|
|
191
|
-
return { action:
|
|
342
|
+
deletePath(rootCfg, legacyDepthPath);
|
|
343
|
+
await writeFileAtomic(file, JSON.stringify(rootCfg, null, 2) + '\n');
|
|
344
|
+
return { action: replaceMcp ? 'replaced' : 'written', reason, configFile: basename(file), keys: ours() };
|
|
192
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* 6.7: wolf-запись MCP (нативная и легаси, только если она наша) удаляется
|
|
348
|
+
* из ВСЕХ локаций, где она реально объявлена.
|
|
349
|
+
*/
|
|
193
350
|
async removeWolf(projectRoot) {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
if (cfg === null)
|
|
351
|
+
const surface = await this.loadSurface(projectRoot);
|
|
352
|
+
if (surface.files.length === 0)
|
|
197
353
|
return false;
|
|
198
354
|
const legacyMcpPath = legacyPathOf(MCP_KEY);
|
|
199
|
-
const nativeEntry = getPath(cfg, MCP_KEY.writePath);
|
|
200
|
-
const legacyEntry = getPath(cfg, legacyMcpPath);
|
|
201
355
|
let removed = false;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
356
|
+
for (const file of surface.files) {
|
|
357
|
+
let changed = false;
|
|
358
|
+
const nativeEntry = getPath(file.cfg, MCP_KEY.writePath);
|
|
359
|
+
const legacyEntry = getPath(file.cfg, legacyMcpPath);
|
|
360
|
+
if (nativeEntry !== undefined && isWolfMcpEntry(nativeEntry, WOLF_CMD)) {
|
|
361
|
+
deletePath(file.cfg, MCP_KEY.writePath);
|
|
362
|
+
changed = true;
|
|
363
|
+
}
|
|
364
|
+
if (legacyEntry !== undefined && isWolfMcpEntry(legacyEntry, WOLF_CMD)) {
|
|
365
|
+
deletePath(file.cfg, legacyMcpPath);
|
|
366
|
+
changed = true;
|
|
367
|
+
}
|
|
368
|
+
if (!changed)
|
|
369
|
+
continue;
|
|
208
370
|
removed = true;
|
|
371
|
+
await writeFileAtomic(file.file, JSON.stringify(file.cfg, null, 2) + '\n');
|
|
209
372
|
}
|
|
210
|
-
|
|
211
|
-
return false;
|
|
212
|
-
await writeFileAtomic(file, JSON.stringify(cfg, null, 2) + '\n');
|
|
213
|
-
return true;
|
|
373
|
+
return removed;
|
|
214
374
|
}
|
|
215
375
|
/**
|
|
216
376
|
* 2.16.2 (uninstall, F-07): только wolf-ключи с точным нашим значением.
|
|
217
377
|
* Пользовательские default_agent/subagent_depth (не наши точные значения) не трогаем.
|
|
218
378
|
* Глубина: `experimental.subagent_depth === 2` (действующий путь) + легаси верхнеуровневый.
|
|
219
379
|
* 6.3: удаляем обе формы MCP (нативную и легаси) — только если запись наша.
|
|
380
|
+
* 6.7: снимаем ключи из ВСЕХ локаций, где они объявлены; битый файл — в причину.
|
|
220
381
|
*/
|
|
221
382
|
async removeWolfKeys(projectRoot) {
|
|
222
|
-
const file = this.
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
removed.push(MCP_KEY.label);
|
|
233
|
-
}
|
|
234
|
-
if (legacyEntry !== undefined && isWolfMcpEntry(legacyEntry, WOLF_CMD)) {
|
|
235
|
-
deletePath(cfg, legacyMcpPath);
|
|
236
|
-
if (!removed.includes(MCP_KEY.label))
|
|
237
|
-
removed.push(MCP_KEY.label);
|
|
238
|
-
}
|
|
239
|
-
if (getPath(cfg, DEFAULT_AGENT_KEY.writePath) === DEFAULT_AGENT) {
|
|
240
|
-
deletePath(cfg, DEFAULT_AGENT_KEY.writePath);
|
|
241
|
-
removed.push(`${DEFAULT_AGENT_KEY.label}=${DEFAULT_AGENT}`);
|
|
383
|
+
const file = this.rootFile(projectRoot);
|
|
384
|
+
const surface = await this.loadSurface(projectRoot);
|
|
385
|
+
const broken = surface.errors.map((error) => `skipped: ${error.rel} is ${error.kind}`);
|
|
386
|
+
if (surface.files.length === 0) {
|
|
387
|
+
// «no opencode.json» — только когда конфиг-файлов нет НИ В ОДНОЙ локации
|
|
388
|
+
return {
|
|
389
|
+
action: 'skipped',
|
|
390
|
+
reason: broken.length > 0 ? broken.join('; ') : `no ${basename(file)}`,
|
|
391
|
+
configFile: basename(file),
|
|
392
|
+
};
|
|
242
393
|
}
|
|
394
|
+
let mcpRemoved = false;
|
|
395
|
+
let agentRemoved = false;
|
|
396
|
+
let depthRemoved = false;
|
|
397
|
+
const changed = [];
|
|
398
|
+
const legacyMcpPath = legacyPathOf(MCP_KEY);
|
|
243
399
|
const depthLeaf = leaf(SUBAGENT_DEPTH_KEY);
|
|
244
400
|
const legacyDepthPath = legacyPathOf(SUBAGENT_DEPTH_KEY);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
401
|
+
// корень первым — так же, как в configFile списке результата
|
|
402
|
+
for (const file of [...surface.files].reverse()) {
|
|
403
|
+
let touched = false;
|
|
404
|
+
const nativeEntry = getPath(file.cfg, MCP_KEY.writePath);
|
|
405
|
+
const legacyEntry = getPath(file.cfg, legacyMcpPath);
|
|
406
|
+
if (nativeEntry !== undefined && isWolfMcpEntry(nativeEntry, WOLF_CMD)) {
|
|
407
|
+
deletePath(file.cfg, MCP_KEY.writePath);
|
|
408
|
+
mcpRemoved = true;
|
|
409
|
+
touched = true;
|
|
410
|
+
}
|
|
411
|
+
if (legacyEntry !== undefined && isWolfMcpEntry(legacyEntry, WOLF_CMD)) {
|
|
412
|
+
deletePath(file.cfg, legacyMcpPath);
|
|
413
|
+
mcpRemoved = true;
|
|
414
|
+
touched = true;
|
|
415
|
+
}
|
|
416
|
+
if (getPath(file.cfg, DEFAULT_AGENT_KEY.writePath) === DEFAULT_AGENT) {
|
|
417
|
+
deletePath(file.cfg, DEFAULT_AGENT_KEY.writePath);
|
|
418
|
+
agentRemoved = true;
|
|
419
|
+
touched = true;
|
|
420
|
+
}
|
|
421
|
+
if (getPath(file.cfg, SUBAGENT_DEPTH_KEY.writePath) === SUBAGENT_DEPTH) {
|
|
422
|
+
deletePath(file.cfg, SUBAGENT_DEPTH_KEY.writePath);
|
|
423
|
+
depthRemoved = true;
|
|
424
|
+
touched = true;
|
|
425
|
+
}
|
|
426
|
+
if (getPath(file.cfg, legacyDepthPath) === SUBAGENT_DEPTH) {
|
|
427
|
+
// легаси верхнеуровневый ключ (deprecated в opencode v2) — тоже наш
|
|
428
|
+
deletePath(file.cfg, legacyDepthPath);
|
|
429
|
+
depthRemoved = true;
|
|
430
|
+
touched = true;
|
|
431
|
+
}
|
|
432
|
+
if (!touched)
|
|
433
|
+
continue; // перезаписываем только файлы, из которых сняты наши ключи
|
|
434
|
+
changed.push(file.rel);
|
|
435
|
+
await writeFileAtomic(file.file, JSON.stringify(file.cfg, null, 2) + '\n');
|
|
254
436
|
}
|
|
255
|
-
|
|
256
|
-
|
|
437
|
+
const removed = [
|
|
438
|
+
...(mcpRemoved ? [MCP_KEY.label] : []),
|
|
439
|
+
...(agentRemoved ? [`${DEFAULT_AGENT_KEY.label}=${DEFAULT_AGENT}`] : []),
|
|
440
|
+
...(depthRemoved ? [`${depthLeaf}=${SUBAGENT_DEPTH}`] : []),
|
|
441
|
+
];
|
|
257
442
|
if (removed.length === 0) {
|
|
258
443
|
return {
|
|
259
444
|
action: 'unchanged',
|
|
260
|
-
reason: 'no wolf keys with our exact values',
|
|
445
|
+
reason: ['no wolf keys with our exact values', ...broken].join('; '),
|
|
261
446
|
configFile: basename(file),
|
|
262
447
|
keys: [],
|
|
263
448
|
};
|
|
264
449
|
}
|
|
265
|
-
|
|
266
|
-
|
|
450
|
+
return {
|
|
451
|
+
action: 'removed',
|
|
452
|
+
...(broken.length > 0 ? { reason: broken.join('; ') } : {}),
|
|
453
|
+
configFile: changed.join(', '),
|
|
454
|
+
keys: removed,
|
|
455
|
+
};
|
|
267
456
|
}
|
|
268
457
|
/**
|
|
269
458
|
* 6.1/6.3/6.4: doctor-проверки, которые знает только opencode-адаптер —
|
|
270
459
|
* все ключи, которые Wolf пишет, + смешанный frontmatter managed-агентов.
|
|
271
460
|
* Строки идут в общий `issues` проекта и маскируются doctor'ом как обычно.
|
|
461
|
+
* 6.7: проверки — по эффективному виду обеих локаций; битый файл — свой issue с именем.
|
|
272
462
|
*/
|
|
273
463
|
async configIssues(projectRoot) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
282
|
-
const kind = message.includes('not valid JSONC') ? 'not valid JSONC' : 'not a JSON object';
|
|
283
|
-
return [`${this.id}: config is ${kind} — fix it or remove the file, then run wolf init`];
|
|
464
|
+
const surface = await this.loadSurface(projectRoot);
|
|
465
|
+
const issues = surface.errors.map((error) => `${this.id}: ${error.rel} is ${error.kind} — fix it or remove the file, then run wolf init`);
|
|
466
|
+
if (surface.files.length === 0) {
|
|
467
|
+
// «missing» советовал бы init, который честно скипает битый конфиг
|
|
468
|
+
if (issues.length === 0)
|
|
469
|
+
return [`${this.id}: wolf entry missing — run wolf init`];
|
|
470
|
+
return issues;
|
|
284
471
|
}
|
|
285
|
-
if (cfg === null)
|
|
286
|
-
return [`${this.id}: wolf entry missing — run wolf init`];
|
|
287
472
|
return [
|
|
288
|
-
...
|
|
289
|
-
...
|
|
290
|
-
...
|
|
473
|
+
...issues,
|
|
474
|
+
...mcpIssues(surface),
|
|
475
|
+
...defaultAgentIssues(surface.effective),
|
|
476
|
+
...depthIssues(surface),
|
|
291
477
|
...(await mixedFrontmatterIssues(projectRoot, this.id)),
|
|
292
478
|
];
|
|
293
479
|
}
|
|
294
480
|
}
|
|
295
|
-
/**
|
|
296
|
-
function
|
|
481
|
+
/** Собственный конфиг корневой локации (не слейдженный): в него wolf пишет. */
|
|
482
|
+
function rootConfigOf(surface) {
|
|
483
|
+
return surface.files.find((f) => f.location === WRITE_LOCATION_ID)?.cfg ?? {};
|
|
484
|
+
}
|
|
485
|
+
/** Путь файла локации относительно корня проекта (для сообщений). */
|
|
486
|
+
function relOf(surface, location) {
|
|
487
|
+
return surface.files.find((f) => f.location === location)?.rel ?? '';
|
|
488
|
+
}
|
|
489
|
+
/** Эффективный вид после записи: обновлённый корень + .opencode (порядок — в mergeSurface). */
|
|
490
|
+
function effectiveAfter(surface, rootCfg) {
|
|
491
|
+
return mergeSurface(surface.files, { location: WRITE_LOCATION_ID, cfg: rootCfg });
|
|
492
|
+
}
|
|
493
|
+
/** 6.3/6.7: wolf-сервер отсутствует в обеих формах / легаси-форма подлежит миграции. */
|
|
494
|
+
function mcpIssues(surface) {
|
|
495
|
+
const cfg = surface.effective;
|
|
297
496
|
// запись невозможна: init не вылечит такой конфиг — не советуем его
|
|
298
497
|
const blocked = blockedSegment(cfg, MCP_KEY.writePath);
|
|
299
498
|
if (blocked !== undefined)
|
|
300
499
|
return [`opencode: ${blocked} is not an object — ${MCP_KEY.label} is not written; fix it manually`];
|
|
301
|
-
const
|
|
302
|
-
|
|
500
|
+
const legacyPath = legacyPathOf(MCP_KEY);
|
|
501
|
+
const nativeAt = locatePath(surface.files, MCP_KEY.writePath);
|
|
502
|
+
const legacyAt = locatePath(surface.files, legacyPath);
|
|
503
|
+
// сервер не объявлен НИ В ОДНОЙ локации — только тогда «wolf entry missing»
|
|
504
|
+
if (nativeAt === undefined && legacyAt === undefined)
|
|
303
505
|
return [`opencode: wolf entry missing — run wolf init`];
|
|
304
|
-
if (
|
|
506
|
+
if (legacyAt === undefined)
|
|
305
507
|
return [];
|
|
306
|
-
const
|
|
508
|
+
const ours = isWolfMcpEntry(legacyAt.value, WOLF_CMD);
|
|
509
|
+
// легаси в .opencode: init туда не пишет — советуем ручную миграцию с именем файла
|
|
510
|
+
if (legacyAt.location === WRITE_LOCATION_ID) {
|
|
511
|
+
return [
|
|
512
|
+
ours
|
|
513
|
+
? `opencode: wolf entry is in the deprecated top-level ${legacyPath.join('.')} form — run wolf init to migrate it to ${MCP_KEY.label}`
|
|
514
|
+
: `opencode: top-level ${legacyPath.join('.')} is foreign and deprecated (opencode v2 reads ${MCP_ROOT}.${MCP_SERVERS}) — wolf leaves it untouched`,
|
|
515
|
+
];
|
|
516
|
+
}
|
|
517
|
+
const rel = relOf(surface, legacyAt.location);
|
|
307
518
|
return [
|
|
308
|
-
|
|
309
|
-
? `opencode: wolf entry is in the deprecated top-level ${legacyPath.join('.')} form
|
|
310
|
-
: `opencode: top-level ${legacyPath.join('.')} is foreign and deprecated (opencode v2 reads ${MCP_ROOT}.${MCP_SERVERS}) — wolf leaves it untouched`,
|
|
519
|
+
ours
|
|
520
|
+
? `opencode: wolf entry is in the deprecated top-level ${legacyPath.join('.')} form in ${rel} — migrate it to ${MCP_KEY.label} manually`
|
|
521
|
+
: `opencode: top-level ${legacyPath.join('.')} in ${rel} is foreign and deprecated (opencode v2 reads ${MCP_ROOT}.${MCP_SERVERS}) — wolf leaves it untouched`,
|
|
311
522
|
];
|
|
312
523
|
}
|
|
313
524
|
/** 6.1: default_agent === mr-wolf, иначе L0 стартует не тем агентом. */
|
|
@@ -323,8 +534,9 @@ function defaultAgentIssues(cfg) {
|
|
|
323
534
|
];
|
|
324
535
|
return [];
|
|
325
536
|
}
|
|
326
|
-
/** 6.1: experimental.subagent_depth >= 2 + deprecated верхнеуровневый
|
|
327
|
-
function depthIssues(
|
|
537
|
+
/** 6.1/6.7: experimental.subagent_depth >= 2 + deprecated верхнеуровневый ключ (по эффективному виду). */
|
|
538
|
+
function depthIssues(surface) {
|
|
539
|
+
const cfg = surface.effective;
|
|
328
540
|
const out = [];
|
|
329
541
|
const depthPath = SUBAGENT_DEPTH_KEY.writePath;
|
|
330
542
|
const parentPath = depthPath.slice(0, -1);
|
|
@@ -343,11 +555,21 @@ function depthIssues(cfg) {
|
|
|
343
555
|
out.push(`opencode: ${depthLabel}=${String(depth)} — the three-level scheme needs >=${SUBAGENT_DEPTH}; foreign value is never overwritten`);
|
|
344
556
|
}
|
|
345
557
|
const legacyPath = legacyPathOf(SUBAGENT_DEPTH_KEY);
|
|
346
|
-
const
|
|
347
|
-
if (
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
558
|
+
const legacyAt = locatePath(surface.files, legacyPath);
|
|
559
|
+
if (legacyAt !== undefined) {
|
|
560
|
+
const legacy = legacyAt.value;
|
|
561
|
+
// легаси в .opencode: init туда не пишет — миграция ручная, с именем файла
|
|
562
|
+
if (legacyAt.location === WRITE_LOCATION_ID) {
|
|
563
|
+
out.push(legacy === SUBAGENT_DEPTH
|
|
564
|
+
? `opencode: top-level ${legacyPath.join('.')}=${SUBAGENT_DEPTH} is deprecated and ignored by opencode v2 — run wolf init to migrate it to ${depthLabel}`
|
|
565
|
+
: `opencode: top-level ${legacyPath.join('.')}=${String(legacy)} is deprecated and ignored by opencode v2 — foreign value, set ${depthLabel} >=${SUBAGENT_DEPTH} manually`);
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
const rel = relOf(surface, legacyAt.location);
|
|
569
|
+
out.push(legacy === SUBAGENT_DEPTH
|
|
570
|
+
? `opencode: top-level ${legacyPath.join('.')}=${SUBAGENT_DEPTH} in ${rel} is deprecated and ignored by opencode v2 — migrate it to ${depthLabel} manually`
|
|
571
|
+
: `opencode: top-level ${legacyPath.join('.')}=${String(legacy)} in ${rel} is deprecated and ignored by opencode v2 — foreign value, set ${depthLabel} >=${SUBAGENT_DEPTH} manually`);
|
|
572
|
+
}
|
|
351
573
|
}
|
|
352
574
|
return out;
|
|
353
575
|
}
|
|
@@ -395,13 +617,6 @@ function frontmatterOf(raw) {
|
|
|
395
617
|
return undefined;
|
|
396
618
|
}
|
|
397
619
|
}
|
|
398
|
-
function asConfig(value, file) {
|
|
399
|
-
const cfg = asRecord(value);
|
|
400
|
-
if (cfg === undefined) {
|
|
401
|
-
throw new UserFacingError(`${file} is not a JSON object — refusing to touch it`);
|
|
402
|
-
}
|
|
403
|
-
return cfg;
|
|
404
|
-
}
|
|
405
620
|
function asRecord(value) {
|
|
406
621
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
407
622
|
? value
|