opencode-manifold 0.4.11 → 0.4.13
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/index.js +714 -950
- package/package.json +1 -1
- package/src/templates/manifold/settings.json +2 -0
package/dist/index.js
CHANGED
|
@@ -1,449 +1,114 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
3
|
|
|
4
|
-
// src/
|
|
5
|
-
import { readFile, writeFile, mkdir, readdir } from "fs/promises";
|
|
4
|
+
// src/tui.ts
|
|
6
5
|
import { existsSync } from "fs";
|
|
7
|
-
import { join
|
|
8
|
-
import { fileURLToPath } from "url";
|
|
6
|
+
import { join } from "path";
|
|
9
7
|
import { homedir } from "os";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
try {
|
|
17
|
-
const packageJson = require2(join(__dirname2, "..", "package.json"));
|
|
18
|
-
return packageJson.version || "unknown";
|
|
19
|
-
} catch {
|
|
20
|
-
return "unknown";
|
|
21
|
-
}
|
|
8
|
+
import { readFile, writeFile, readdir } from "fs/promises";
|
|
9
|
+
|
|
10
|
+
// node_modules/js-yaml/dist/js-yaml.mjs
|
|
11
|
+
/*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT */
|
|
12
|
+
function isNothing(subject) {
|
|
13
|
+
return typeof subject === "undefined" || subject === null;
|
|
22
14
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return false;
|
|
26
|
-
try {
|
|
27
|
-
const entries = await readdir(dirPath);
|
|
28
|
-
return entries.length > 0;
|
|
29
|
-
} catch {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
15
|
+
function isObject(subject) {
|
|
16
|
+
return typeof subject === "object" && subject !== null;
|
|
32
17
|
}
|
|
33
|
-
|
|
34
|
-
if (
|
|
18
|
+
function toArray(sequence) {
|
|
19
|
+
if (Array.isArray(sequence))
|
|
20
|
+
return sequence;
|
|
21
|
+
else if (isNothing(sequence))
|
|
35
22
|
return [];
|
|
36
|
-
|
|
37
|
-
const copied = [];
|
|
38
|
-
const entries = await readdir(src, { withFileTypes: true });
|
|
39
|
-
for (const entry of entries) {
|
|
40
|
-
const srcPath = join(src, entry.name);
|
|
41
|
-
const destPath = join(dest, entry.name);
|
|
42
|
-
if (entry.isDirectory()) {
|
|
43
|
-
const subCopied = await copyMissingFiles(srcPath, destPath);
|
|
44
|
-
if (subCopied.length > 0) {
|
|
45
|
-
copied.push(entry.name);
|
|
46
|
-
}
|
|
47
|
-
} else if (!existsSync(destPath)) {
|
|
48
|
-
await writeFile(destPath, await readFile(srcPath));
|
|
49
|
-
copied.push(entry.name);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return copied;
|
|
23
|
+
return [sequence];
|
|
53
24
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
25
|
+
function extend(target, source) {
|
|
26
|
+
var index, length, key, sourceKeys;
|
|
27
|
+
if (source) {
|
|
28
|
+
sourceKeys = Object.keys(source);
|
|
29
|
+
for (index = 0, length = sourceKeys.length;index < length; index += 1) {
|
|
30
|
+
key = sourceKeys[index];
|
|
31
|
+
target[key] = source[key];
|
|
60
32
|
}
|
|
61
|
-
});
|
|
62
|
-
if (!existsSync(bundledTemplatesDir)) {
|
|
63
|
-
await ctx.client.app.log({
|
|
64
|
-
body: {
|
|
65
|
-
service: "opencode-manifold",
|
|
66
|
-
level: "error",
|
|
67
|
-
message: `Bundled templates not found at ${bundledTemplatesDir}`
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
return;
|
|
71
33
|
}
|
|
72
|
-
|
|
73
|
-
const globalCommandsDir = join(homedir(), ".config", "opencode", "commands");
|
|
74
|
-
const bundledCommandsDir = join(bundledTemplatesDir, "commands");
|
|
75
|
-
if (existsSync(bundledCommandsDir)) {
|
|
76
|
-
await copyMissingFiles(bundledCommandsDir, globalCommandsDir);
|
|
77
|
-
}
|
|
78
|
-
await ctx.client.app.log({
|
|
79
|
-
body: {
|
|
80
|
-
service: "opencode-manifold",
|
|
81
|
-
level: "info",
|
|
82
|
-
message: "Global templates ready"
|
|
83
|
-
}
|
|
84
|
-
});
|
|
34
|
+
return target;
|
|
85
35
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
service: "opencode-manifold",
|
|
91
|
-
level: "info",
|
|
92
|
-
message: `Running /manifold-init in ${directory}`
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
if (!await dirHasContent(globalTemplatesDir)) {
|
|
96
|
-
await client.app.log({
|
|
97
|
-
body: {
|
|
98
|
-
service: "opencode-manifold",
|
|
99
|
-
level: "error",
|
|
100
|
-
message: `Global templates not found at ${globalTemplatesDir}. Plugin may not have loaded correctly.`
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
return initialized;
|
|
104
|
-
}
|
|
105
|
-
const agentsCopied = await copyMissingFiles(join(globalTemplatesDir, "agents"), join(directory, ".opencode", "agents"));
|
|
106
|
-
if (agentsCopied.length > 0) {
|
|
107
|
-
initialized.push(`agents (${agentsCopied.join(", ")})`);
|
|
36
|
+
function repeat(string, count) {
|
|
37
|
+
var result = "", cycle;
|
|
38
|
+
for (cycle = 0;cycle < count; cycle += 1) {
|
|
39
|
+
result += string;
|
|
108
40
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
function isNegativeZero(number) {
|
|
44
|
+
return number === 0 && Number.NEGATIVE_INFINITY === 1 / number;
|
|
45
|
+
}
|
|
46
|
+
var isNothing_1 = isNothing;
|
|
47
|
+
var isObject_1 = isObject;
|
|
48
|
+
var toArray_1 = toArray;
|
|
49
|
+
var repeat_1 = repeat;
|
|
50
|
+
var isNegativeZero_1 = isNegativeZero;
|
|
51
|
+
var extend_1 = extend;
|
|
52
|
+
var common = {
|
|
53
|
+
isNothing: isNothing_1,
|
|
54
|
+
isObject: isObject_1,
|
|
55
|
+
toArray: toArray_1,
|
|
56
|
+
repeat: repeat_1,
|
|
57
|
+
isNegativeZero: isNegativeZero_1,
|
|
58
|
+
extend: extend_1
|
|
59
|
+
};
|
|
60
|
+
function formatError(exception, compact) {
|
|
61
|
+
var where = "", message = exception.reason || "(unknown reason)";
|
|
62
|
+
if (!exception.mark)
|
|
63
|
+
return message;
|
|
64
|
+
if (exception.mark.name) {
|
|
65
|
+
where += 'in "' + exception.mark.name + '" ';
|
|
112
66
|
}
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
|
|
67
|
+
where += "(" + (exception.mark.line + 1) + ":" + (exception.mark.column + 1) + ")";
|
|
68
|
+
if (!compact && exception.mark.snippet) {
|
|
69
|
+
where += `
|
|
70
|
+
|
|
71
|
+
` + exception.mark.snippet;
|
|
116
72
|
}
|
|
117
|
-
|
|
118
|
-
await writeFile(join(directory, "Manifold", "VERSION"), version + `
|
|
119
|
-
`);
|
|
120
|
-
await client.app.log({
|
|
121
|
-
body: {
|
|
122
|
-
service: "opencode-manifold",
|
|
123
|
-
level: "info",
|
|
124
|
-
message: `/manifold-init complete: ${initialized.join(", ") || "already initialized"}`
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
return initialized;
|
|
73
|
+
return message + " " + where;
|
|
128
74
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
import { join as join4 } from "path";
|
|
140
|
-
|
|
141
|
-
// src/session-spawner.ts
|
|
142
|
-
async function waitForSessionIdle(client, sessionId, timeoutMs) {
|
|
143
|
-
const startTime = Date.now();
|
|
144
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
145
|
-
const statusResponse = await client.session.status({});
|
|
146
|
-
const statusData = statusResponse.data;
|
|
147
|
-
if (statusData && statusData[sessionId]) {
|
|
148
|
-
const status = statusData[sessionId];
|
|
149
|
-
if (status.type === "idle") {
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
if (status.type === "retry") {
|
|
153
|
-
const waitTime = status.next - Date.now();
|
|
154
|
-
if (waitTime > 0) {
|
|
155
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(waitTime, 1000)));
|
|
156
|
-
}
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
75
|
+
function YAMLException$1(reason, mark) {
|
|
76
|
+
Error.call(this);
|
|
77
|
+
this.name = "YAMLException";
|
|
78
|
+
this.reason = reason;
|
|
79
|
+
this.mark = mark;
|
|
80
|
+
this.message = formatError(this, false);
|
|
81
|
+
if (Error.captureStackTrace) {
|
|
82
|
+
Error.captureStackTrace(this, this.constructor);
|
|
83
|
+
} else {
|
|
84
|
+
this.stack = new Error().stack || "";
|
|
161
85
|
}
|
|
162
|
-
return false;
|
|
163
86
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
87
|
+
YAMLException$1.prototype = Object.create(Error.prototype);
|
|
88
|
+
YAMLException$1.prototype.constructor = YAMLException$1;
|
|
89
|
+
YAMLException$1.prototype.toString = function toString(compact) {
|
|
90
|
+
return this.name + ": " + formatError(this, compact);
|
|
91
|
+
};
|
|
92
|
+
var exception = YAMLException$1;
|
|
93
|
+
function getLine(buffer, lineStart, lineEnd, position, maxLineLength) {
|
|
94
|
+
var head = "";
|
|
95
|
+
var tail = "";
|
|
96
|
+
var maxHalfLength = Math.floor(maxLineLength / 2) - 1;
|
|
97
|
+
if (position - lineStart > maxHalfLength) {
|
|
98
|
+
head = " ... ";
|
|
99
|
+
lineStart = position - maxHalfLength + head.length;
|
|
171
100
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (msg.parts && Array.isArray(msg.parts)) {
|
|
176
|
-
for (const part of msg.parts) {
|
|
177
|
-
if (part.type === "text" && part.text) {
|
|
178
|
-
return part.text;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
101
|
+
if (lineEnd - position > maxHalfLength) {
|
|
102
|
+
tail = " ...";
|
|
103
|
+
lineEnd = position + maxHalfLength - tail.length;
|
|
183
104
|
}
|
|
184
|
-
return
|
|
105
|
+
return {
|
|
106
|
+
str: head + buffer.slice(lineStart, lineEnd).replace(/\t/g, "→") + tail,
|
|
107
|
+
pos: position - lineStart + head.length
|
|
108
|
+
};
|
|
185
109
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
await client.session.delete({ path: { id: sessionId } });
|
|
189
|
-
} catch {}
|
|
190
|
-
}
|
|
191
|
-
async function spawnSession(client, agent, prompt, timeoutSeconds) {
|
|
192
|
-
const timeoutMs = timeoutSeconds * 1000;
|
|
193
|
-
try {
|
|
194
|
-
const createResponse = await client.session.create({});
|
|
195
|
-
const session = createResponse.data;
|
|
196
|
-
if (!session || !session.id) {
|
|
197
|
-
return {
|
|
198
|
-
content: "",
|
|
199
|
-
success: false,
|
|
200
|
-
error: "Failed to create session"
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
const sessionId = session.id;
|
|
204
|
-
await client.app.log({
|
|
205
|
-
body: {
|
|
206
|
-
service: "opencode-manifold",
|
|
207
|
-
level: "info",
|
|
208
|
-
message: `Created session ${sessionId} for agent ${agent}`
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
try {
|
|
212
|
-
await client.session.promptAsync({
|
|
213
|
-
path: { id: sessionId },
|
|
214
|
-
body: {
|
|
215
|
-
agent,
|
|
216
|
-
noReply: true,
|
|
217
|
-
parts: [{ type: "text", text: prompt }]
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
const isIdle = await waitForSessionIdle(client, sessionId, timeoutMs);
|
|
221
|
-
if (!isIdle) {
|
|
222
|
-
await client.session.abort({ path: { id: sessionId } });
|
|
223
|
-
return {
|
|
224
|
-
content: "",
|
|
225
|
-
success: false,
|
|
226
|
-
error: `Session timed out after ${timeoutSeconds}s`
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
const content = await getLastAssistantMessage(client, sessionId);
|
|
230
|
-
await client.app.log({
|
|
231
|
-
body: {
|
|
232
|
-
service: "opencode-manifold",
|
|
233
|
-
level: "info",
|
|
234
|
-
message: `Session ${sessionId} completed, content length: ${content.length}`
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
return {
|
|
238
|
-
content,
|
|
239
|
-
success: true
|
|
240
|
-
};
|
|
241
|
-
} finally {
|
|
242
|
-
await cleanupSession(client, sessionId);
|
|
243
|
-
}
|
|
244
|
-
} catch (error) {
|
|
245
|
-
return {
|
|
246
|
-
content: "",
|
|
247
|
-
success: false,
|
|
248
|
-
error: error instanceof Error ? error.message : String(error)
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
async function spawnClerkSession(client, prompt, agent, timeout) {
|
|
253
|
-
await client.app.log({
|
|
254
|
-
body: {
|
|
255
|
-
service: "opencode-manifold",
|
|
256
|
-
level: "info",
|
|
257
|
-
message: `Spawning Clerk session (agent: ${agent}, timeout: ${timeout}s)`
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
return spawnSession(client, agent, prompt, timeout);
|
|
261
|
-
}
|
|
262
|
-
async function spawnSeniorDevSession(client, prompt, agent, timeout) {
|
|
263
|
-
await client.app.log({
|
|
264
|
-
body: {
|
|
265
|
-
service: "opencode-manifold",
|
|
266
|
-
level: "info",
|
|
267
|
-
message: `Spawning Senior Dev session (agent: ${agent}, timeout: ${timeout}s)`
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
return spawnSession(client, agent, prompt, timeout);
|
|
271
|
-
}
|
|
272
|
-
async function spawnJuniorDevSession(client, prompt, seniorOutput, agent, timeout) {
|
|
273
|
-
await client.app.log({
|
|
274
|
-
body: {
|
|
275
|
-
service: "opencode-manifold",
|
|
276
|
-
level: "info",
|
|
277
|
-
message: `Spawning Junior Dev session (agent: ${agent}, timeout: ${timeout}s)`
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
const fullPrompt = `${prompt}
|
|
281
|
-
|
|
282
|
-
---
|
|
283
|
-
|
|
284
|
-
Senior Dev's Implementation:
|
|
285
|
-
${seniorOutput}`;
|
|
286
|
-
return spawnSession(client, agent, fullPrompt, timeout);
|
|
287
|
-
}
|
|
288
|
-
async function spawnDebugSession(client, prompt, loopHistory, agent, timeout) {
|
|
289
|
-
await client.app.log({
|
|
290
|
-
body: {
|
|
291
|
-
service: "opencode-manifold",
|
|
292
|
-
level: "info",
|
|
293
|
-
message: `Spawning Debug session (agent: ${agent}, timeout: ${timeout}s)`
|
|
294
|
-
}
|
|
295
|
-
});
|
|
296
|
-
const fullPrompt = `${prompt}
|
|
297
|
-
|
|
298
|
-
---
|
|
299
|
-
|
|
300
|
-
Loop History:
|
|
301
|
-
${loopHistory}`;
|
|
302
|
-
return spawnSession(client, agent, fullPrompt, timeout);
|
|
303
|
-
}
|
|
304
|
-
function parseJuniorFirstWord(response) {
|
|
305
|
-
const firstWord = response.trim().split(/\s+/)[0].toUpperCase();
|
|
306
|
-
if (firstWord === "COMPLETE") {
|
|
307
|
-
return "COMPLETE";
|
|
308
|
-
}
|
|
309
|
-
return "QUESTIONS";
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// src/error-handler.ts
|
|
313
|
-
class ModelCallError extends Error {
|
|
314
|
-
agent;
|
|
315
|
-
attempt;
|
|
316
|
-
constructor(message, agent, attempt) {
|
|
317
|
-
super(message);
|
|
318
|
-
this.agent = agent;
|
|
319
|
-
this.attempt = attempt;
|
|
320
|
-
this.name = "ModelCallError";
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
async function retryWithBackoff(fn, options) {
|
|
324
|
-
let lastError;
|
|
325
|
-
for (let attempt = 0;attempt <= options.maxRetries; attempt++) {
|
|
326
|
-
try {
|
|
327
|
-
return await fn();
|
|
328
|
-
} catch (error) {
|
|
329
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
330
|
-
if (attempt < options.maxRetries) {
|
|
331
|
-
options.onRetry?.(attempt + 1, lastError);
|
|
332
|
-
const delay = Math.pow(2, attempt) * 100;
|
|
333
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
throw lastError;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// src/graph.ts
|
|
341
|
-
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
342
|
-
import { existsSync as existsSync2 } from "fs";
|
|
343
|
-
import { join as join2, dirname as dirname2 } from "path";
|
|
344
|
-
|
|
345
|
-
// node_modules/js-yaml/dist/js-yaml.mjs
|
|
346
|
-
/*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT */
|
|
347
|
-
function isNothing(subject) {
|
|
348
|
-
return typeof subject === "undefined" || subject === null;
|
|
349
|
-
}
|
|
350
|
-
function isObject(subject) {
|
|
351
|
-
return typeof subject === "object" && subject !== null;
|
|
352
|
-
}
|
|
353
|
-
function toArray(sequence) {
|
|
354
|
-
if (Array.isArray(sequence))
|
|
355
|
-
return sequence;
|
|
356
|
-
else if (isNothing(sequence))
|
|
357
|
-
return [];
|
|
358
|
-
return [sequence];
|
|
359
|
-
}
|
|
360
|
-
function extend(target, source) {
|
|
361
|
-
var index, length, key, sourceKeys;
|
|
362
|
-
if (source) {
|
|
363
|
-
sourceKeys = Object.keys(source);
|
|
364
|
-
for (index = 0, length = sourceKeys.length;index < length; index += 1) {
|
|
365
|
-
key = sourceKeys[index];
|
|
366
|
-
target[key] = source[key];
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return target;
|
|
370
|
-
}
|
|
371
|
-
function repeat(string, count) {
|
|
372
|
-
var result = "", cycle;
|
|
373
|
-
for (cycle = 0;cycle < count; cycle += 1) {
|
|
374
|
-
result += string;
|
|
375
|
-
}
|
|
376
|
-
return result;
|
|
377
|
-
}
|
|
378
|
-
function isNegativeZero(number) {
|
|
379
|
-
return number === 0 && Number.NEGATIVE_INFINITY === 1 / number;
|
|
380
|
-
}
|
|
381
|
-
var isNothing_1 = isNothing;
|
|
382
|
-
var isObject_1 = isObject;
|
|
383
|
-
var toArray_1 = toArray;
|
|
384
|
-
var repeat_1 = repeat;
|
|
385
|
-
var isNegativeZero_1 = isNegativeZero;
|
|
386
|
-
var extend_1 = extend;
|
|
387
|
-
var common = {
|
|
388
|
-
isNothing: isNothing_1,
|
|
389
|
-
isObject: isObject_1,
|
|
390
|
-
toArray: toArray_1,
|
|
391
|
-
repeat: repeat_1,
|
|
392
|
-
isNegativeZero: isNegativeZero_1,
|
|
393
|
-
extend: extend_1
|
|
394
|
-
};
|
|
395
|
-
function formatError(exception, compact) {
|
|
396
|
-
var where = "", message = exception.reason || "(unknown reason)";
|
|
397
|
-
if (!exception.mark)
|
|
398
|
-
return message;
|
|
399
|
-
if (exception.mark.name) {
|
|
400
|
-
where += 'in "' + exception.mark.name + '" ';
|
|
401
|
-
}
|
|
402
|
-
where += "(" + (exception.mark.line + 1) + ":" + (exception.mark.column + 1) + ")";
|
|
403
|
-
if (!compact && exception.mark.snippet) {
|
|
404
|
-
where += `
|
|
405
|
-
|
|
406
|
-
` + exception.mark.snippet;
|
|
407
|
-
}
|
|
408
|
-
return message + " " + where;
|
|
409
|
-
}
|
|
410
|
-
function YAMLException$1(reason, mark) {
|
|
411
|
-
Error.call(this);
|
|
412
|
-
this.name = "YAMLException";
|
|
413
|
-
this.reason = reason;
|
|
414
|
-
this.mark = mark;
|
|
415
|
-
this.message = formatError(this, false);
|
|
416
|
-
if (Error.captureStackTrace) {
|
|
417
|
-
Error.captureStackTrace(this, this.constructor);
|
|
418
|
-
} else {
|
|
419
|
-
this.stack = new Error().stack || "";
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
YAMLException$1.prototype = Object.create(Error.prototype);
|
|
423
|
-
YAMLException$1.prototype.constructor = YAMLException$1;
|
|
424
|
-
YAMLException$1.prototype.toString = function toString(compact) {
|
|
425
|
-
return this.name + ": " + formatError(this, compact);
|
|
426
|
-
};
|
|
427
|
-
var exception = YAMLException$1;
|
|
428
|
-
function getLine(buffer, lineStart, lineEnd, position, maxLineLength) {
|
|
429
|
-
var head = "";
|
|
430
|
-
var tail = "";
|
|
431
|
-
var maxHalfLength = Math.floor(maxLineLength / 2) - 1;
|
|
432
|
-
if (position - lineStart > maxHalfLength) {
|
|
433
|
-
head = " ... ";
|
|
434
|
-
lineStart = position - maxHalfLength + head.length;
|
|
435
|
-
}
|
|
436
|
-
if (lineEnd - position > maxHalfLength) {
|
|
437
|
-
tail = " ...";
|
|
438
|
-
lineEnd = position + maxHalfLength - tail.length;
|
|
439
|
-
}
|
|
440
|
-
return {
|
|
441
|
-
str: head + buffer.slice(lineStart, lineEnd).replace(/\t/g, "→") + tail,
|
|
442
|
-
pos: position - lineStart + head.length
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
function padStart(string, max) {
|
|
446
|
-
return common.repeat(" ", max - string.length) + string;
|
|
110
|
+
function padStart(string, max) {
|
|
111
|
+
return common.repeat(" ", max - string.length) + string;
|
|
447
112
|
}
|
|
448
113
|
function makeSnippet(mark, options) {
|
|
449
114
|
options = Object.create(options || null);
|
|
@@ -3027,7 +2692,599 @@ var jsYaml = {
|
|
|
3027
2692
|
safeDump
|
|
3028
2693
|
};
|
|
3029
2694
|
|
|
2695
|
+
// src/tui.ts
|
|
2696
|
+
var MANIFOLD_AGENTS = ["clerk", "senior-dev", "junior-dev", "debug"];
|
|
2697
|
+
async function getManifoldAgents(directory) {
|
|
2698
|
+
const agentsDir = join(directory, ".opencode", "agents");
|
|
2699
|
+
if (!existsSync(agentsDir)) {
|
|
2700
|
+
return [];
|
|
2701
|
+
}
|
|
2702
|
+
const files = await readdir(agentsDir);
|
|
2703
|
+
const agents = [];
|
|
2704
|
+
for (const file of files) {
|
|
2705
|
+
if (!file.endsWith(".md"))
|
|
2706
|
+
continue;
|
|
2707
|
+
const name = file.replace(".md", "");
|
|
2708
|
+
if (MANIFOLD_AGENTS.includes(name)) {
|
|
2709
|
+
agents.push(name);
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
return agents;
|
|
2713
|
+
}
|
|
2714
|
+
async function readAgentFile(agentName, directory) {
|
|
2715
|
+
const agentPath = join(directory, ".opencode", "agents", `${agentName}.md`);
|
|
2716
|
+
if (!existsSync(agentPath)) {
|
|
2717
|
+
const globalPath = join(homedir(), ".config", "opencode", "manifold", "agents", `${agentName}.md`);
|
|
2718
|
+
if (existsSync(globalPath)) {
|
|
2719
|
+
const content2 = await readFile(globalPath, "utf-8");
|
|
2720
|
+
const { frontmatter: frontmatter2, body: body2 } = parseFrontmatter(content2);
|
|
2721
|
+
return { content: content2, frontmatter: frontmatter2, body: body2 };
|
|
2722
|
+
}
|
|
2723
|
+
throw new Error(`Agent file not found for ${agentName}`);
|
|
2724
|
+
}
|
|
2725
|
+
const content = await readFile(agentPath, "utf-8");
|
|
2726
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
2727
|
+
return { content, frontmatter, body };
|
|
2728
|
+
}
|
|
2729
|
+
function parseFrontmatter(content) {
|
|
2730
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
2731
|
+
if (!match) {
|
|
2732
|
+
return { frontmatter: {}, body: content };
|
|
2733
|
+
}
|
|
2734
|
+
const frontmatterYaml = match[1];
|
|
2735
|
+
const body = match[2];
|
|
2736
|
+
const frontmatter = jsYaml.load(frontmatterYaml) || {};
|
|
2737
|
+
return { frontmatter, body };
|
|
2738
|
+
}
|
|
2739
|
+
function buildAgentFile(frontmatter, body) {
|
|
2740
|
+
const yamlContent = jsYaml.dump(frontmatter, {
|
|
2741
|
+
lineWidth: -1,
|
|
2742
|
+
noCompatMode: true
|
|
2743
|
+
});
|
|
2744
|
+
return `---
|
|
2745
|
+
${yamlContent}---
|
|
2746
|
+
${body}`;
|
|
2747
|
+
}
|
|
2748
|
+
async function updateAgentModel(agentName, modelId, directory) {
|
|
2749
|
+
const { frontmatter, body } = await readAgentFile(agentName, directory);
|
|
2750
|
+
frontmatter.model = modelId;
|
|
2751
|
+
const newContent = buildAgentFile(frontmatter, body);
|
|
2752
|
+
const agentPath = join(directory, ".opencode", "agents", `${agentName}.md`);
|
|
2753
|
+
await writeFile(agentPath, newContent);
|
|
2754
|
+
}
|
|
2755
|
+
var tui = async (api) => {
|
|
2756
|
+
api.command.register(() => [
|
|
2757
|
+
{
|
|
2758
|
+
title: "Manifold Models",
|
|
2759
|
+
value: "manifold-models",
|
|
2760
|
+
description: "Set the model for a Manifold sub-agent",
|
|
2761
|
+
category: "Manifold",
|
|
2762
|
+
slash: {
|
|
2763
|
+
name: "manifold-models"
|
|
2764
|
+
}
|
|
2765
|
+
},
|
|
2766
|
+
{
|
|
2767
|
+
title: "Manifold Update",
|
|
2768
|
+
value: "manifold-update",
|
|
2769
|
+
description: "Clear opencode-manifold plugin cache and prompt for restart",
|
|
2770
|
+
category: "Manifold",
|
|
2771
|
+
slash: {
|
|
2772
|
+
name: "manifold-update"
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
]);
|
|
2776
|
+
api.event.on("tui.command.execute", async (event) => {
|
|
2777
|
+
if (event.properties.command === "manifold-models") {
|
|
2778
|
+
await handleManifoldModels(api);
|
|
2779
|
+
} else if (event.properties.command === "manifold-update") {
|
|
2780
|
+
await handleManifoldUpdate(api);
|
|
2781
|
+
}
|
|
2782
|
+
});
|
|
2783
|
+
};
|
|
2784
|
+
async function handleManifoldModels(api) {
|
|
2785
|
+
const directory = api.state.path.directory;
|
|
2786
|
+
const agents = await getManifoldAgents(directory);
|
|
2787
|
+
if (agents.length === 0) {
|
|
2788
|
+
api.ui.toast({
|
|
2789
|
+
variant: "error",
|
|
2790
|
+
message: "No Manifold agents found. Run /manifold-init first."
|
|
2791
|
+
});
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2794
|
+
const providers = api.state.provider;
|
|
2795
|
+
const models = [];
|
|
2796
|
+
for (const provider of providers) {
|
|
2797
|
+
if (provider.models) {
|
|
2798
|
+
for (const [modelId, model] of Object.entries(provider.models)) {
|
|
2799
|
+
models.push({
|
|
2800
|
+
id: `${provider.id}/${modelId}`,
|
|
2801
|
+
name: model.name || modelId,
|
|
2802
|
+
providerID: provider.id
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
if (models.length === 0) {
|
|
2808
|
+
api.ui.toast({
|
|
2809
|
+
variant: "error",
|
|
2810
|
+
message: "No models available. Configure providers first."
|
|
2811
|
+
});
|
|
2812
|
+
return;
|
|
2813
|
+
}
|
|
2814
|
+
models.sort((a, b) => a.name.localeCompare(b.name));
|
|
2815
|
+
const agentOptions = agents.map((agent) => ({
|
|
2816
|
+
title: `${agent} (sub-agent)`,
|
|
2817
|
+
value: agent,
|
|
2818
|
+
description: `Configure model for ${agent}`
|
|
2819
|
+
}));
|
|
2820
|
+
api.ui.dialog.setSize("medium");
|
|
2821
|
+
return new Promise((resolve) => {
|
|
2822
|
+
api.ui.ui.DialogSelect({
|
|
2823
|
+
title: "Select Manifold Sub-Agent",
|
|
2824
|
+
options: agentOptions,
|
|
2825
|
+
onSelect: async (option) => {
|
|
2826
|
+
const selectedAgent = option.value;
|
|
2827
|
+
const currentModel = await readAgentFile(selectedAgent, directory).then((f) => f.frontmatter.model || "not set").catch(() => "not set");
|
|
2828
|
+
const modelOptions = models.map((model) => ({
|
|
2829
|
+
title: `${model.name}`,
|
|
2830
|
+
value: model.id,
|
|
2831
|
+
description: model.id,
|
|
2832
|
+
footer: model.id === currentModel ? "✓ Current" : undefined
|
|
2833
|
+
}));
|
|
2834
|
+
api.ui.ui.DialogSelect({
|
|
2835
|
+
title: `Select Model for ${selectedAgent}`,
|
|
2836
|
+
options: modelOptions,
|
|
2837
|
+
current: currentModel !== "not set" ? currentModel : undefined,
|
|
2838
|
+
onSelect: async (modelOption) => {
|
|
2839
|
+
const selectedModelId = modelOption.value;
|
|
2840
|
+
await updateAgentModel(selectedAgent, selectedModelId, directory);
|
|
2841
|
+
api.ui.toast({
|
|
2842
|
+
variant: "success",
|
|
2843
|
+
message: `Set ${selectedAgent} to ${selectedModelId}`
|
|
2844
|
+
});
|
|
2845
|
+
resolve();
|
|
2846
|
+
}
|
|
2847
|
+
});
|
|
2848
|
+
}
|
|
2849
|
+
});
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
async function handleManifoldUpdate(api) {
|
|
2853
|
+
const directory = api.state.path.directory;
|
|
2854
|
+
const settingsPath = join(directory, "Manifold", "settings.json");
|
|
2855
|
+
let settings = {};
|
|
2856
|
+
if (existsSync(settingsPath)) {
|
|
2857
|
+
const content = await readFile(settingsPath, "utf-8");
|
|
2858
|
+
try {
|
|
2859
|
+
settings = JSON.parse(content);
|
|
2860
|
+
} catch {}
|
|
2861
|
+
}
|
|
2862
|
+
const configuredPaths = settings.updateCachePaths || [];
|
|
2863
|
+
if (configuredPaths.length === 0) {
|
|
2864
|
+
api.ui.dialog.setSize("large");
|
|
2865
|
+
api.ui.ui.DialogSelect({
|
|
2866
|
+
title: "Manifold Plugin Update",
|
|
2867
|
+
options: [
|
|
2868
|
+
{
|
|
2869
|
+
title: "Configure Cache Paths",
|
|
2870
|
+
value: "configure",
|
|
2871
|
+
description: "Add updateCachePaths to Manifold/settings.json",
|
|
2872
|
+
footer: "Opens settings file"
|
|
2873
|
+
}
|
|
2874
|
+
],
|
|
2875
|
+
onSelect: () => {
|
|
2876
|
+
api.ui.toast({
|
|
2877
|
+
variant: "info",
|
|
2878
|
+
message: "Add updateCachePaths to Manifold/settings.json"
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
});
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
const resolvedPaths = configuredPaths.map((p) => {
|
|
2885
|
+
const expanded = p.startsWith("~") ? join(homedir(), p.slice(1)) : p;
|
|
2886
|
+
return expanded;
|
|
2887
|
+
});
|
|
2888
|
+
api.ui.dialog.setSize("large");
|
|
2889
|
+
return new Promise((resolve) => {
|
|
2890
|
+
api.ui.ui.DialogSelect({
|
|
2891
|
+
title: "Manifold Plugin Update",
|
|
2892
|
+
options: [
|
|
2893
|
+
{
|
|
2894
|
+
title: "Clear Cache",
|
|
2895
|
+
value: "confirm",
|
|
2896
|
+
description: `Will clear ${resolvedPaths.length} path(s)`,
|
|
2897
|
+
footer: resolvedPaths.map((p) => `• ${p}`).join(`
|
|
2898
|
+
`)
|
|
2899
|
+
},
|
|
2900
|
+
{
|
|
2901
|
+
title: "Cancel",
|
|
2902
|
+
value: "cancel",
|
|
2903
|
+
description: "Do not clear cache"
|
|
2904
|
+
}
|
|
2905
|
+
],
|
|
2906
|
+
onSelect: async (option) => {
|
|
2907
|
+
if (option.value === "cancel") {
|
|
2908
|
+
resolve();
|
|
2909
|
+
return;
|
|
2910
|
+
}
|
|
2911
|
+
const { exec } = await import("child_process");
|
|
2912
|
+
const { promisify } = await import("util");
|
|
2913
|
+
const execAsync = promisify(exec);
|
|
2914
|
+
const cleared = [];
|
|
2915
|
+
const skipped = [];
|
|
2916
|
+
const blocked = [];
|
|
2917
|
+
for (const pathStr of resolvedPaths) {
|
|
2918
|
+
try {
|
|
2919
|
+
await execAsync(`rm -rf "${pathStr}"`);
|
|
2920
|
+
cleared.push(pathStr);
|
|
2921
|
+
} catch (error) {
|
|
2922
|
+
blocked.push({ path: pathStr, reason: `Failed to delete: ${error}` });
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
let message = "";
|
|
2926
|
+
if (cleared.length > 0) {
|
|
2927
|
+
message += `✅ Cleared: ${cleared.length} path(s)
|
|
2928
|
+
`;
|
|
2929
|
+
}
|
|
2930
|
+
if (blocked.length > 0) {
|
|
2931
|
+
message += `\uD83D\uDEAB Blocked: ${blocked.length} path(s)
|
|
2932
|
+
`;
|
|
2933
|
+
}
|
|
2934
|
+
if (cleared.length > 0) {
|
|
2935
|
+
message += `
|
|
2936
|
+
Restart opencode to pull the latest plugin version.`;
|
|
2937
|
+
}
|
|
2938
|
+
api.ui.toast({
|
|
2939
|
+
variant: cleared.length > 0 ? "success" : "error",
|
|
2940
|
+
message: cleared.length > 0 ? `Cache cleared. Restart opencode to update.` : `Cache update blocked`
|
|
2941
|
+
});
|
|
2942
|
+
resolve();
|
|
2943
|
+
}
|
|
2944
|
+
});
|
|
2945
|
+
});
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
// src/init.ts
|
|
2949
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir, readdir as readdir2 } from "fs/promises";
|
|
2950
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2951
|
+
import { join as join2, dirname } from "path";
|
|
2952
|
+
import { fileURLToPath } from "url";
|
|
2953
|
+
import { homedir as homedir2 } from "os";
|
|
2954
|
+
import { createRequire as createRequire2 } from "module";
|
|
2955
|
+
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
2956
|
+
var require2 = createRequire2(import.meta.url);
|
|
2957
|
+
var bundledTemplatesDir = join2(__dirname2, "..", "src", "templates");
|
|
2958
|
+
var globalTemplatesDir = join2(homedir2(), ".config", "opencode", "manifold");
|
|
2959
|
+
async function getPluginVersion() {
|
|
2960
|
+
try {
|
|
2961
|
+
const packageJson = require2(join2(__dirname2, "..", "package.json"));
|
|
2962
|
+
return packageJson.version || "unknown";
|
|
2963
|
+
} catch {
|
|
2964
|
+
return "unknown";
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
async function dirHasContent(dirPath) {
|
|
2968
|
+
if (!existsSync2(dirPath))
|
|
2969
|
+
return false;
|
|
2970
|
+
try {
|
|
2971
|
+
const entries = await readdir2(dirPath);
|
|
2972
|
+
return entries.length > 0;
|
|
2973
|
+
} catch {
|
|
2974
|
+
return false;
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
async function copyFiles(src, dest) {
|
|
2978
|
+
if (!existsSync2(src))
|
|
2979
|
+
return [];
|
|
2980
|
+
await mkdir(dest, { recursive: true });
|
|
2981
|
+
const copied = [];
|
|
2982
|
+
const entries = await readdir2(src, { withFileTypes: true });
|
|
2983
|
+
for (const entry of entries) {
|
|
2984
|
+
const srcPath = join2(src, entry.name);
|
|
2985
|
+
const destPath = join2(dest, entry.name);
|
|
2986
|
+
if (entry.isDirectory()) {
|
|
2987
|
+
const subCopied = await copyFiles(srcPath, destPath);
|
|
2988
|
+
if (subCopied.length > 0) {
|
|
2989
|
+
copied.push(entry.name);
|
|
2990
|
+
}
|
|
2991
|
+
} else {
|
|
2992
|
+
await writeFile2(destPath, await readFile2(srcPath));
|
|
2993
|
+
copied.push(entry.name);
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
return copied;
|
|
2997
|
+
}
|
|
2998
|
+
async function ensureGlobalTemplates(ctx) {
|
|
2999
|
+
await ctx.client.app.log({
|
|
3000
|
+
body: {
|
|
3001
|
+
service: "opencode-manifold",
|
|
3002
|
+
level: "info",
|
|
3003
|
+
message: "Synchronizing global templates..."
|
|
3004
|
+
}
|
|
3005
|
+
});
|
|
3006
|
+
if (!existsSync2(bundledTemplatesDir)) {
|
|
3007
|
+
await ctx.client.app.log({
|
|
3008
|
+
body: {
|
|
3009
|
+
service: "opencode-manifold",
|
|
3010
|
+
level: "error",
|
|
3011
|
+
message: `Bundled templates not found at ${bundledTemplatesDir}`
|
|
3012
|
+
}
|
|
3013
|
+
});
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
await copyFiles(bundledTemplatesDir, globalTemplatesDir);
|
|
3017
|
+
const globalCommandsDir = join2(homedir2(), ".config", "opencode", "commands");
|
|
3018
|
+
const bundledCommandsDir = join2(bundledTemplatesDir, "commands");
|
|
3019
|
+
if (existsSync2(bundledCommandsDir)) {
|
|
3020
|
+
await copyFiles(bundledCommandsDir, globalCommandsDir);
|
|
3021
|
+
}
|
|
3022
|
+
await ctx.client.app.log({
|
|
3023
|
+
body: {
|
|
3024
|
+
service: "opencode-manifold",
|
|
3025
|
+
level: "info",
|
|
3026
|
+
message: "Global templates synchronized"
|
|
3027
|
+
}
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
async function initProject(directory, client) {
|
|
3031
|
+
const initialized = [];
|
|
3032
|
+
await client.app.log({
|
|
3033
|
+
body: {
|
|
3034
|
+
service: "opencode-manifold",
|
|
3035
|
+
level: "info",
|
|
3036
|
+
message: `Running /manifold-init in ${directory}`
|
|
3037
|
+
}
|
|
3038
|
+
});
|
|
3039
|
+
if (!await dirHasContent(globalTemplatesDir)) {
|
|
3040
|
+
await client.app.log({
|
|
3041
|
+
body: {
|
|
3042
|
+
service: "opencode-manifold",
|
|
3043
|
+
level: "error",
|
|
3044
|
+
message: `Global templates not found at ${globalTemplatesDir}. Plugin may not have loaded correctly.`
|
|
3045
|
+
}
|
|
3046
|
+
});
|
|
3047
|
+
return initialized;
|
|
3048
|
+
}
|
|
3049
|
+
const agentsCopied = await copyMissingFiles(join2(globalTemplatesDir, "agents"), join2(directory, ".opencode", "agents"));
|
|
3050
|
+
if (agentsCopied.length > 0) {
|
|
3051
|
+
initialized.push(`agents (${agentsCopied.join(", ")})`);
|
|
3052
|
+
}
|
|
3053
|
+
const skillsCopied = await copyMissingFiles(join2(globalTemplatesDir, "skills"), join2(directory, ".opencode", "skills"));
|
|
3054
|
+
if (skillsCopied.length > 0) {
|
|
3055
|
+
initialized.push(`skills (${skillsCopied.join(", ")})`);
|
|
3056
|
+
}
|
|
3057
|
+
const manifoldCopied = await copyMissingFiles(join2(globalTemplatesDir, "manifold"), join2(directory, "Manifold"));
|
|
3058
|
+
if (manifoldCopied.length > 0) {
|
|
3059
|
+
initialized.push(`Manifold/ (${manifoldCopied.join(", ")})`);
|
|
3060
|
+
}
|
|
3061
|
+
const version = await getPluginVersion();
|
|
3062
|
+
await writeFile2(join2(directory, "Manifold", "VERSION"), version + `
|
|
3063
|
+
`);
|
|
3064
|
+
await client.app.log({
|
|
3065
|
+
body: {
|
|
3066
|
+
service: "opencode-manifold",
|
|
3067
|
+
level: "info",
|
|
3068
|
+
message: `/manifold-init complete: ${initialized.join(", ") || "already initialized"}`
|
|
3069
|
+
}
|
|
3070
|
+
});
|
|
3071
|
+
return initialized;
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
// src/tools/dispatch-task.ts
|
|
3075
|
+
import { tool } from "@opencode-ai/plugin";
|
|
3076
|
+
import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
3077
|
+
import { existsSync as existsSync6 } from "fs";
|
|
3078
|
+
import { join as join6 } from "path";
|
|
3079
|
+
|
|
3080
|
+
// src/state-machine.ts
|
|
3081
|
+
import { readFile as readFile4, writeFile as writeFile4, readdir as readdir3 } from "fs/promises";
|
|
3082
|
+
import { existsSync as existsSync5 } from "fs";
|
|
3083
|
+
import { join as join5 } from "path";
|
|
3084
|
+
|
|
3085
|
+
// src/session-spawner.ts
|
|
3086
|
+
async function waitForSessionIdle(client, sessionId, timeoutMs) {
|
|
3087
|
+
const startTime = Date.now();
|
|
3088
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
3089
|
+
const statusResponse = await client.session.status({});
|
|
3090
|
+
const statusData = statusResponse.data;
|
|
3091
|
+
if (statusData && statusData[sessionId]) {
|
|
3092
|
+
const status = statusData[sessionId];
|
|
3093
|
+
if (status.type === "idle") {
|
|
3094
|
+
return true;
|
|
3095
|
+
}
|
|
3096
|
+
if (status.type === "retry") {
|
|
3097
|
+
const waitTime = status.next - Date.now();
|
|
3098
|
+
if (waitTime > 0) {
|
|
3099
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(waitTime, 1000)));
|
|
3100
|
+
}
|
|
3101
|
+
continue;
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3105
|
+
}
|
|
3106
|
+
return false;
|
|
3107
|
+
}
|
|
3108
|
+
async function getLastAssistantMessage(client, sessionId) {
|
|
3109
|
+
const messagesResponse = await client.session.messages({
|
|
3110
|
+
path: { id: sessionId }
|
|
3111
|
+
});
|
|
3112
|
+
const messages = messagesResponse.data;
|
|
3113
|
+
if (!messages || !Array.isArray(messages)) {
|
|
3114
|
+
return "";
|
|
3115
|
+
}
|
|
3116
|
+
for (let i2 = messages.length - 1;i2 >= 0; i2--) {
|
|
3117
|
+
const msg = messages[i2];
|
|
3118
|
+
if (msg.info && msg.info.role === "assistant") {
|
|
3119
|
+
if (msg.parts && Array.isArray(msg.parts)) {
|
|
3120
|
+
for (const part of msg.parts) {
|
|
3121
|
+
if (part.type === "text" && part.text) {
|
|
3122
|
+
return part.text;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
return "";
|
|
3129
|
+
}
|
|
3130
|
+
async function cleanupSession(client, sessionId) {
|
|
3131
|
+
try {
|
|
3132
|
+
await client.session.delete({ path: { id: sessionId } });
|
|
3133
|
+
} catch {}
|
|
3134
|
+
}
|
|
3135
|
+
async function spawnSession(client, agent, prompt, timeoutSeconds) {
|
|
3136
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
3137
|
+
try {
|
|
3138
|
+
const createResponse = await client.session.create({});
|
|
3139
|
+
const session = createResponse.data;
|
|
3140
|
+
if (!session || !session.id) {
|
|
3141
|
+
return {
|
|
3142
|
+
content: "",
|
|
3143
|
+
success: false,
|
|
3144
|
+
error: "Failed to create session"
|
|
3145
|
+
};
|
|
3146
|
+
}
|
|
3147
|
+
const sessionId = session.id;
|
|
3148
|
+
await client.app.log({
|
|
3149
|
+
body: {
|
|
3150
|
+
service: "opencode-manifold",
|
|
3151
|
+
level: "info",
|
|
3152
|
+
message: `Created session ${sessionId} for agent ${agent}`
|
|
3153
|
+
}
|
|
3154
|
+
});
|
|
3155
|
+
try {
|
|
3156
|
+
await client.session.promptAsync({
|
|
3157
|
+
path: { id: sessionId },
|
|
3158
|
+
body: {
|
|
3159
|
+
agent,
|
|
3160
|
+
noReply: true,
|
|
3161
|
+
parts: [{ type: "text", text: prompt }]
|
|
3162
|
+
}
|
|
3163
|
+
});
|
|
3164
|
+
const isIdle = await waitForSessionIdle(client, sessionId, timeoutMs);
|
|
3165
|
+
if (!isIdle) {
|
|
3166
|
+
await client.session.abort({ path: { id: sessionId } });
|
|
3167
|
+
return {
|
|
3168
|
+
content: "",
|
|
3169
|
+
success: false,
|
|
3170
|
+
error: `Session timed out after ${timeoutSeconds}s`
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
const content = await getLastAssistantMessage(client, sessionId);
|
|
3174
|
+
await client.app.log({
|
|
3175
|
+
body: {
|
|
3176
|
+
service: "opencode-manifold",
|
|
3177
|
+
level: "info",
|
|
3178
|
+
message: `Session ${sessionId} completed, content length: ${content.length}`
|
|
3179
|
+
}
|
|
3180
|
+
});
|
|
3181
|
+
return {
|
|
3182
|
+
content,
|
|
3183
|
+
success: true
|
|
3184
|
+
};
|
|
3185
|
+
} finally {
|
|
3186
|
+
await cleanupSession(client, sessionId);
|
|
3187
|
+
}
|
|
3188
|
+
} catch (error) {
|
|
3189
|
+
return {
|
|
3190
|
+
content: "",
|
|
3191
|
+
success: false,
|
|
3192
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
async function spawnClerkSession(client, prompt, agent, timeout) {
|
|
3197
|
+
await client.app.log({
|
|
3198
|
+
body: {
|
|
3199
|
+
service: "opencode-manifold",
|
|
3200
|
+
level: "info",
|
|
3201
|
+
message: `Spawning Clerk session (agent: ${agent}, timeout: ${timeout}s)`
|
|
3202
|
+
}
|
|
3203
|
+
});
|
|
3204
|
+
return spawnSession(client, agent, prompt, timeout);
|
|
3205
|
+
}
|
|
3206
|
+
async function spawnSeniorDevSession(client, prompt, agent, timeout) {
|
|
3207
|
+
await client.app.log({
|
|
3208
|
+
body: {
|
|
3209
|
+
service: "opencode-manifold",
|
|
3210
|
+
level: "info",
|
|
3211
|
+
message: `Spawning Senior Dev session (agent: ${agent}, timeout: ${timeout}s)`
|
|
3212
|
+
}
|
|
3213
|
+
});
|
|
3214
|
+
return spawnSession(client, agent, prompt, timeout);
|
|
3215
|
+
}
|
|
3216
|
+
async function spawnJuniorDevSession(client, prompt, seniorOutput, agent, timeout) {
|
|
3217
|
+
await client.app.log({
|
|
3218
|
+
body: {
|
|
3219
|
+
service: "opencode-manifold",
|
|
3220
|
+
level: "info",
|
|
3221
|
+
message: `Spawning Junior Dev session (agent: ${agent}, timeout: ${timeout}s)`
|
|
3222
|
+
}
|
|
3223
|
+
});
|
|
3224
|
+
const fullPrompt = `${prompt}
|
|
3225
|
+
|
|
3226
|
+
---
|
|
3227
|
+
|
|
3228
|
+
Senior Dev's Implementation:
|
|
3229
|
+
${seniorOutput}`;
|
|
3230
|
+
return spawnSession(client, agent, fullPrompt, timeout);
|
|
3231
|
+
}
|
|
3232
|
+
async function spawnDebugSession(client, prompt, loopHistory, agent, timeout) {
|
|
3233
|
+
await client.app.log({
|
|
3234
|
+
body: {
|
|
3235
|
+
service: "opencode-manifold",
|
|
3236
|
+
level: "info",
|
|
3237
|
+
message: `Spawning Debug session (agent: ${agent}, timeout: ${timeout}s)`
|
|
3238
|
+
}
|
|
3239
|
+
});
|
|
3240
|
+
const fullPrompt = `${prompt}
|
|
3241
|
+
|
|
3242
|
+
---
|
|
3243
|
+
|
|
3244
|
+
Loop History:
|
|
3245
|
+
${loopHistory}`;
|
|
3246
|
+
return spawnSession(client, agent, fullPrompt, timeout);
|
|
3247
|
+
}
|
|
3248
|
+
function parseJuniorFirstWord(response) {
|
|
3249
|
+
const firstWord = response.trim().split(/\s+/)[0].toUpperCase();
|
|
3250
|
+
if (firstWord === "COMPLETE") {
|
|
3251
|
+
return "COMPLETE";
|
|
3252
|
+
}
|
|
3253
|
+
return "QUESTIONS";
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
// src/error-handler.ts
|
|
3257
|
+
class ModelCallError extends Error {
|
|
3258
|
+
agent;
|
|
3259
|
+
attempt;
|
|
3260
|
+
constructor(message, agent, attempt) {
|
|
3261
|
+
super(message);
|
|
3262
|
+
this.agent = agent;
|
|
3263
|
+
this.attempt = attempt;
|
|
3264
|
+
this.name = "ModelCallError";
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
async function retryWithBackoff(fn, options) {
|
|
3268
|
+
let lastError;
|
|
3269
|
+
for (let attempt = 0;attempt <= options.maxRetries; attempt++) {
|
|
3270
|
+
try {
|
|
3271
|
+
return await fn();
|
|
3272
|
+
} catch (error) {
|
|
3273
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
3274
|
+
if (attempt < options.maxRetries) {
|
|
3275
|
+
options.onRetry?.(attempt + 1, lastError);
|
|
3276
|
+
const delay = Math.pow(2, attempt) * 100;
|
|
3277
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
throw lastError;
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3030
3284
|
// src/graph.ts
|
|
3285
|
+
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
3286
|
+
import { existsSync as existsSync3 } from "fs";
|
|
3287
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
3031
3288
|
var ENCODE_MAP = { "/": "__SL__", ".": "__DT__" };
|
|
3032
3289
|
var ENCODE_RE = /[/.]/g;
|
|
3033
3290
|
function pathToGraphFilename(filePath) {
|
|
@@ -3137,12 +3394,12 @@ ${tasksSection}
|
|
|
3137
3394
|
}
|
|
3138
3395
|
async function readGraphFile(directory, filePath) {
|
|
3139
3396
|
const filename = pathToGraphFilename(filePath);
|
|
3140
|
-
const graphPath =
|
|
3141
|
-
if (!
|
|
3397
|
+
const graphPath = join3(directory, "Manifold", "graph", filename);
|
|
3398
|
+
if (!existsSync3(graphPath)) {
|
|
3142
3399
|
return null;
|
|
3143
3400
|
}
|
|
3144
3401
|
try {
|
|
3145
|
-
const content = await
|
|
3402
|
+
const content = await readFile3(graphPath, "utf-8");
|
|
3146
3403
|
return parseGraphContent(content);
|
|
3147
3404
|
} catch {
|
|
3148
3405
|
return null;
|
|
@@ -3150,9 +3407,9 @@ async function readGraphFile(directory, filePath) {
|
|
|
3150
3407
|
}
|
|
3151
3408
|
async function updateGraphFile(directory, filePath, taskId, calls, dependsOn) {
|
|
3152
3409
|
const filename = pathToGraphFilename(filePath);
|
|
3153
|
-
const graphPath =
|
|
3410
|
+
const graphPath = join3(directory, "Manifold", "graph", filename);
|
|
3154
3411
|
let entry;
|
|
3155
|
-
if (
|
|
3412
|
+
if (existsSync3(graphPath)) {
|
|
3156
3413
|
const existing = await readGraphFile(directory, filePath);
|
|
3157
3414
|
entry = existing || {
|
|
3158
3415
|
filePath,
|
|
@@ -3178,11 +3435,11 @@ async function updateGraphFile(directory, filePath, taskId, calls, dependsOn) {
|
|
|
3178
3435
|
entry.tasksThatEdited.push(taskId);
|
|
3179
3436
|
}
|
|
3180
3437
|
const graphDir = dirname2(graphPath);
|
|
3181
|
-
if (!
|
|
3438
|
+
if (!existsSync3(graphDir)) {
|
|
3182
3439
|
await mkdir2(graphDir, { recursive: true });
|
|
3183
3440
|
}
|
|
3184
3441
|
const content = formatGraphContent(entry);
|
|
3185
|
-
await
|
|
3442
|
+
await writeFile3(graphPath, content, "utf-8");
|
|
3186
3443
|
}
|
|
3187
3444
|
async function updateGraphFilesForTask(directory, taskId, files) {
|
|
3188
3445
|
for (const file of files) {
|
|
@@ -3195,9 +3452,9 @@ async function updateGraphFilesForTask(directory, taskId, files) {
|
|
|
3195
3452
|
}
|
|
3196
3453
|
async function replaceGraphCalls(directory, filePath, calls, dependsOn) {
|
|
3197
3454
|
const filename = pathToGraphFilename(filePath);
|
|
3198
|
-
const graphPath =
|
|
3455
|
+
const graphPath = join3(directory, "Manifold", "graph", filename);
|
|
3199
3456
|
let entry;
|
|
3200
|
-
if (
|
|
3457
|
+
if (existsSync3(graphPath)) {
|
|
3201
3458
|
const existing = await readGraphFile(directory, filePath);
|
|
3202
3459
|
entry = existing || {
|
|
3203
3460
|
filePath,
|
|
@@ -3216,20 +3473,20 @@ async function replaceGraphCalls(directory, filePath, calls, dependsOn) {
|
|
|
3216
3473
|
entry.calls = [...new Set(calls)];
|
|
3217
3474
|
entry.dependsOn = [...new Set(dependsOn)];
|
|
3218
3475
|
const graphDir = dirname2(graphPath);
|
|
3219
|
-
if (!
|
|
3476
|
+
if (!existsSync3(graphDir)) {
|
|
3220
3477
|
await mkdir2(graphDir, { recursive: true });
|
|
3221
3478
|
}
|
|
3222
3479
|
const content = formatGraphContent(entry);
|
|
3223
|
-
await
|
|
3480
|
+
await writeFile3(graphPath, content, "utf-8");
|
|
3224
3481
|
}
|
|
3225
3482
|
|
|
3226
3483
|
// src/graph-sync.ts
|
|
3227
3484
|
import Database from "better-sqlite3";
|
|
3228
|
-
import { existsSync as
|
|
3229
|
-
import { join as
|
|
3485
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3486
|
+
import { join as join4 } from "path";
|
|
3230
3487
|
function openIndexDb(projectRoot) {
|
|
3231
|
-
const dbPath =
|
|
3232
|
-
if (!
|
|
3488
|
+
const dbPath = join4(projectRoot, ".opencode", "index", "codebase.db");
|
|
3489
|
+
if (!existsSync4(dbPath)) {
|
|
3233
3490
|
return null;
|
|
3234
3491
|
}
|
|
3235
3492
|
try {
|
|
@@ -3317,9 +3574,9 @@ async function syncGraphFilesFromIndex(projectRoot, files) {
|
|
|
3317
3574
|
|
|
3318
3575
|
// src/state-machine.ts
|
|
3319
3576
|
async function readState(directory) {
|
|
3320
|
-
const statePath =
|
|
3321
|
-
if (
|
|
3322
|
-
const content = await
|
|
3577
|
+
const statePath = join5(directory, "Manifold", "state.json");
|
|
3578
|
+
if (existsSync5(statePath)) {
|
|
3579
|
+
const content = await readFile4(statePath, "utf-8");
|
|
3323
3580
|
return JSON.parse(content);
|
|
3324
3581
|
}
|
|
3325
3582
|
return {
|
|
@@ -3335,8 +3592,8 @@ async function readState(directory) {
|
|
|
3335
3592
|
};
|
|
3336
3593
|
}
|
|
3337
3594
|
async function writeState(directory, state) {
|
|
3338
|
-
const statePath =
|
|
3339
|
-
await
|
|
3595
|
+
const statePath = join5(directory, "Manifold", "state.json");
|
|
3596
|
+
await writeFile4(statePath, JSON.stringify(state, null, 2));
|
|
3340
3597
|
}
|
|
3341
3598
|
function buildLoopHistory(state) {
|
|
3342
3599
|
if (state.loop_history.length === 0) {
|
|
@@ -3348,16 +3605,16 @@ ${entry}`).join(`
|
|
|
3348
3605
|
`);
|
|
3349
3606
|
}
|
|
3350
3607
|
async function readRecentTaskLogs(directory, count) {
|
|
3351
|
-
const tasksDir =
|
|
3352
|
-
if (!
|
|
3608
|
+
const tasksDir = join5(directory, "Manifold", "tasks");
|
|
3609
|
+
if (!existsSync5(tasksDir)) {
|
|
3353
3610
|
return [];
|
|
3354
3611
|
}
|
|
3355
3612
|
try {
|
|
3356
|
-
const files = await
|
|
3613
|
+
const files = await readdir3(tasksDir);
|
|
3357
3614
|
const mdFiles = files.filter((f) => f.endsWith(".md")).sort();
|
|
3358
3615
|
const recentFiles = mdFiles.slice(-count);
|
|
3359
3616
|
const tasks = await Promise.all(recentFiles.map(async (filename) => {
|
|
3360
|
-
const content = await
|
|
3617
|
+
const content = await readFile4(join5(tasksDir, filename), "utf-8");
|
|
3361
3618
|
return { filename, content };
|
|
3362
3619
|
}));
|
|
3363
3620
|
return tasks;
|
|
@@ -4065,9 +4322,9 @@ function setPluginContext(client) {
|
|
|
4065
4322
|
pluginClient = client;
|
|
4066
4323
|
}
|
|
4067
4324
|
async function readSettings(directory) {
|
|
4068
|
-
const settingsPath =
|
|
4069
|
-
if (
|
|
4070
|
-
const content = await
|
|
4325
|
+
const settingsPath = join6(directory, "Manifold", "settings.json");
|
|
4326
|
+
if (existsSync6(settingsPath)) {
|
|
4327
|
+
const content = await readFile5(settingsPath, "utf-8");
|
|
4071
4328
|
return JSON.parse(content);
|
|
4072
4329
|
}
|
|
4073
4330
|
return {
|
|
@@ -4082,10 +4339,10 @@ async function readSettings(directory) {
|
|
|
4082
4339
|
};
|
|
4083
4340
|
}
|
|
4084
4341
|
async function updatePlansRegistry(directory, planFile) {
|
|
4085
|
-
const plansPath =
|
|
4342
|
+
const plansPath = join6(directory, "Manifold", "plans.json");
|
|
4086
4343
|
let plans = {};
|
|
4087
|
-
if (
|
|
4088
|
-
const content = await
|
|
4344
|
+
if (existsSync6(plansPath)) {
|
|
4345
|
+
const content = await readFile5(plansPath, "utf-8");
|
|
4089
4346
|
plans = JSON.parse(content);
|
|
4090
4347
|
}
|
|
4091
4348
|
const planSlug = planFile.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase().substring(0, 30);
|
|
@@ -4099,7 +4356,7 @@ async function updatePlansRegistry(directory, planFile) {
|
|
|
4099
4356
|
};
|
|
4100
4357
|
}
|
|
4101
4358
|
plans[planSlug].task_count++;
|
|
4102
|
-
await
|
|
4359
|
+
await writeFile5(plansPath, JSON.stringify(plans, null, 2));
|
|
4103
4360
|
return plans[planSlug].task_count;
|
|
4104
4361
|
}
|
|
4105
4362
|
function getClient() {
|
|
@@ -4195,484 +4452,6 @@ ${testResult.result.output}`,
|
|
|
4195
4452
|
}
|
|
4196
4453
|
});
|
|
4197
4454
|
|
|
4198
|
-
// src/tools/set-subagent-model.ts
|
|
4199
|
-
import { readFile as readFile5, writeFile as writeFile5, readdir as readdir3 } from "fs/promises";
|
|
4200
|
-
import { existsSync as existsSync6 } from "fs";
|
|
4201
|
-
import { join as join6 } from "path";
|
|
4202
|
-
import { homedir as homedir2 } from "os";
|
|
4203
|
-
var MANIFOLD_AGENTS = ["clerk", "senior-dev", "junior-dev", "debug"];
|
|
4204
|
-
async function getManifoldAgents(directory) {
|
|
4205
|
-
const agentsDir = join6(directory, ".opencode", "agents");
|
|
4206
|
-
if (!existsSync6(agentsDir)) {
|
|
4207
|
-
return [];
|
|
4208
|
-
}
|
|
4209
|
-
const files = await readdir3(agentsDir);
|
|
4210
|
-
const agents = [];
|
|
4211
|
-
for (const file of files) {
|
|
4212
|
-
if (!file.endsWith(".md"))
|
|
4213
|
-
continue;
|
|
4214
|
-
const name = file.replace(".md", "");
|
|
4215
|
-
if (MANIFOLD_AGENTS.includes(name)) {
|
|
4216
|
-
agents.push(name);
|
|
4217
|
-
}
|
|
4218
|
-
}
|
|
4219
|
-
return agents;
|
|
4220
|
-
}
|
|
4221
|
-
async function getAvailableModels(client) {
|
|
4222
|
-
try {
|
|
4223
|
-
const result = await client.config.providers();
|
|
4224
|
-
const providers = result.data?.providers || [];
|
|
4225
|
-
const models = [];
|
|
4226
|
-
for (const provider of providers) {
|
|
4227
|
-
if (provider.models) {
|
|
4228
|
-
for (const [modelId, model] of Object.entries(provider.models)) {
|
|
4229
|
-
models.push({
|
|
4230
|
-
id: `${provider.id}/${modelId}`,
|
|
4231
|
-
name: model.name || modelId,
|
|
4232
|
-
providerID: provider.id
|
|
4233
|
-
});
|
|
4234
|
-
}
|
|
4235
|
-
}
|
|
4236
|
-
}
|
|
4237
|
-
return models.sort((a, b) => a.name.localeCompare(b.name));
|
|
4238
|
-
} catch (error) {
|
|
4239
|
-
await client.app.log({
|
|
4240
|
-
body: {
|
|
4241
|
-
service: "opencode-manifold",
|
|
4242
|
-
level: "error",
|
|
4243
|
-
message: `Error fetching models: ${error}`
|
|
4244
|
-
}
|
|
4245
|
-
});
|
|
4246
|
-
return [];
|
|
4247
|
-
}
|
|
4248
|
-
}
|
|
4249
|
-
async function readAgentFile(agentName, directory) {
|
|
4250
|
-
const agentPath = join6(directory, ".opencode", "agents", `${agentName}.md`);
|
|
4251
|
-
if (!existsSync6(agentPath)) {
|
|
4252
|
-
const globalPath = join6(homedir2(), ".config", "opencode", "manifold", "agents", `${agentName}.md`);
|
|
4253
|
-
if (existsSync6(globalPath)) {
|
|
4254
|
-
const content2 = await readFile5(globalPath, "utf-8");
|
|
4255
|
-
const { frontmatter: frontmatter2, body: body2 } = parseFrontmatter(content2);
|
|
4256
|
-
return { content: content2, frontmatter: frontmatter2, body: body2 };
|
|
4257
|
-
}
|
|
4258
|
-
throw new Error(`Agent file not found for ${agentName}`);
|
|
4259
|
-
}
|
|
4260
|
-
const content = await readFile5(agentPath, "utf-8");
|
|
4261
|
-
const { frontmatter, body } = parseFrontmatter(content);
|
|
4262
|
-
return { content, frontmatter, body };
|
|
4263
|
-
}
|
|
4264
|
-
function parseFrontmatter(content) {
|
|
4265
|
-
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
4266
|
-
if (!match) {
|
|
4267
|
-
return { frontmatter: {}, body: content };
|
|
4268
|
-
}
|
|
4269
|
-
const frontmatterYaml = match[1];
|
|
4270
|
-
const body = match[2];
|
|
4271
|
-
const frontmatter = jsYaml.load(frontmatterYaml) || {};
|
|
4272
|
-
return { frontmatter, body };
|
|
4273
|
-
}
|
|
4274
|
-
function buildAgentFile(frontmatter, body) {
|
|
4275
|
-
const yamlContent = jsYaml.dump(frontmatter, {
|
|
4276
|
-
lineWidth: -1,
|
|
4277
|
-
noCompatMode: true
|
|
4278
|
-
});
|
|
4279
|
-
return `---
|
|
4280
|
-
${yamlContent}---
|
|
4281
|
-
${body}`;
|
|
4282
|
-
}
|
|
4283
|
-
async function updateAgentModel(client, agentName, modelId, directory) {
|
|
4284
|
-
const { frontmatter, body } = await readAgentFile(agentName, directory);
|
|
4285
|
-
frontmatter.model = modelId;
|
|
4286
|
-
const newContent = buildAgentFile(frontmatter, body);
|
|
4287
|
-
const agentPath = join6(directory, ".opencode", "agents", `${agentName}.md`);
|
|
4288
|
-
await writeFile5(agentPath, newContent);
|
|
4289
|
-
await client.app.log({
|
|
4290
|
-
body: {
|
|
4291
|
-
service: "opencode-manifold",
|
|
4292
|
-
level: "info",
|
|
4293
|
-
message: `Updated ${agentName} model to ${modelId}`
|
|
4294
|
-
}
|
|
4295
|
-
});
|
|
4296
|
-
}
|
|
4297
|
-
async function handleSubModelCommand(client, directory) {
|
|
4298
|
-
await client.app.log({
|
|
4299
|
-
body: {
|
|
4300
|
-
service: "opencode-manifold",
|
|
4301
|
-
level: "info",
|
|
4302
|
-
message: "Starting /sub-model command"
|
|
4303
|
-
}
|
|
4304
|
-
});
|
|
4305
|
-
const agents = await getManifoldAgents(directory);
|
|
4306
|
-
if (agents.length === 0) {
|
|
4307
|
-
await client.tui.showToast({
|
|
4308
|
-
body: {
|
|
4309
|
-
type: "error",
|
|
4310
|
-
message: "No Manifold agents found. Run /manifold-init first."
|
|
4311
|
-
}
|
|
4312
|
-
});
|
|
4313
|
-
return;
|
|
4314
|
-
}
|
|
4315
|
-
const models = await getAvailableModels(client);
|
|
4316
|
-
if (models.length === 0) {
|
|
4317
|
-
await client.tui.showToast({
|
|
4318
|
-
body: {
|
|
4319
|
-
type: "error",
|
|
4320
|
-
message: "No models available. Configure providers first."
|
|
4321
|
-
}
|
|
4322
|
-
});
|
|
4323
|
-
return;
|
|
4324
|
-
}
|
|
4325
|
-
const agentList = agents.map((a) => `${a} (sub-agent)`).join(`
|
|
4326
|
-
`);
|
|
4327
|
-
await client.tui.appendPrompt({
|
|
4328
|
-
body: {
|
|
4329
|
-
type: "text",
|
|
4330
|
-
text: `\uD83D\uDCCB Select a Manifold sub-agent to configure:
|
|
4331
|
-
|
|
4332
|
-
${agentList}`
|
|
4333
|
-
}
|
|
4334
|
-
});
|
|
4335
|
-
await client.tui.publish({
|
|
4336
|
-
body: {
|
|
4337
|
-
type: "tui.prompt_append",
|
|
4338
|
-
data: {
|
|
4339
|
-
type: "text",
|
|
4340
|
-
text: `
|
|
4341
|
-
|
|
4342
|
-
Type the agent name (clerk, senior-dev, junior-dev, or debug): `
|
|
4343
|
-
}
|
|
4344
|
-
}
|
|
4345
|
-
});
|
|
4346
|
-
const agentResponse = await client.tui.control.next();
|
|
4347
|
-
const selectedAgent = agentResponse.data?.input?.trim().toLowerCase();
|
|
4348
|
-
if (!selectedAgent || !MANIFOLD_AGENTS.includes(selectedAgent)) {
|
|
4349
|
-
await client.tui.showToast({
|
|
4350
|
-
body: {
|
|
4351
|
-
type: "error",
|
|
4352
|
-
message: `Invalid agent selection. Must be one of: ${MANIFOLD_AGENTS.join(", ")}`
|
|
4353
|
-
}
|
|
4354
|
-
});
|
|
4355
|
-
return;
|
|
4356
|
-
}
|
|
4357
|
-
if (!agents.includes(selectedAgent)) {
|
|
4358
|
-
await client.tui.showToast({
|
|
4359
|
-
body: {
|
|
4360
|
-
type: "error",
|
|
4361
|
-
message: `Agent ${selectedAgent} not found in .opencode/agents/`
|
|
4362
|
-
}
|
|
4363
|
-
});
|
|
4364
|
-
return;
|
|
4365
|
-
}
|
|
4366
|
-
const currentModel = await readAgentFile(selectedAgent, directory).then((f) => f.frontmatter.model || "not set");
|
|
4367
|
-
const modelList = models.map((m, i2) => `${i2 + 1}. ${m.name} (${m.id})`).join(`
|
|
4368
|
-
`);
|
|
4369
|
-
await client.tui.appendPrompt({
|
|
4370
|
-
body: {
|
|
4371
|
-
type: "text",
|
|
4372
|
-
text: `
|
|
4373
|
-
\uD83D\uDCE6 Available models for ${selectedAgent} (current: ${currentModel}):
|
|
4374
|
-
|
|
4375
|
-
${modelList}`
|
|
4376
|
-
}
|
|
4377
|
-
});
|
|
4378
|
-
await client.tui.publish({
|
|
4379
|
-
body: {
|
|
4380
|
-
type: "tui.prompt_append",
|
|
4381
|
-
data: {
|
|
4382
|
-
type: "text",
|
|
4383
|
-
text: `
|
|
4384
|
-
|
|
4385
|
-
Type the model number or full model ID (e.g., anthropic/claude-sonnet-4-20250514): `
|
|
4386
|
-
}
|
|
4387
|
-
}
|
|
4388
|
-
});
|
|
4389
|
-
const modelResponse = await client.tui.control.next();
|
|
4390
|
-
const selectedModelInput = modelResponse.data?.input?.trim();
|
|
4391
|
-
if (!selectedModelInput) {
|
|
4392
|
-
await client.tui.showToast({
|
|
4393
|
-
body: {
|
|
4394
|
-
type: "error",
|
|
4395
|
-
message: "No model selected"
|
|
4396
|
-
}
|
|
4397
|
-
});
|
|
4398
|
-
return;
|
|
4399
|
-
}
|
|
4400
|
-
let selectedModelId;
|
|
4401
|
-
const modelIndex = parseInt(selectedModelInput, 10) - 1;
|
|
4402
|
-
if (!isNaN(modelIndex) && modelIndex >= 0 && modelIndex < models.length) {
|
|
4403
|
-
selectedModelId = models[modelIndex].id;
|
|
4404
|
-
} else {
|
|
4405
|
-
const found = models.find((m) => m.id.toLowerCase() === selectedModelInput.toLowerCase());
|
|
4406
|
-
if (found) {
|
|
4407
|
-
selectedModelId = found.id;
|
|
4408
|
-
}
|
|
4409
|
-
}
|
|
4410
|
-
if (!selectedModelId) {
|
|
4411
|
-
await client.tui.showToast({
|
|
4412
|
-
body: {
|
|
4413
|
-
type: "error",
|
|
4414
|
-
message: `Invalid model selection: ${selectedModelInput}`
|
|
4415
|
-
}
|
|
4416
|
-
});
|
|
4417
|
-
return;
|
|
4418
|
-
}
|
|
4419
|
-
await updateAgentModel(client, selectedAgent, selectedModelId, directory);
|
|
4420
|
-
await client.tui.showToast({
|
|
4421
|
-
body: {
|
|
4422
|
-
type: "success",
|
|
4423
|
-
message: `✅ Set ${selectedAgent} to ${selectedModelId}`
|
|
4424
|
-
}
|
|
4425
|
-
});
|
|
4426
|
-
await client.tui.appendPrompt({
|
|
4427
|
-
body: {
|
|
4428
|
-
type: "text",
|
|
4429
|
-
text: `
|
|
4430
|
-
✅ Model updated for ${selectedAgent}: ${selectedModelId}
|
|
4431
|
-
`
|
|
4432
|
-
}
|
|
4433
|
-
});
|
|
4434
|
-
}
|
|
4435
|
-
|
|
4436
|
-
// src/tools/clear-cache.ts
|
|
4437
|
-
import { rm } from "fs/promises";
|
|
4438
|
-
import { existsSync as existsSync7 } from "fs";
|
|
4439
|
-
import { join as join7, isAbsolute } from "path";
|
|
4440
|
-
import { homedir as homedir3 } from "os";
|
|
4441
|
-
var PROTECTED_PATHS = [
|
|
4442
|
-
"/",
|
|
4443
|
-
"/System",
|
|
4444
|
-
"/Applications",
|
|
4445
|
-
"/Users",
|
|
4446
|
-
"/etc",
|
|
4447
|
-
"/bin",
|
|
4448
|
-
"/sbin",
|
|
4449
|
-
"/usr"
|
|
4450
|
-
];
|
|
4451
|
-
function resolvePath(pathStr) {
|
|
4452
|
-
const expanded = pathStr.startsWith("~") ? join7(homedir3(), pathStr.slice(1)) : pathStr;
|
|
4453
|
-
return isAbsolute(expanded) ? expanded : join7(process.cwd(), expanded);
|
|
4454
|
-
}
|
|
4455
|
-
function isPathSafe(pathStr) {
|
|
4456
|
-
const normalized = pathStr.toLowerCase();
|
|
4457
|
-
if (normalized.includes("*")) {
|
|
4458
|
-
return { safe: false, reason: "Wildcards are not allowed for safety" };
|
|
4459
|
-
}
|
|
4460
|
-
for (const protectedPath of PROTECTED_PATHS) {
|
|
4461
|
-
if (normalized === protectedPath.toLowerCase() || normalized.startsWith(protectedPath.toLowerCase() + "/") || normalized.startsWith(protectedPath.toLowerCase() + "\\")) {
|
|
4462
|
-
return { safe: false, reason: `Cannot delete protected system path: ${protectedPath}` };
|
|
4463
|
-
}
|
|
4464
|
-
}
|
|
4465
|
-
if (normalized === homedir3().toLowerCase() || normalized.startsWith(homedir3().toLowerCase() + "/") && !normalized.includes("cache") && !normalized.includes("node_modules")) {
|
|
4466
|
-
const parts = normalized.replace(homedir3().toLowerCase(), "").split(/[\/\\]/).filter(Boolean);
|
|
4467
|
-
if (parts.length === 1) {
|
|
4468
|
-
return { safe: false, reason: "Cannot delete directories directly in home folder" };
|
|
4469
|
-
}
|
|
4470
|
-
}
|
|
4471
|
-
return { safe: true };
|
|
4472
|
-
}
|
|
4473
|
-
async function isProjectDirectory(pathStr) {
|
|
4474
|
-
try {
|
|
4475
|
-
const hasGit = existsSync7(join7(pathStr, ".git"));
|
|
4476
|
-
const hasOpencodeConfig = existsSync7(join7(pathStr, "opencode.json"));
|
|
4477
|
-
const hasPackageJson = existsSync7(join7(pathStr, "package.json"));
|
|
4478
|
-
if (hasGit && (hasOpencodeConfig || hasPackageJson)) {
|
|
4479
|
-
return true;
|
|
4480
|
-
}
|
|
4481
|
-
const entries = await Promise.all([
|
|
4482
|
-
existsSync7(join7(pathStr, ".git")) ? "git" : null,
|
|
4483
|
-
existsSync7(join7(pathStr, "opencode.json")) ? "opencode" : null
|
|
4484
|
-
].filter(Boolean));
|
|
4485
|
-
return entries.length >= 2;
|
|
4486
|
-
} catch {
|
|
4487
|
-
return false;
|
|
4488
|
-
}
|
|
4489
|
-
}
|
|
4490
|
-
async function clearCachePaths(paths) {
|
|
4491
|
-
const cleared = [];
|
|
4492
|
-
const skipped = [];
|
|
4493
|
-
const blocked = [];
|
|
4494
|
-
for (const pathStr of paths) {
|
|
4495
|
-
const resolved = resolvePath(pathStr);
|
|
4496
|
-
const safetyCheck = isPathSafe(resolved);
|
|
4497
|
-
if (!safetyCheck.safe) {
|
|
4498
|
-
blocked.push({ path: resolved, reason: safetyCheck.reason || "Failed safety check" });
|
|
4499
|
-
continue;
|
|
4500
|
-
}
|
|
4501
|
-
if (!existsSync7(resolved)) {
|
|
4502
|
-
skipped.push(resolved);
|
|
4503
|
-
continue;
|
|
4504
|
-
}
|
|
4505
|
-
const isProject = await isProjectDirectory(resolved);
|
|
4506
|
-
if (isProject) {
|
|
4507
|
-
blocked.push({ path: resolved, reason: "Appears to be a project directory (contains .git + config files)" });
|
|
4508
|
-
continue;
|
|
4509
|
-
}
|
|
4510
|
-
try {
|
|
4511
|
-
await rm(resolved, { recursive: true, force: true });
|
|
4512
|
-
cleared.push(resolved);
|
|
4513
|
-
} catch (error) {
|
|
4514
|
-
blocked.push({ path: resolved, reason: `Failed to delete: ${error}` });
|
|
4515
|
-
}
|
|
4516
|
-
}
|
|
4517
|
-
return { cleared, skipped, blocked };
|
|
4518
|
-
}
|
|
4519
|
-
async function handleUpdateCommand(client, directory) {
|
|
4520
|
-
await client.app.log({
|
|
4521
|
-
body: {
|
|
4522
|
-
service: "opencode-manifold",
|
|
4523
|
-
level: "info",
|
|
4524
|
-
message: "Starting /manifold-update command"
|
|
4525
|
-
}
|
|
4526
|
-
});
|
|
4527
|
-
const settings = await readSettings(directory);
|
|
4528
|
-
const configuredPaths = settings.updateCachePaths || [];
|
|
4529
|
-
if (configuredPaths.length === 0) {
|
|
4530
|
-
const examplePaths = [
|
|
4531
|
-
"~/.cache/opencode/packages/opencode-manifold@latest",
|
|
4532
|
-
"~/node_modules/opencode-manifold"
|
|
4533
|
-
];
|
|
4534
|
-
await client.tui.appendPrompt({
|
|
4535
|
-
body: {
|
|
4536
|
-
type: "text",
|
|
4537
|
-
text: `\uD83D\uDD04 **Manifold Plugin Update**
|
|
4538
|
-
|
|
4539
|
-
No cache paths configured in \`Manifold/settings.json\`.
|
|
4540
|
-
|
|
4541
|
-
To enable this feature, add cache paths to your settings:
|
|
4542
|
-
|
|
4543
|
-
\`\`\`json
|
|
4544
|
-
{
|
|
4545
|
-
"updateCachePaths": [
|
|
4546
|
-
"~/.cache/opencode/packages/opencode-manifold@latest",
|
|
4547
|
-
"~/node_modules/opencode-manifold"
|
|
4548
|
-
]
|
|
4549
|
-
}
|
|
4550
|
-
\`\`\`
|
|
4551
|
-
|
|
4552
|
-
**Safety features:**
|
|
4553
|
-
• Wildcards are blocked
|
|
4554
|
-
• System directories are protected
|
|
4555
|
-
• Project directories (with .git + config) are protected
|
|
4556
|
-
|
|
4557
|
-
**macOS users:** The paths above are typical cache locations.
|
|
4558
|
-
**Linux/Windows users:** Add your opencode cache paths manually.
|
|
4559
|
-
`
|
|
4560
|
-
}
|
|
4561
|
-
});
|
|
4562
|
-
await client.tui.showToast({
|
|
4563
|
-
body: {
|
|
4564
|
-
type: "info",
|
|
4565
|
-
message: "Configure updateCachePaths in Manifold/settings.json"
|
|
4566
|
-
}
|
|
4567
|
-
});
|
|
4568
|
-
return;
|
|
4569
|
-
}
|
|
4570
|
-
const resolvedPaths = configuredPaths.map(resolvePath);
|
|
4571
|
-
await client.tui.appendPrompt({
|
|
4572
|
-
body: {
|
|
4573
|
-
type: "text",
|
|
4574
|
-
text: `\uD83D\uDD04 **Manifold Plugin Update**
|
|
4575
|
-
|
|
4576
|
-
The following paths will be cleared:
|
|
4577
|
-
|
|
4578
|
-
${resolvedPaths.map((p) => `• ${p}`).join(`
|
|
4579
|
-
`)}
|
|
4580
|
-
|
|
4581
|
-
⚠️ **This action is irreversible.** These paths are read from \`Manifold/settings.json\`.
|
|
4582
|
-
`
|
|
4583
|
-
}
|
|
4584
|
-
});
|
|
4585
|
-
await client.tui.publish({
|
|
4586
|
-
body: {
|
|
4587
|
-
type: "tui.prompt_append",
|
|
4588
|
-
data: {
|
|
4589
|
-
type: "text",
|
|
4590
|
-
text: `
|
|
4591
|
-
|
|
4592
|
-
Type 'yes' to confirm and clear cache: `
|
|
4593
|
-
}
|
|
4594
|
-
}
|
|
4595
|
-
});
|
|
4596
|
-
const response = await client.tui.control.next();
|
|
4597
|
-
const answer = response.data?.input?.trim().toLowerCase();
|
|
4598
|
-
if (answer !== "yes" && answer !== "y") {
|
|
4599
|
-
await client.tui.appendPrompt({
|
|
4600
|
-
body: {
|
|
4601
|
-
type: "text",
|
|
4602
|
-
text: `
|
|
4603
|
-
❌ Cancelled. No cache cleared.
|
|
4604
|
-
`
|
|
4605
|
-
}
|
|
4606
|
-
});
|
|
4607
|
-
return;
|
|
4608
|
-
}
|
|
4609
|
-
const { cleared, skipped, blocked } = await clearCachePaths(configuredPaths);
|
|
4610
|
-
let message = `
|
|
4611
|
-
`;
|
|
4612
|
-
if (cleared.length > 0) {
|
|
4613
|
-
message += `✅ **Cleared:**
|
|
4614
|
-
${cleared.map((p) => `• ${p}`).join(`
|
|
4615
|
-
`)}
|
|
4616
|
-
|
|
4617
|
-
`;
|
|
4618
|
-
}
|
|
4619
|
-
if (skipped.length > 0) {
|
|
4620
|
-
message += `⏭️ **Already clean / skipped:**
|
|
4621
|
-
${skipped.map((p) => `• ${p}`).join(`
|
|
4622
|
-
`)}
|
|
4623
|
-
|
|
4624
|
-
`;
|
|
4625
|
-
}
|
|
4626
|
-
if (blocked.length > 0) {
|
|
4627
|
-
message += `\uD83D\uDEAB **Blocked (safety check):**
|
|
4628
|
-
`;
|
|
4629
|
-
for (const item of blocked) {
|
|
4630
|
-
message += `• ${item.path}
|
|
4631
|
-
Reason: ${item.reason}
|
|
4632
|
-
`;
|
|
4633
|
-
}
|
|
4634
|
-
message += `
|
|
4635
|
-
`;
|
|
4636
|
-
}
|
|
4637
|
-
if (cleared.length > 0) {
|
|
4638
|
-
message += `**Next step:** Restart opencode to pull the latest plugin version.
|
|
4639
|
-
|
|
4640
|
-
Your project files (.opencode/, Manifold/) are untouched — only the configured cache paths were cleared.
|
|
4641
|
-
`;
|
|
4642
|
-
} else if (blocked.length > 0) {
|
|
4643
|
-
message += `⚠️ No paths were cleared. Review the blocked paths above and adjust your settings if needed.
|
|
4644
|
-
`;
|
|
4645
|
-
}
|
|
4646
|
-
await client.tui.appendPrompt({
|
|
4647
|
-
body: {
|
|
4648
|
-
type: "text",
|
|
4649
|
-
text: message
|
|
4650
|
-
}
|
|
4651
|
-
});
|
|
4652
|
-
if (cleared.length > 0) {
|
|
4653
|
-
await client.tui.showToast({
|
|
4654
|
-
body: {
|
|
4655
|
-
type: "success",
|
|
4656
|
-
message: `Cache cleared (${cleared.length} paths). Restart opencode to update.`
|
|
4657
|
-
}
|
|
4658
|
-
});
|
|
4659
|
-
} else if (blocked.length > 0) {
|
|
4660
|
-
await client.tui.showToast({
|
|
4661
|
-
body: {
|
|
4662
|
-
type: "error",
|
|
4663
|
-
message: `Cache update blocked (${blocked.length} paths failed safety check)`
|
|
4664
|
-
}
|
|
4665
|
-
});
|
|
4666
|
-
}
|
|
4667
|
-
await client.app.log({
|
|
4668
|
-
body: {
|
|
4669
|
-
service: "opencode-manifold",
|
|
4670
|
-
level: "info",
|
|
4671
|
-
message: `/manifold-update complete: cleared ${cleared.length}, skipped ${skipped.length}, blocked ${blocked.length}`
|
|
4672
|
-
}
|
|
4673
|
-
});
|
|
4674
|
-
}
|
|
4675
|
-
|
|
4676
4455
|
// src/index.ts
|
|
4677
4456
|
var ManifoldPlugin = async (ctx) => {
|
|
4678
4457
|
setPluginContext(ctx.client);
|
|
@@ -4706,27 +4485,12 @@ var ManifoldPlugin = async (ctx) => {
|
|
|
4706
4485
|
}
|
|
4707
4486
|
];
|
|
4708
4487
|
}
|
|
4709
|
-
} else if (input.command === "manifold-models") {
|
|
4710
|
-
await handleSubModelCommand(ctx.client, ctx.directory);
|
|
4711
|
-
output.parts = [
|
|
4712
|
-
{
|
|
4713
|
-
type: "text",
|
|
4714
|
-
text: "Sub-agent model configuration complete."
|
|
4715
|
-
}
|
|
4716
|
-
];
|
|
4717
|
-
} else if (input.command === "manifold-update") {
|
|
4718
|
-
await handleUpdateCommand(ctx.client, ctx.directory);
|
|
4719
|
-
output.parts = [
|
|
4720
|
-
{
|
|
4721
|
-
type: "text",
|
|
4722
|
-
text: "Plugin cache update complete."
|
|
4723
|
-
}
|
|
4724
|
-
];
|
|
4725
4488
|
}
|
|
4726
4489
|
}
|
|
4727
4490
|
};
|
|
4728
4491
|
};
|
|
4729
4492
|
var server = ManifoldPlugin;
|
|
4730
4493
|
export {
|
|
4494
|
+
tui,
|
|
4731
4495
|
server
|
|
4732
4496
|
};
|