oh-my-opencode-slim 1.0.2 → 1.0.3
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 -2
- package/dist/cli/index.js +3 -1
- package/dist/config/schema.d.ts +4 -0
- package/dist/hooks/task-session-manager/index.d.ts +4 -0
- package/dist/index.js +320 -100
- package/dist/utils/agent-variant.d.ts +2 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/session-manager.d.ts +18 -1
- package/oh-my-opencode-slim.schema.json +12 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
<a href="https://boringdystopia.ai/"><img src="https://img.shields.io/badge/boringdystopia.ai-111111?style=for-the-badge&logo=vercel&logoColor=white" alt="boringdystopia.ai"></a>
|
|
9
9
|
<a href="https://x.com/alvinunreal"><img src="https://img.shields.io/badge/X-@alvinunreal-000000?style=for-the-badge&logo=x&logoColor=white" alt="X @alvinunreal"></a>
|
|
10
10
|
<a href="https://t.me/boringdystopiadevelopment"><img src="https://img.shields.io/badge/Telegram-Join%20channel-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white" alt="Telegram Join channel"></a>
|
|
11
|
-
<a href="https://deepwiki.com/alvinunreal/oh-my-opencode-slim"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
|
12
11
|
</p>
|
|
13
12
|
</div>
|
|
14
13
|
|
|
@@ -69,7 +68,7 @@ The default generated configuration looks like this:
|
|
|
69
68
|
"preset": "openai",
|
|
70
69
|
"presets": {
|
|
71
70
|
"openai": {
|
|
72
|
-
"orchestrator": { "model": "openai/gpt-5.5", "
|
|
71
|
+
"orchestrator": { "model": "openai/gpt-5.5", "skills": ["*"], "mcps": ["*", "!context7"] },
|
|
73
72
|
"oracle": { "model": "openai/gpt-5.5", "variant": "high", "skills": ["simplify"], "mcps": [] },
|
|
74
73
|
"librarian": { "model": "openai/gpt-5.4-mini", "variant": "low", "skills": [], "mcps": ["websearch", "context7", "grep_app"] },
|
|
75
74
|
"explorer": { "model": "openai/gpt-5.4-mini", "variant": "low", "skills": [], "mcps": [] },
|
package/dist/cli/index.js
CHANGED
|
@@ -260,7 +260,9 @@ var InterviewConfigSchema = z2.object({
|
|
|
260
260
|
dashboard: z2.boolean().default(false)
|
|
261
261
|
});
|
|
262
262
|
var SessionManagerConfigSchema = z2.object({
|
|
263
|
-
maxSessionsPerAgent: z2.number().int().min(1).max(10).default(2)
|
|
263
|
+
maxSessionsPerAgent: z2.number().int().min(1).max(10).default(2),
|
|
264
|
+
readContextMinLines: z2.number().int().min(0).max(1000).default(10),
|
|
265
|
+
readContextMaxFiles: z2.number().int().min(0).max(50).default(8)
|
|
264
266
|
});
|
|
265
267
|
var TodoContinuationConfigSchema = z2.object({
|
|
266
268
|
maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -160,6 +160,8 @@ export declare const InterviewConfigSchema: z.ZodObject<{
|
|
|
160
160
|
export type InterviewConfig = z.infer<typeof InterviewConfigSchema>;
|
|
161
161
|
export declare const SessionManagerConfigSchema: z.ZodObject<{
|
|
162
162
|
maxSessionsPerAgent: z.ZodDefault<z.ZodNumber>;
|
|
163
|
+
readContextMinLines: z.ZodDefault<z.ZodNumber>;
|
|
164
|
+
readContextMaxFiles: z.ZodDefault<z.ZodNumber>;
|
|
163
165
|
}, z.core.$strip>;
|
|
164
166
|
export type SessionManagerConfig = z.infer<typeof SessionManagerConfigSchema>;
|
|
165
167
|
export declare const TodoContinuationConfigSchema: z.ZodObject<{
|
|
@@ -305,6 +307,8 @@ export declare const PluginConfigSchema: z.ZodObject<{
|
|
|
305
307
|
}, z.core.$strip>>;
|
|
306
308
|
sessionManager: z.ZodOptional<z.ZodObject<{
|
|
307
309
|
maxSessionsPerAgent: z.ZodDefault<z.ZodNumber>;
|
|
310
|
+
readContextMinLines: z.ZodDefault<z.ZodNumber>;
|
|
311
|
+
readContextMaxFiles: z.ZodDefault<z.ZodNumber>;
|
|
308
312
|
}, z.core.$strip>>;
|
|
309
313
|
todoContinuation: z.ZodOptional<z.ZodObject<{
|
|
310
314
|
maxContinuations: z.ZodDefault<z.ZodNumber>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { PluginInput } from '@opencode-ai/plugin';
|
|
2
2
|
export declare function createTaskSessionManagerHook(_ctx: PluginInput, options: {
|
|
3
3
|
maxSessionsPerAgent: number;
|
|
4
|
+
readContextMinLines?: number;
|
|
5
|
+
readContextMaxFiles?: number;
|
|
4
6
|
shouldManageSession: (sessionID: string) => boolean;
|
|
5
7
|
}): {
|
|
6
8
|
'tool.execute.before': (input: {
|
|
@@ -16,6 +18,7 @@ export declare function createTaskSessionManagerHook(_ctx: PluginInput, options:
|
|
|
16
18
|
callID?: string;
|
|
17
19
|
}, output: {
|
|
18
20
|
output: unknown;
|
|
21
|
+
metadata?: unknown;
|
|
19
22
|
}) => Promise<void>;
|
|
20
23
|
'experimental.chat.system.transform': (input: {
|
|
21
24
|
sessionID?: string;
|
|
@@ -28,6 +31,7 @@ export declare function createTaskSessionManagerHook(_ctx: PluginInput, options:
|
|
|
28
31
|
properties?: {
|
|
29
32
|
info?: {
|
|
30
33
|
id?: string;
|
|
34
|
+
parentID?: string;
|
|
31
35
|
};
|
|
32
36
|
sessionID?: string;
|
|
33
37
|
};
|
package/dist/index.js
CHANGED
|
@@ -6246,33 +6246,33 @@ var require_URL = __commonJS((exports, module) => {
|
|
|
6246
6246
|
else
|
|
6247
6247
|
return basepath.substring(0, lastslash + 1) + refpath;
|
|
6248
6248
|
}
|
|
6249
|
-
function remove_dot_segments(
|
|
6250
|
-
if (!
|
|
6251
|
-
return
|
|
6249
|
+
function remove_dot_segments(path14) {
|
|
6250
|
+
if (!path14)
|
|
6251
|
+
return path14;
|
|
6252
6252
|
var output = "";
|
|
6253
|
-
while (
|
|
6254
|
-
if (
|
|
6255
|
-
|
|
6253
|
+
while (path14.length > 0) {
|
|
6254
|
+
if (path14 === "." || path14 === "..") {
|
|
6255
|
+
path14 = "";
|
|
6256
6256
|
break;
|
|
6257
6257
|
}
|
|
6258
|
-
var twochars =
|
|
6259
|
-
var threechars =
|
|
6260
|
-
var fourchars =
|
|
6258
|
+
var twochars = path14.substring(0, 2);
|
|
6259
|
+
var threechars = path14.substring(0, 3);
|
|
6260
|
+
var fourchars = path14.substring(0, 4);
|
|
6261
6261
|
if (threechars === "../") {
|
|
6262
|
-
|
|
6262
|
+
path14 = path14.substring(3);
|
|
6263
6263
|
} else if (twochars === "./") {
|
|
6264
|
-
|
|
6264
|
+
path14 = path14.substring(2);
|
|
6265
6265
|
} else if (threechars === "/./") {
|
|
6266
|
-
|
|
6267
|
-
} else if (twochars === "/." &&
|
|
6268
|
-
|
|
6269
|
-
} else if (fourchars === "/../" || threechars === "/.." &&
|
|
6270
|
-
|
|
6266
|
+
path14 = "/" + path14.substring(3);
|
|
6267
|
+
} else if (twochars === "/." && path14.length === 2) {
|
|
6268
|
+
path14 = "/";
|
|
6269
|
+
} else if (fourchars === "/../" || threechars === "/.." && path14.length === 3) {
|
|
6270
|
+
path14 = "/" + path14.substring(4);
|
|
6271
6271
|
output = output.replace(/\/?[^\/]*$/, "");
|
|
6272
6272
|
} else {
|
|
6273
|
-
var segment =
|
|
6273
|
+
var segment = path14.match(/(\/?([^\/]*))/)[0];
|
|
6274
6274
|
output += segment;
|
|
6275
|
-
|
|
6275
|
+
path14 = path14.substring(segment.length);
|
|
6276
6276
|
}
|
|
6277
6277
|
}
|
|
6278
6278
|
return output;
|
|
@@ -18526,7 +18526,9 @@ var InterviewConfigSchema = z2.object({
|
|
|
18526
18526
|
dashboard: z2.boolean().default(false)
|
|
18527
18527
|
});
|
|
18528
18528
|
var SessionManagerConfigSchema = z2.object({
|
|
18529
|
-
maxSessionsPerAgent: z2.number().int().min(1).max(10).default(2)
|
|
18529
|
+
maxSessionsPerAgent: z2.number().int().min(1).max(10).default(2),
|
|
18530
|
+
readContextMinLines: z2.number().int().min(0).max(1000).default(10),
|
|
18531
|
+
readContextMaxFiles: z2.number().int().min(0).max(50).default(8)
|
|
18530
18532
|
});
|
|
18531
18533
|
var TodoContinuationConfigSchema = z2.object({
|
|
18532
18534
|
maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
|
|
@@ -19769,12 +19771,14 @@ function getDisabledAgents(config) {
|
|
|
19769
19771
|
|
|
19770
19772
|
// src/utils/logger.ts
|
|
19771
19773
|
import * as fs2 from "node:fs";
|
|
19774
|
+
import { appendFile } from "node:fs/promises";
|
|
19772
19775
|
import * as os from "node:os";
|
|
19773
19776
|
import * as path2 from "node:path";
|
|
19774
19777
|
var LOG_PREFIX = "oh-my-opencode-slim.";
|
|
19775
19778
|
var LOG_SUFFIX = ".log";
|
|
19776
19779
|
var RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
19777
19780
|
var logFile = null;
|
|
19781
|
+
var writeChain = Promise.resolve();
|
|
19778
19782
|
function getLogDir() {
|
|
19779
19783
|
return process.env.OPENCODE_LOG_DIR ?? path2.join(os.homedir(), ".local/share/opencode");
|
|
19780
19784
|
}
|
|
@@ -19817,10 +19821,14 @@ function initLogger(sessionId) {
|
|
|
19817
19821
|
fs2.mkdirSync(dir, { recursive: true });
|
|
19818
19822
|
} catch {}
|
|
19819
19823
|
logFile = path2.join(dir, `${LOG_PREFIX}${sessionId}${LOG_SUFFIX}`);
|
|
19824
|
+
try {
|
|
19825
|
+
fs2.closeSync(fs2.openSync(logFile, "a"));
|
|
19826
|
+
} catch {}
|
|
19820
19827
|
cleanupOldLogs(dir);
|
|
19821
19828
|
}
|
|
19822
19829
|
function log(message, data) {
|
|
19823
|
-
|
|
19830
|
+
const target = logFile;
|
|
19831
|
+
if (!target)
|
|
19824
19832
|
return;
|
|
19825
19833
|
try {
|
|
19826
19834
|
const timestamp = new Date().toISOString();
|
|
@@ -19834,7 +19842,7 @@ function log(message, data) {
|
|
|
19834
19842
|
}
|
|
19835
19843
|
const logEntry = `[${timestamp}] ${message} ${dataStr}
|
|
19836
19844
|
`;
|
|
19837
|
-
|
|
19845
|
+
writeChain = writeChain.then(() => appendFile(target, logEntry)).catch(() => {});
|
|
19838
19846
|
} catch {}
|
|
19839
19847
|
}
|
|
19840
19848
|
|
|
@@ -20020,7 +20028,7 @@ class CouncilManager {
|
|
|
20020
20028
|
}
|
|
20021
20029
|
} else {
|
|
20022
20030
|
const promises = entries.map(([name, config], index) => (async () => {
|
|
20023
|
-
if (index > 0) {
|
|
20031
|
+
if (this.tmuxEnabled && index > 0) {
|
|
20024
20032
|
await new Promise((r) => setTimeout(r, index * COUNCILLOR_STAGGER_MS));
|
|
20025
20033
|
}
|
|
20026
20034
|
return this.runCouncillorWithRetry(name, config, prompt, parentSessionId, timeout, maxRetries);
|
|
@@ -22036,11 +22044,8 @@ function resolveRuntimeAgentName(config, agentName) {
|
|
|
22036
22044
|
function escapeRegExp2(value) {
|
|
22037
22045
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22038
22046
|
}
|
|
22039
|
-
function
|
|
22040
|
-
|
|
22041
|
-
return text;
|
|
22042
|
-
}
|
|
22043
|
-
let rewritten = text;
|
|
22047
|
+
function createDisplayNameMentionRewriter(config) {
|
|
22048
|
+
const replacements = [];
|
|
22044
22049
|
for (const internalName of getRuntimeAgentNames(config)) {
|
|
22045
22050
|
const displayName = getAgentOverride(config, internalName)?.displayName;
|
|
22046
22051
|
if (!displayName) {
|
|
@@ -22050,9 +22055,24 @@ function rewriteDisplayNameMentions(config, text) {
|
|
|
22050
22055
|
if (!normalizedDisplayName || normalizedDisplayName === internalName) {
|
|
22051
22056
|
continue;
|
|
22052
22057
|
}
|
|
22053
|
-
|
|
22058
|
+
replacements.push({
|
|
22059
|
+
regex: new RegExp(`(^|[^\\w.])@${escapeRegExp2(normalizedDisplayName)}\\b`, "g"),
|
|
22060
|
+
internalName
|
|
22061
|
+
});
|
|
22062
|
+
}
|
|
22063
|
+
if (replacements.length === 0) {
|
|
22064
|
+
return (text) => text;
|
|
22054
22065
|
}
|
|
22055
|
-
return
|
|
22066
|
+
return (text) => {
|
|
22067
|
+
if (!text.includes("@")) {
|
|
22068
|
+
return text;
|
|
22069
|
+
}
|
|
22070
|
+
let rewritten = text;
|
|
22071
|
+
for (const replacement of replacements) {
|
|
22072
|
+
rewritten = rewritten.replace(replacement.regex, `$1@${replacement.internalName}`);
|
|
22073
|
+
}
|
|
22074
|
+
return rewritten;
|
|
22075
|
+
};
|
|
22056
22076
|
}
|
|
22057
22077
|
// src/utils/internal-initiator.ts
|
|
22058
22078
|
var SLIM_INTERNAL_INITIATOR_MARKER = "<!-- SLIM_INTERNAL_INITIATOR -->";
|
|
@@ -22076,6 +22096,8 @@ function hasInternalInitiatorMarker(part) {
|
|
|
22076
22096
|
return part.text.includes(SLIM_INTERNAL_INITIATOR_MARKER);
|
|
22077
22097
|
}
|
|
22078
22098
|
// src/utils/session-manager.ts
|
|
22099
|
+
var MIN_CONTEXT_FILE_LINES = 10;
|
|
22100
|
+
var MAX_CONTEXT_FILES_PER_SESSION = 8;
|
|
22079
22101
|
function aliasPrefix(agentType) {
|
|
22080
22102
|
switch (agentType) {
|
|
22081
22103
|
case "explorer":
|
|
@@ -22115,11 +22137,15 @@ function deriveTaskSessionLabel(input) {
|
|
|
22115
22137
|
|
|
22116
22138
|
class SessionManager {
|
|
22117
22139
|
maxSessionsPerAgent;
|
|
22140
|
+
readContextMinLines;
|
|
22141
|
+
readContextMaxFiles;
|
|
22118
22142
|
sessionsByParent = new Map;
|
|
22119
22143
|
nextAliasIndexByParent = new Map;
|
|
22120
22144
|
orderCounter = 0;
|
|
22121
|
-
constructor(maxSessionsPerAgent) {
|
|
22145
|
+
constructor(maxSessionsPerAgent, options = {}) {
|
|
22122
22146
|
this.maxSessionsPerAgent = maxSessionsPerAgent;
|
|
22147
|
+
this.readContextMinLines = options.readContextMinLines ?? MIN_CONTEXT_FILE_LINES;
|
|
22148
|
+
this.readContextMaxFiles = options.readContextMaxFiles ?? MAX_CONTEXT_FILES_PER_SESSION;
|
|
22123
22149
|
}
|
|
22124
22150
|
remember(input) {
|
|
22125
22151
|
const now = this.nextOrder();
|
|
@@ -22138,6 +22164,7 @@ class SessionManager {
|
|
|
22138
22164
|
taskId: input.taskId,
|
|
22139
22165
|
agentType: input.agentType,
|
|
22140
22166
|
label: input.label,
|
|
22167
|
+
contextFiles: [],
|
|
22141
22168
|
createdAt: now,
|
|
22142
22169
|
lastUsedAt: now
|
|
22143
22170
|
};
|
|
@@ -22171,6 +22198,39 @@ class SessionManager {
|
|
|
22171
22198
|
}
|
|
22172
22199
|
}
|
|
22173
22200
|
}
|
|
22201
|
+
taskIds() {
|
|
22202
|
+
const ids = new Set;
|
|
22203
|
+
for (const groups of this.sessionsByParent.values()) {
|
|
22204
|
+
for (const group of groups.values()) {
|
|
22205
|
+
for (const entry of group) {
|
|
22206
|
+
ids.add(entry.taskId);
|
|
22207
|
+
}
|
|
22208
|
+
}
|
|
22209
|
+
}
|
|
22210
|
+
return ids;
|
|
22211
|
+
}
|
|
22212
|
+
addContext(taskId, files) {
|
|
22213
|
+
if (files.length === 0)
|
|
22214
|
+
return;
|
|
22215
|
+
for (const groups of this.sessionsByParent.values()) {
|
|
22216
|
+
for (const group of groups.values()) {
|
|
22217
|
+
const match = group.find((entry) => entry.taskId === taskId);
|
|
22218
|
+
if (!match)
|
|
22219
|
+
continue;
|
|
22220
|
+
const existing = new Map(match.contextFiles.map((file) => [file.path, file]));
|
|
22221
|
+
for (const file of files) {
|
|
22222
|
+
const previous = existing.get(file.path);
|
|
22223
|
+
if (previous) {
|
|
22224
|
+
previous.lineCount = Math.max(previous.lineCount, file.lineCount);
|
|
22225
|
+
previous.lastReadAt = Math.max(previous.lastReadAt, file.lastReadAt);
|
|
22226
|
+
continue;
|
|
22227
|
+
}
|
|
22228
|
+
match.contextFiles.push({ ...file });
|
|
22229
|
+
}
|
|
22230
|
+
this.trimContextFiles(match);
|
|
22231
|
+
}
|
|
22232
|
+
}
|
|
22233
|
+
}
|
|
22174
22234
|
clearParent(parentSessionId) {
|
|
22175
22235
|
this.sessionsByParent.delete(parentSessionId);
|
|
22176
22236
|
this.nextAliasIndexByParent.delete(parentSessionId);
|
|
@@ -22182,7 +22242,17 @@ class SessionManager {
|
|
|
22182
22242
|
const lines = [...groups.entries()].map(([agentType, entries]) => [
|
|
22183
22243
|
agentType,
|
|
22184
22244
|
[...entries].sort((a, b) => b.lastUsedAt - a.lastUsedAt)
|
|
22185
|
-
]).filter(([, entries]) => entries.length > 0).sort((a, b) => b[1][0].lastUsedAt - a[1][0].lastUsedAt).map(([agentType, entries]) =>
|
|
22245
|
+
]).filter(([, entries]) => entries.length > 0).sort((a, b) => b[1][0].lastUsedAt - a[1][0].lastUsedAt).map(([agentType, entries]) => [
|
|
22246
|
+
`- ${agentType}: ${entries.map((entry) => `${entry.alias} ${entry.label}`).join("; ")}`,
|
|
22247
|
+
...entries.map((entry) => [
|
|
22248
|
+
entry,
|
|
22249
|
+
formatContextFiles(entry.contextFiles, {
|
|
22250
|
+
minLines: this.readContextMinLines,
|
|
22251
|
+
maxFiles: this.readContextMaxFiles
|
|
22252
|
+
})
|
|
22253
|
+
]).filter(([, context]) => context.length > 0).map(([entry, context]) => ` Context read by ${entry.alias}: ${context}`)
|
|
22254
|
+
].join(`
|
|
22255
|
+
`));
|
|
22186
22256
|
if (lines.length === 0)
|
|
22187
22257
|
return;
|
|
22188
22258
|
return [
|
|
@@ -22236,11 +22306,25 @@ class SessionManager {
|
|
|
22236
22306
|
group.length = this.maxSessionsPerAgent;
|
|
22237
22307
|
}
|
|
22238
22308
|
}
|
|
22309
|
+
trimContextFiles(entry) {
|
|
22310
|
+
if (this.readContextMaxFiles === 0) {
|
|
22311
|
+
entry.contextFiles = [];
|
|
22312
|
+
return;
|
|
22313
|
+
}
|
|
22314
|
+
entry.contextFiles = entry.contextFiles.filter((file) => file.lineCount >= this.readContextMinLines).sort((a, b) => b.lastReadAt - a.lastReadAt).slice(0, this.readContextMaxFiles + 1);
|
|
22315
|
+
}
|
|
22239
22316
|
nextOrder() {
|
|
22240
22317
|
this.orderCounter += 1;
|
|
22241
22318
|
return this.orderCounter;
|
|
22242
22319
|
}
|
|
22243
22320
|
}
|
|
22321
|
+
function formatContextFiles(files, options) {
|
|
22322
|
+
const eligible = files.filter((file) => file.lineCount >= options.minLines).sort((a, b) => b.lastReadAt - a.lastReadAt);
|
|
22323
|
+
const shown = eligible.slice(0, options.maxFiles);
|
|
22324
|
+
const rest = eligible.length - shown.length;
|
|
22325
|
+
const rendered = shown.map((file) => `${file.path} (${file.lineCount} lines)`);
|
|
22326
|
+
return `${rendered.join(", ")}${rest > 0 ? ` (+${rest} more)` : ""}`;
|
|
22327
|
+
}
|
|
22244
22328
|
// src/utils/task.ts
|
|
22245
22329
|
function parseTaskIdFromTaskOutput(output) {
|
|
22246
22330
|
const lines = output.split(/\r?\n/);
|
|
@@ -22542,6 +22626,17 @@ ${allowedEntries.map((entry) => entry.block).join(`
|
|
|
22542
22626
|
});
|
|
22543
22627
|
}
|
|
22544
22628
|
function createFilterAvailableSkillsHook(_ctx, config) {
|
|
22629
|
+
const permissionRulesByAgent = new Map;
|
|
22630
|
+
const getPermissionRules = (agentName) => {
|
|
22631
|
+
const cached = permissionRulesByAgent.get(agentName);
|
|
22632
|
+
if (cached) {
|
|
22633
|
+
return cached;
|
|
22634
|
+
}
|
|
22635
|
+
const configuredSkills = getAgentOverride(config, agentName)?.skills;
|
|
22636
|
+
const permissionRules = getSkillPermissionsForAgent(agentName, configuredSkills);
|
|
22637
|
+
permissionRulesByAgent.set(agentName, permissionRules);
|
|
22638
|
+
return permissionRules;
|
|
22639
|
+
};
|
|
22545
22640
|
return {
|
|
22546
22641
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
22547
22642
|
const { messages } = output;
|
|
@@ -22549,8 +22644,7 @@ function createFilterAvailableSkillsHook(_ctx, config) {
|
|
|
22549
22644
|
return;
|
|
22550
22645
|
}
|
|
22551
22646
|
const agentName = getCurrentAgent(messages);
|
|
22552
|
-
const
|
|
22553
|
-
const permissionRules = getSkillPermissionsForAgent(agentName, configuredSkills);
|
|
22647
|
+
const permissionRules = getPermissionRules(agentName);
|
|
22554
22648
|
for (const message of messages) {
|
|
22555
22649
|
for (const part of message.parts) {
|
|
22556
22650
|
if (part.type !== "text" || !part.text || !part.text.includes("<available_skills>")) {
|
|
@@ -22903,7 +22997,21 @@ function processImageAttachments(args) {
|
|
|
22903
22997
|
const observerEnabled = !disabledAgents.has("observer");
|
|
22904
22998
|
if (!observerEnabled)
|
|
22905
22999
|
return;
|
|
23000
|
+
const messagesWithImages = [];
|
|
23001
|
+
for (const msg of messages) {
|
|
23002
|
+
if (msg.info.role !== "user")
|
|
23003
|
+
continue;
|
|
23004
|
+
const imageParts = msg.parts.filter(isImagePart);
|
|
23005
|
+
if (imageParts.length > 0) {
|
|
23006
|
+
messagesWithImages.push({ msg, imageParts });
|
|
23007
|
+
}
|
|
23008
|
+
}
|
|
22906
23009
|
const saveDir = join7(workDir, ".opencode", "images");
|
|
23010
|
+
if (messagesWithImages.length === 0) {
|
|
23011
|
+
if (existsSync4(saveDir))
|
|
23012
|
+
cleanupAllSessions(saveDir);
|
|
23013
|
+
return;
|
|
23014
|
+
}
|
|
22907
23015
|
const gitignorePath = join7(workDir, ".opencode", ".gitignore");
|
|
22908
23016
|
try {
|
|
22909
23017
|
mkdirSync2(saveDir, { recursive: true });
|
|
@@ -22914,12 +23022,7 @@ function processImageAttachments(args) {
|
|
|
22914
23022
|
log2(`[image-hook] failed to create image directory: ${e}`);
|
|
22915
23023
|
}
|
|
22916
23024
|
cleanupAllSessions(saveDir);
|
|
22917
|
-
for (const msg of
|
|
22918
|
-
if (msg.info.role !== "user")
|
|
22919
|
-
continue;
|
|
22920
|
-
const imageParts = msg.parts.filter(isImagePart);
|
|
22921
|
-
if (imageParts.length === 0)
|
|
22922
|
-
continue;
|
|
23025
|
+
for (const { msg, imageParts } of messagesWithImages) {
|
|
22923
23026
|
const sessionSubdir = msg.info.sessionID ? sanitizeFilename(msg.info.sessionID) : undefined;
|
|
22924
23027
|
const targetDir = sessionSubdir ? join7(saveDir, sessionSubdir) : saveDir;
|
|
22925
23028
|
try {
|
|
@@ -23080,6 +23183,7 @@ function createPostFileToolNudgeHook(options = {}) {
|
|
|
23080
23183
|
};
|
|
23081
23184
|
}
|
|
23082
23185
|
// src/hooks/task-session-manager/index.ts
|
|
23186
|
+
import path8 from "node:path";
|
|
23083
23187
|
var AGENT_NAME_SET = new Set([
|
|
23084
23188
|
"orchestrator",
|
|
23085
23189
|
"oracle",
|
|
@@ -23098,10 +23202,89 @@ function isAgentName(value) {
|
|
|
23098
23202
|
function isObjectRecord(value) {
|
|
23099
23203
|
return typeof value === "object" && value !== null;
|
|
23100
23204
|
}
|
|
23205
|
+
function extractPath(output) {
|
|
23206
|
+
return /<path>([^<]+)<\/path>/.exec(output)?.[1];
|
|
23207
|
+
}
|
|
23208
|
+
function normalizePath(root, file) {
|
|
23209
|
+
const relative = path8.relative(root, file);
|
|
23210
|
+
if (!relative || relative.startsWith("..") || path8.isAbsolute(relative)) {
|
|
23211
|
+
return file;
|
|
23212
|
+
}
|
|
23213
|
+
return relative;
|
|
23214
|
+
}
|
|
23215
|
+
function extractReadFiles(root, output) {
|
|
23216
|
+
if (typeof output.output !== "string")
|
|
23217
|
+
return [];
|
|
23218
|
+
const file = extractPath(output.output);
|
|
23219
|
+
if (!file)
|
|
23220
|
+
return [];
|
|
23221
|
+
return [
|
|
23222
|
+
{
|
|
23223
|
+
path: normalizePath(root, file),
|
|
23224
|
+
lineCount: countReadLines(output.output).length,
|
|
23225
|
+
lineNumbers: countReadLines(output.output),
|
|
23226
|
+
lastReadAt: Date.now()
|
|
23227
|
+
}
|
|
23228
|
+
];
|
|
23229
|
+
}
|
|
23230
|
+
function countReadLines(output) {
|
|
23231
|
+
const lines = new Set;
|
|
23232
|
+
for (const match of output.matchAll(/^([0-9]+):/gm)) {
|
|
23233
|
+
lines.add(Number(match[1]));
|
|
23234
|
+
}
|
|
23235
|
+
return [...lines];
|
|
23236
|
+
}
|
|
23101
23237
|
function createTaskSessionManagerHook(_ctx, options) {
|
|
23102
|
-
const sessionManager = new SessionManager(options.maxSessionsPerAgent
|
|
23238
|
+
const sessionManager = new SessionManager(options.maxSessionsPerAgent, {
|
|
23239
|
+
readContextMinLines: options.readContextMinLines,
|
|
23240
|
+
readContextMaxFiles: options.readContextMaxFiles
|
|
23241
|
+
});
|
|
23103
23242
|
const pendingCalls = new Map;
|
|
23104
23243
|
const pendingCallOrder = [];
|
|
23244
|
+
const contextByTask = new Map;
|
|
23245
|
+
const pendingManagedTaskIds = new Set;
|
|
23246
|
+
function addTaskContext(taskId, files) {
|
|
23247
|
+
if (files.length === 0)
|
|
23248
|
+
return;
|
|
23249
|
+
let context = contextByTask.get(taskId);
|
|
23250
|
+
if (!context) {
|
|
23251
|
+
context = new Map;
|
|
23252
|
+
contextByTask.set(taskId, context);
|
|
23253
|
+
}
|
|
23254
|
+
for (const file of files) {
|
|
23255
|
+
const pending = context.get(file.path) ?? {
|
|
23256
|
+
path: file.path,
|
|
23257
|
+
lines: new Set,
|
|
23258
|
+
lastReadAt: file.lastReadAt
|
|
23259
|
+
};
|
|
23260
|
+
for (const line of file.lineNumbers ?? []) {
|
|
23261
|
+
pending.lines.add(line);
|
|
23262
|
+
}
|
|
23263
|
+
pending.lastReadAt = Math.max(pending.lastReadAt, file.lastReadAt);
|
|
23264
|
+
context.set(file.path, pending);
|
|
23265
|
+
}
|
|
23266
|
+
sessionManager.addContext(taskId, contextFilesForPrompt(context));
|
|
23267
|
+
}
|
|
23268
|
+
function contextFilesForPrompt(context) {
|
|
23269
|
+
if (!context)
|
|
23270
|
+
return [];
|
|
23271
|
+
return [...context.values()].map((file) => ({
|
|
23272
|
+
path: file.path,
|
|
23273
|
+
lineCount: file.lines.size,
|
|
23274
|
+
lastReadAt: file.lastReadAt
|
|
23275
|
+
}));
|
|
23276
|
+
}
|
|
23277
|
+
function canTrackTaskContext(taskId) {
|
|
23278
|
+
return pendingManagedTaskIds.has(taskId) || sessionManager.taskIds().has(taskId);
|
|
23279
|
+
}
|
|
23280
|
+
function pruneContext() {
|
|
23281
|
+
const remembered = sessionManager.taskIds();
|
|
23282
|
+
for (const taskId of contextByTask.keys()) {
|
|
23283
|
+
if (!pendingManagedTaskIds.has(taskId) && !remembered.has(taskId)) {
|
|
23284
|
+
contextByTask.delete(taskId);
|
|
23285
|
+
}
|
|
23286
|
+
}
|
|
23287
|
+
}
|
|
23105
23288
|
function isMissingRememberedSessionError(output) {
|
|
23106
23289
|
const firstLine = output.split(/\r?\n/, 1)[0]?.trim().toLowerCase() ?? "";
|
|
23107
23290
|
return firstLine.startsWith("[error]") && firstLine.includes("session") && (firstLine.includes("not found") || firstLine.includes("no session"));
|
|
@@ -23167,6 +23350,7 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
23167
23350
|
return;
|
|
23168
23351
|
}
|
|
23169
23352
|
args.task_id = remembered.taskId;
|
|
23353
|
+
pendingManagedTaskIds.add(remembered.taskId);
|
|
23170
23354
|
sessionManager.markUsed(input.sessionID, args.subagent_type, remembered.taskId);
|
|
23171
23355
|
if (input.callID) {
|
|
23172
23356
|
rememberPendingCall({
|
|
@@ -23179,6 +23363,12 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
23179
23363
|
}
|
|
23180
23364
|
},
|
|
23181
23365
|
"tool.execute.after": async (input, output) => {
|
|
23366
|
+
if (input.tool.toLowerCase() === "read") {
|
|
23367
|
+
if (input.sessionID && canTrackTaskContext(input.sessionID)) {
|
|
23368
|
+
addTaskContext(input.sessionID, extractReadFiles(_ctx.directory, output));
|
|
23369
|
+
}
|
|
23370
|
+
return;
|
|
23371
|
+
}
|
|
23182
23372
|
if (input.tool.toLowerCase() !== "task")
|
|
23183
23373
|
return;
|
|
23184
23374
|
const pending = takePendingCall(input.callID);
|
|
@@ -23200,6 +23390,10 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
23200
23390
|
agentType: pending.agentType,
|
|
23201
23391
|
label: pending.label
|
|
23202
23392
|
});
|
|
23393
|
+
pendingManagedTaskIds.delete(taskId);
|
|
23394
|
+
const contextFiles = contextFilesForPrompt(contextByTask.get(taskId));
|
|
23395
|
+
sessionManager.addContext(taskId, contextFiles);
|
|
23396
|
+
pruneContext();
|
|
23203
23397
|
},
|
|
23204
23398
|
"experimental.chat.system.transform": async (input, output) => {
|
|
23205
23399
|
if (!input.sessionID || !options.shouldManageSession(input.sessionID)) {
|
|
@@ -23211,6 +23405,13 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
23211
23405
|
output.system.push(reminder);
|
|
23212
23406
|
},
|
|
23213
23407
|
event: async (input) => {
|
|
23408
|
+
if (input.event.type === "session.created") {
|
|
23409
|
+
const info = input.event.properties?.info;
|
|
23410
|
+
if (info?.id && info.parentID && options.shouldManageSession(info.parentID)) {
|
|
23411
|
+
pendingManagedTaskIds.add(info.id);
|
|
23412
|
+
}
|
|
23413
|
+
return;
|
|
23414
|
+
}
|
|
23214
23415
|
if (input.event.type !== "session.deleted")
|
|
23215
23416
|
return;
|
|
23216
23417
|
const sessionId = input.event.properties?.info?.id ?? input.event.properties?.sessionID;
|
|
@@ -23218,6 +23419,9 @@ function createTaskSessionManagerHook(_ctx, options) {
|
|
|
23218
23419
|
return;
|
|
23219
23420
|
sessionManager.clearParent(sessionId);
|
|
23220
23421
|
sessionManager.dropTask(sessionId);
|
|
23422
|
+
contextByTask.delete(sessionId);
|
|
23423
|
+
pendingManagedTaskIds.delete(sessionId);
|
|
23424
|
+
pruneContext();
|
|
23221
23425
|
for (const [callId, pending] of pendingCalls.entries()) {
|
|
23222
23426
|
if (pending.parentSessionId !== sessionId) {
|
|
23223
23427
|
continue;
|
|
@@ -23889,7 +24093,7 @@ function createTodoContinuationHook(ctx, config) {
|
|
|
23889
24093
|
};
|
|
23890
24094
|
}
|
|
23891
24095
|
// src/interview/manager.ts
|
|
23892
|
-
import
|
|
24096
|
+
import path12 from "node:path";
|
|
23893
24097
|
|
|
23894
24098
|
// src/interview/dashboard.ts
|
|
23895
24099
|
import crypto from "node:crypto";
|
|
@@ -23899,27 +24103,27 @@ import {
|
|
|
23899
24103
|
createServer
|
|
23900
24104
|
} from "node:http";
|
|
23901
24105
|
import os3 from "node:os";
|
|
23902
|
-
import
|
|
24106
|
+
import path10 from "node:path";
|
|
23903
24107
|
import { URL as URL2 } from "node:url";
|
|
23904
24108
|
|
|
23905
24109
|
// src/interview/document.ts
|
|
23906
24110
|
import * as fsSync from "node:fs";
|
|
23907
24111
|
import * as fs6 from "node:fs/promises";
|
|
23908
|
-
import * as
|
|
24112
|
+
import * as path9 from "node:path";
|
|
23909
24113
|
var DEFAULT_OUTPUT_FOLDER = "interview";
|
|
23910
24114
|
function normalizeOutputFolder(outputFolder) {
|
|
23911
24115
|
const normalized = outputFolder.trim().replace(/^\/+|\/+$/g, "");
|
|
23912
24116
|
return normalized || DEFAULT_OUTPUT_FOLDER;
|
|
23913
24117
|
}
|
|
23914
24118
|
function createInterviewDirectoryPath(directory, outputFolder) {
|
|
23915
|
-
return
|
|
24119
|
+
return path9.join(directory, normalizeOutputFolder(outputFolder));
|
|
23916
24120
|
}
|
|
23917
24121
|
function createInterviewFilePath(directory, outputFolder, idea) {
|
|
23918
24122
|
const fileName = `${slugify(idea) || "interview"}.md`;
|
|
23919
|
-
return
|
|
24123
|
+
return path9.join(createInterviewDirectoryPath(directory, outputFolder), fileName);
|
|
23920
24124
|
}
|
|
23921
24125
|
function relativeInterviewPath(directory, filePath) {
|
|
23922
|
-
return
|
|
24126
|
+
return path9.relative(directory, filePath) || path9.basename(filePath);
|
|
23923
24127
|
}
|
|
23924
24128
|
function resolveExistingInterviewPath(directory, outputFolder, value) {
|
|
23925
24129
|
const trimmed = value.trim();
|
|
@@ -23928,22 +24132,22 @@ function resolveExistingInterviewPath(directory, outputFolder, value) {
|
|
|
23928
24132
|
}
|
|
23929
24133
|
const outputDir = createInterviewDirectoryPath(directory, outputFolder);
|
|
23930
24134
|
const candidates = new Set;
|
|
23931
|
-
const resolvedRoot =
|
|
23932
|
-
if (
|
|
24135
|
+
const resolvedRoot = path9.resolve(directory);
|
|
24136
|
+
if (path9.isAbsolute(trimmed)) {
|
|
23933
24137
|
candidates.add(trimmed);
|
|
23934
24138
|
} else {
|
|
23935
|
-
candidates.add(
|
|
23936
|
-
candidates.add(
|
|
24139
|
+
candidates.add(path9.resolve(directory, trimmed));
|
|
24140
|
+
candidates.add(path9.join(outputDir, trimmed));
|
|
23937
24141
|
if (!trimmed.endsWith(".md")) {
|
|
23938
|
-
candidates.add(
|
|
24142
|
+
candidates.add(path9.join(outputDir, `${trimmed}.md`));
|
|
23939
24143
|
}
|
|
23940
24144
|
}
|
|
23941
24145
|
for (const candidate of candidates) {
|
|
23942
|
-
if (
|
|
24146
|
+
if (path9.extname(candidate) !== ".md") {
|
|
23943
24147
|
continue;
|
|
23944
24148
|
}
|
|
23945
|
-
const resolved =
|
|
23946
|
-
if (!resolved.startsWith(resolvedRoot +
|
|
24149
|
+
const resolved = path9.resolve(candidate);
|
|
24150
|
+
if (!resolved.startsWith(resolvedRoot + path9.sep) && resolved !== resolvedRoot) {
|
|
23947
24151
|
continue;
|
|
23948
24152
|
}
|
|
23949
24153
|
if (fsSync.existsSync(candidate)) {
|
|
@@ -24023,7 +24227,7 @@ function parseFrontmatter(content) {
|
|
|
24023
24227
|
return result;
|
|
24024
24228
|
}
|
|
24025
24229
|
async function ensureInterviewFile(record) {
|
|
24026
|
-
await fs6.mkdir(
|
|
24230
|
+
await fs6.mkdir(path9.dirname(record.markdownPath), { recursive: true });
|
|
24027
24231
|
try {
|
|
24028
24232
|
await fs6.access(record.markdownPath);
|
|
24029
24233
|
} catch {
|
|
@@ -25693,12 +25897,12 @@ function renderInterviewPage(interviewId, resumeSlug) {
|
|
|
25693
25897
|
|
|
25694
25898
|
// src/interview/dashboard.ts
|
|
25695
25899
|
function getAuthFilePath(port) {
|
|
25696
|
-
const dataHome = process.env.XDG_DATA_HOME ||
|
|
25697
|
-
return
|
|
25900
|
+
const dataHome = process.env.XDG_DATA_HOME || path10.join(os3.homedir(), ".local", "share");
|
|
25901
|
+
return path10.join(dataHome, "opencode", `.dashboard-${port}.json`);
|
|
25698
25902
|
}
|
|
25699
25903
|
function writeAuthFile(port, token) {
|
|
25700
25904
|
const filePath = getAuthFilePath(port);
|
|
25701
|
-
const dir =
|
|
25905
|
+
const dir = path10.dirname(filePath);
|
|
25702
25906
|
try {
|
|
25703
25907
|
fsSync2.mkdirSync(dir, { recursive: true });
|
|
25704
25908
|
} catch {}
|
|
@@ -25835,7 +26039,7 @@ function createDashboardServer(config) {
|
|
|
25835
26039
|
const directories = getKnownDirectories();
|
|
25836
26040
|
const items = [];
|
|
25837
26041
|
for (const dir of directories) {
|
|
25838
|
-
const interviewDir =
|
|
26042
|
+
const interviewDir = path10.join(dir, config.outputFolder);
|
|
25839
26043
|
let entries;
|
|
25840
26044
|
try {
|
|
25841
26045
|
entries = await fs7.readdir(interviewDir);
|
|
@@ -25847,7 +26051,7 @@ function createDashboardServer(config) {
|
|
|
25847
26051
|
continue;
|
|
25848
26052
|
let content;
|
|
25849
26053
|
try {
|
|
25850
|
-
content = await fs7.readFile(
|
|
26054
|
+
content = await fs7.readFile(path10.join(interviewDir, entry), "utf8");
|
|
25851
26055
|
} catch {
|
|
25852
26056
|
continue;
|
|
25853
26057
|
}
|
|
@@ -25873,7 +26077,7 @@ function createDashboardServer(config) {
|
|
|
25873
26077
|
const directories = getKnownDirectories();
|
|
25874
26078
|
let rebuilt = 0;
|
|
25875
26079
|
for (const dir of directories) {
|
|
25876
|
-
const interviewDir =
|
|
26080
|
+
const interviewDir = path10.join(dir, config.outputFolder);
|
|
25877
26081
|
let entries;
|
|
25878
26082
|
try {
|
|
25879
26083
|
entries = await fs7.readdir(interviewDir);
|
|
@@ -25885,7 +26089,7 @@ function createDashboardServer(config) {
|
|
|
25885
26089
|
continue;
|
|
25886
26090
|
let content;
|
|
25887
26091
|
try {
|
|
25888
|
-
content = await fs7.readFile(
|
|
26092
|
+
content = await fs7.readFile(path10.join(interviewDir, entry), "utf8");
|
|
25889
26093
|
} catch {
|
|
25890
26094
|
continue;
|
|
25891
26095
|
}
|
|
@@ -25911,7 +26115,7 @@ function createDashboardServer(config) {
|
|
|
25911
26115
|
questions: [],
|
|
25912
26116
|
pendingAnswers: null,
|
|
25913
26117
|
lastUpdatedAt: fm.updatedAt ? new Date(fm.updatedAt).getTime() : Date.now(),
|
|
25914
|
-
filePath:
|
|
26118
|
+
filePath: path10.join(interviewDir, entry),
|
|
25915
26119
|
nudgeAction: null
|
|
25916
26120
|
});
|
|
25917
26121
|
if (!sessions.has(fm.sessionID)) {
|
|
@@ -26175,7 +26379,7 @@ function createDashboardServer(config) {
|
|
|
26175
26379
|
const dirs = getKnownDirectories();
|
|
26176
26380
|
for (const dir of dirs) {
|
|
26177
26381
|
const slug = extractResumeSlug(interviewId);
|
|
26178
|
-
const candidate =
|
|
26382
|
+
const candidate = path10.join(dir, config.outputFolder, `${slug}.md`);
|
|
26179
26383
|
try {
|
|
26180
26384
|
document = await fs7.readFile(candidate, "utf8");
|
|
26181
26385
|
markdownPath = candidate;
|
|
@@ -26703,7 +26907,7 @@ function createInterviewServer(deps) {
|
|
|
26703
26907
|
// src/interview/service.ts
|
|
26704
26908
|
import { spawn } from "node:child_process";
|
|
26705
26909
|
import * as fs8 from "node:fs/promises";
|
|
26706
|
-
import * as
|
|
26910
|
+
import * as path11 from "node:path";
|
|
26707
26911
|
|
|
26708
26912
|
// src/interview/types.ts
|
|
26709
26913
|
import { z as z3 } from "zod";
|
|
@@ -26970,12 +27174,12 @@ function createInterviewService(ctx, config, deps) {
|
|
|
26970
27174
|
if (!newSlug) {
|
|
26971
27175
|
return;
|
|
26972
27176
|
}
|
|
26973
|
-
const currentFileName =
|
|
27177
|
+
const currentFileName = path11.basename(interview.markdownPath, ".md");
|
|
26974
27178
|
if (currentFileName === newSlug) {
|
|
26975
27179
|
return;
|
|
26976
27180
|
}
|
|
26977
|
-
const dir =
|
|
26978
|
-
const newPath =
|
|
27181
|
+
const dir = path11.dirname(interview.markdownPath);
|
|
27182
|
+
const newPath = path11.join(dir, `${newSlug}.md`);
|
|
26979
27183
|
try {
|
|
26980
27184
|
await fs8.access(newPath);
|
|
26981
27185
|
return;
|
|
@@ -27051,9 +27255,9 @@ function createInterviewService(ctx, config, deps) {
|
|
|
27051
27255
|
const messages = await loadMessages(sessionID);
|
|
27052
27256
|
const title = extractTitle(document);
|
|
27053
27257
|
const record = {
|
|
27054
|
-
id: `${Date.now()}-${++idCounter}-${slugify(
|
|
27258
|
+
id: `${Date.now()}-${++idCounter}-${slugify(path11.basename(markdownPath, ".md")) || "interview"}`,
|
|
27055
27259
|
sessionID,
|
|
27056
|
-
idea: title ||
|
|
27260
|
+
idea: title || path11.basename(markdownPath, ".md"),
|
|
27057
27261
|
markdownPath,
|
|
27058
27262
|
createdAt: nowIso(),
|
|
27059
27263
|
status: "active",
|
|
@@ -27280,7 +27484,7 @@ function createInterviewService(ctx, config, deps) {
|
|
|
27280
27484
|
return fileCache.items;
|
|
27281
27485
|
}
|
|
27282
27486
|
const outputDir = createInterviewDirectoryPath(ctx.directory, outputFolder);
|
|
27283
|
-
const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) =>
|
|
27487
|
+
const activePaths = new Set([...interviewsById.values()].filter((i) => i.status === "active").map((i) => path11.resolve(i.markdownPath)));
|
|
27284
27488
|
let entries;
|
|
27285
27489
|
try {
|
|
27286
27490
|
entries = await fs8.readdir(outputDir);
|
|
@@ -27291,8 +27495,8 @@ function createInterviewService(ctx, config, deps) {
|
|
|
27291
27495
|
for (const entry of entries) {
|
|
27292
27496
|
if (!entry.endsWith(".md"))
|
|
27293
27497
|
continue;
|
|
27294
|
-
const fullPath =
|
|
27295
|
-
if (activePaths.has(
|
|
27498
|
+
const fullPath = path11.join(outputDir, entry);
|
|
27499
|
+
if (activePaths.has(path11.resolve(fullPath)))
|
|
27296
27500
|
continue;
|
|
27297
27501
|
let content;
|
|
27298
27502
|
try {
|
|
@@ -27391,7 +27595,7 @@ function createInterviewManager(ctx, config) {
|
|
|
27391
27595
|
const outputFolder = interviewConfig?.outputFolder ?? "interview";
|
|
27392
27596
|
if (!dashboardEnabled) {
|
|
27393
27597
|
const service2 = createInterviewService(ctx, interviewConfig);
|
|
27394
|
-
const resolvedOutputPath =
|
|
27598
|
+
const resolvedOutputPath = path12.join(ctx.directory, outputFolder);
|
|
27395
27599
|
const server = createInterviewServer({
|
|
27396
27600
|
getState: async (interviewId) => service2.getInterviewState(interviewId),
|
|
27397
27601
|
listInterviewFiles: async () => service2.listInterviewFiles(),
|
|
@@ -27496,7 +27700,7 @@ function createInterviewManager(ctx, config) {
|
|
|
27496
27700
|
listInterviews: () => service.listInterviews(),
|
|
27497
27701
|
submitAnswers: async (interviewId, answers) => service.submitAnswers(interviewId, answers),
|
|
27498
27702
|
handleNudgeAction: async (interviewId, action) => service.handleNudgeAction(interviewId, action),
|
|
27499
|
-
outputFolder:
|
|
27703
|
+
outputFolder: path12.join(ctx.directory, outputFolder),
|
|
27500
27704
|
port: 0
|
|
27501
27705
|
});
|
|
27502
27706
|
service.setBaseUrlResolver(() => perSessionServer.ensureStarted());
|
|
@@ -27936,23 +28140,23 @@ class TmuxMultiplexer {
|
|
|
27936
28140
|
return null;
|
|
27937
28141
|
}
|
|
27938
28142
|
const stdout = await proc.stdout();
|
|
27939
|
-
const
|
|
28143
|
+
const path13 = stdout.trim().split(`
|
|
27940
28144
|
`)[0];
|
|
27941
|
-
if (!
|
|
28145
|
+
if (!path13) {
|
|
27942
28146
|
log("[tmux] findBinary: no path in output");
|
|
27943
28147
|
return null;
|
|
27944
28148
|
}
|
|
27945
|
-
const verifyProc = crossSpawn([
|
|
28149
|
+
const verifyProc = crossSpawn([path13, "-V"], {
|
|
27946
28150
|
stdout: "pipe",
|
|
27947
28151
|
stderr: "pipe"
|
|
27948
28152
|
});
|
|
27949
28153
|
const verifyExit = await verifyProc.exited;
|
|
27950
28154
|
if (verifyExit !== 0) {
|
|
27951
|
-
log("[tmux] findBinary: tmux -V failed", { path:
|
|
28155
|
+
log("[tmux] findBinary: tmux -V failed", { path: path13, verifyExit });
|
|
27952
28156
|
return null;
|
|
27953
28157
|
}
|
|
27954
|
-
log("[tmux] findBinary: found", { path:
|
|
27955
|
-
return
|
|
28158
|
+
log("[tmux] findBinary: found", { path: path13 });
|
|
28159
|
+
return path13;
|
|
27956
28160
|
} catch (err) {
|
|
27957
28161
|
log("[tmux] findBinary: exception", { error: String(err) });
|
|
27958
28162
|
return null;
|
|
@@ -28787,9 +28991,9 @@ function findSgCliPathSync() {
|
|
|
28787
28991
|
}
|
|
28788
28992
|
if (process.platform === "darwin") {
|
|
28789
28993
|
const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
|
|
28790
|
-
for (const
|
|
28791
|
-
if (existsSync7(
|
|
28792
|
-
return
|
|
28994
|
+
for (const path13 of homebrewPaths) {
|
|
28995
|
+
if (existsSync7(path13) && isValidBinary(path13)) {
|
|
28996
|
+
return path13;
|
|
28793
28997
|
}
|
|
28794
28998
|
}
|
|
28795
28999
|
}
|
|
@@ -28806,8 +29010,8 @@ function getSgCliPath() {
|
|
|
28806
29010
|
}
|
|
28807
29011
|
return "sg";
|
|
28808
29012
|
}
|
|
28809
|
-
function setSgCliPath(
|
|
28810
|
-
resolvedCliPath =
|
|
29013
|
+
function setSgCliPath(path13) {
|
|
29014
|
+
resolvedCliPath = path13;
|
|
28811
29015
|
}
|
|
28812
29016
|
var DEFAULT_TIMEOUT_MS2 = 300000;
|
|
28813
29017
|
var DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024;
|
|
@@ -29375,14 +29579,14 @@ var BINARY_PREFIXES = [
|
|
|
29375
29579
|
var WEBFETCH_DESCRIPTION = "Fetch a URL with better extraction for static/docs pages. Supports llms.txt probing, content-focused HTML extraction, metadata, redirects, and an optional prompt processed by a cheap secondary model.";
|
|
29376
29580
|
// src/tools/smartfetch/tool.ts
|
|
29377
29581
|
import os4 from "node:os";
|
|
29378
|
-
import
|
|
29582
|
+
import path16 from "node:path";
|
|
29379
29583
|
import {
|
|
29380
29584
|
tool as tool4
|
|
29381
29585
|
} from "@opencode-ai/plugin";
|
|
29382
29586
|
|
|
29383
29587
|
// src/tools/smartfetch/binary.ts
|
|
29384
29588
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
29385
|
-
import
|
|
29589
|
+
import path13 from "node:path";
|
|
29386
29590
|
function extensionForMime(contentType) {
|
|
29387
29591
|
const mime = contentType.split(";")[0]?.trim().toLowerCase();
|
|
29388
29592
|
const map = {
|
|
@@ -29403,10 +29607,10 @@ function buildBinaryResultMessage(fetchResult, savedPath) {
|
|
|
29403
29607
|
async function saveBinary(binaryDir, data, contentType, filename) {
|
|
29404
29608
|
await mkdir2(binaryDir, { recursive: true });
|
|
29405
29609
|
const initialName = filename || `webfetch-${Date.now()}.${extensionForMime(contentType)}`;
|
|
29406
|
-
const parsed =
|
|
29610
|
+
const parsed = path13.parse(initialName);
|
|
29407
29611
|
for (let attempt = 0;attempt < 1000; attempt++) {
|
|
29408
29612
|
const candidateName = attempt === 0 ? initialName : `${parsed.name}-${attempt}${parsed.ext || `.${extensionForMime(contentType)}`}`;
|
|
29409
|
-
const file =
|
|
29613
|
+
const file = path13.join(binaryDir, candidateName);
|
|
29410
29614
|
try {
|
|
29411
29615
|
await writeFile2(file, data, { flag: "wx" });
|
|
29412
29616
|
return file;
|
|
@@ -30060,7 +30264,7 @@ var L = class u2 {
|
|
|
30060
30264
|
};
|
|
30061
30265
|
|
|
30062
30266
|
// src/tools/smartfetch/network.ts
|
|
30063
|
-
import
|
|
30267
|
+
import path14 from "node:path";
|
|
30064
30268
|
|
|
30065
30269
|
// src/tools/smartfetch/utils.ts
|
|
30066
30270
|
var import_readability = __toESM(require_readability(), 1);
|
|
@@ -30785,7 +30989,7 @@ function inferFilenameFromUrl(url) {
|
|
|
30785
30989
|
function truncateFilename(name, maxLength = 180) {
|
|
30786
30990
|
if (name.length <= maxLength)
|
|
30787
30991
|
return name;
|
|
30788
|
-
const parsed =
|
|
30992
|
+
const parsed = path14.parse(name);
|
|
30789
30993
|
const ext = parsed.ext || "";
|
|
30790
30994
|
const baseLimit = Math.max(1, maxLength - ext.length);
|
|
30791
30995
|
return `${parsed.name.slice(0, baseLimit)}${ext}`;
|
|
@@ -30957,7 +31161,7 @@ function isInvalidLlmsResult(fetchResult) {
|
|
|
30957
31161
|
// src/tools/smartfetch/secondary-model.ts
|
|
30958
31162
|
import { existsSync as existsSync9 } from "node:fs";
|
|
30959
31163
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
30960
|
-
import
|
|
31164
|
+
import path15 from "node:path";
|
|
30961
31165
|
function parseModelRef(value) {
|
|
30962
31166
|
if (!value)
|
|
30963
31167
|
return;
|
|
@@ -30983,7 +31187,7 @@ function pickAgentModelRef(value) {
|
|
|
30983
31187
|
}
|
|
30984
31188
|
function findPreferredOpenCodeConfigPath(baseDir) {
|
|
30985
31189
|
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
|
30986
|
-
const fullPath =
|
|
31190
|
+
const fullPath = path15.join(baseDir, file);
|
|
30987
31191
|
if (existsSync9(fullPath))
|
|
30988
31192
|
return fullPath;
|
|
30989
31193
|
}
|
|
@@ -31000,7 +31204,7 @@ async function readOpenCodeConfigFile(configPath) {
|
|
|
31000
31204
|
}
|
|
31001
31205
|
}
|
|
31002
31206
|
async function readEffectiveOpenCodeConfig(directory) {
|
|
31003
|
-
const projectDir =
|
|
31207
|
+
const projectDir = path15.join(directory, ".opencode");
|
|
31004
31208
|
const userDirs = getConfigSearchDirs();
|
|
31005
31209
|
const projectPath = findPreferredOpenCodeConfigPath(projectDir);
|
|
31006
31210
|
const userPath = userDirs.map((configDir) => findPreferredOpenCodeConfigPath(configDir)).find(Boolean);
|
|
@@ -31161,7 +31365,7 @@ async function runSecondaryModelWithFallback(client, directory, models, prompt,
|
|
|
31161
31365
|
// src/tools/smartfetch/tool.ts
|
|
31162
31366
|
var z5 = tool4.schema;
|
|
31163
31367
|
function createWebfetchTool(pluginCtx, options = {}) {
|
|
31164
|
-
const binaryDir = options.binaryDir ||
|
|
31368
|
+
const binaryDir = options.binaryDir || path16.join(os4.tmpdir(), "opencode-smartfetch");
|
|
31165
31369
|
return tool4({
|
|
31166
31370
|
description: WEBFETCH_DESCRIPTION,
|
|
31167
31371
|
args: {
|
|
@@ -31685,6 +31889,16 @@ class SubagentDepthTracker {
|
|
|
31685
31889
|
|
|
31686
31890
|
// src/utils/system-collapse.ts
|
|
31687
31891
|
function collapseSystemInPlace(system2) {
|
|
31892
|
+
if (system2.length === 0) {
|
|
31893
|
+
return;
|
|
31894
|
+
}
|
|
31895
|
+
if (system2.length === 1) {
|
|
31896
|
+
if (system2[0]) {
|
|
31897
|
+
return;
|
|
31898
|
+
}
|
|
31899
|
+
system2.length = 0;
|
|
31900
|
+
return;
|
|
31901
|
+
}
|
|
31688
31902
|
const joined = system2.join(`
|
|
31689
31903
|
|
|
31690
31904
|
`);
|
|
@@ -31723,6 +31937,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31723
31937
|
const sessionId = new Date().toISOString().replace(/[-:]/g, "").slice(0, 15);
|
|
31724
31938
|
initLogger(sessionId);
|
|
31725
31939
|
let config;
|
|
31940
|
+
let disabledAgents;
|
|
31726
31941
|
let agentDefs;
|
|
31727
31942
|
let agents;
|
|
31728
31943
|
let mcps;
|
|
@@ -31748,9 +31963,12 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31748
31963
|
let presetManager;
|
|
31749
31964
|
let councilTools;
|
|
31750
31965
|
let webfetch;
|
|
31966
|
+
let rewriteDisplayNameMentions;
|
|
31751
31967
|
let toolCount = 0;
|
|
31752
31968
|
try {
|
|
31753
31969
|
config = loadPluginConfig(ctx.directory);
|
|
31970
|
+
disabledAgents = getDisabledAgents(config);
|
|
31971
|
+
rewriteDisplayNameMentions = createDisplayNameMentionRewriter(config);
|
|
31754
31972
|
agentDefs = createAgents(config);
|
|
31755
31973
|
agents = getAgentConfigs(config);
|
|
31756
31974
|
modelArrayMap = {};
|
|
@@ -31787,7 +32005,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31787
32005
|
main_pane_size: config.multiplexer?.main_pane_size ?? 60
|
|
31788
32006
|
};
|
|
31789
32007
|
const multiplexer = getMultiplexer(multiplexerConfig);
|
|
31790
|
-
multiplexerEnabled = multiplexerConfig.type !== "none" && multiplexer !== null;
|
|
32008
|
+
multiplexerEnabled = multiplexerConfig.type !== "none" && multiplexer !== null && multiplexer.isInsideSession();
|
|
31791
32009
|
log("[plugin] initialized with multiplexer config", {
|
|
31792
32010
|
multiplexerConfig,
|
|
31793
32011
|
enabled: multiplexerEnabled,
|
|
@@ -31824,6 +32042,8 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
31824
32042
|
});
|
|
31825
32043
|
taskSessionManagerHook = createTaskSessionManagerHook(ctx, {
|
|
31826
32044
|
maxSessionsPerAgent: config.sessionManager?.maxSessionsPerAgent ?? 2,
|
|
32045
|
+
readContextMinLines: config.sessionManager?.readContextMinLines ?? 10,
|
|
32046
|
+
readContextMaxFiles: config.sessionManager?.readContextMaxFiles ?? 8,
|
|
31827
32047
|
shouldManageSession: (sessionID) => sessionAgentMap.get(sessionID) === "orchestrator"
|
|
31828
32048
|
});
|
|
31829
32049
|
interviewManager = createInterviewManager(ctx, config);
|
|
@@ -32043,7 +32263,7 @@ var OhMyOpenCodeLite = async (ctx) => {
|
|
|
32043
32263
|
const alreadyInjected = output.system.some((s) => typeof s === "string" && s.includes("<Role>") && s.includes("orchestrator"));
|
|
32044
32264
|
if (!alreadyInjected) {
|
|
32045
32265
|
const orchestratorDef = agentDefs.find((a) => a.name === "orchestrator");
|
|
32046
|
-
const orchestratorPrompt = typeof orchestratorDef?.config?.prompt === "string" ? orchestratorDef.config.prompt : buildOrchestratorPrompt(
|
|
32266
|
+
const orchestratorPrompt = typeof orchestratorDef?.config?.prompt === "string" ? orchestratorDef.config.prompt : buildOrchestratorPrompt(disabledAgents);
|
|
32047
32267
|
output.system[0] = orchestratorPrompt + (output.system[0] ? `
|
|
32048
32268
|
|
|
32049
32269
|
${output.system[0]}` : "");
|
|
@@ -32064,13 +32284,13 @@ ${output.system[0]}` : "");
|
|
|
32064
32284
|
if (part.type !== "text" || typeof part.text !== "string") {
|
|
32065
32285
|
continue;
|
|
32066
32286
|
}
|
|
32067
|
-
part.text = rewriteDisplayNameMentions(
|
|
32287
|
+
part.text = rewriteDisplayNameMentions(part.text);
|
|
32068
32288
|
}
|
|
32069
32289
|
}
|
|
32070
32290
|
processImageAttachments({
|
|
32071
32291
|
messages: typedOutput.messages,
|
|
32072
32292
|
workDir: ctx.directory,
|
|
32073
|
-
disabledAgents
|
|
32293
|
+
disabledAgents,
|
|
32074
32294
|
log
|
|
32075
32295
|
});
|
|
32076
32296
|
await todoContinuationHook.handleMessagesTransform({
|
|
@@ -36,6 +36,8 @@ export declare function resolveAgentVariant(config: PluginConfig | undefined, ag
|
|
|
36
36
|
* - displayName aliases (e.g. "advisor" -> "oracle")
|
|
37
37
|
*/
|
|
38
38
|
export declare function resolveRuntimeAgentName(config: PluginConfig | undefined, agentName: string): string;
|
|
39
|
+
export type DisplayNameMentionRewriter = (text: string) => string;
|
|
40
|
+
export declare function createDisplayNameMentionRewriter(config: PluginConfig | undefined): DisplayNameMentionRewriter;
|
|
39
41
|
/**
|
|
40
42
|
* Rewrites user-facing display-name mentions (e.g. @advisor) into internal
|
|
41
43
|
* agent mentions (e.g. @oracle) for runtime routing.
|
package/dist/utils/logger.d.ts
CHANGED
|
@@ -2,5 +2,7 @@ declare function getLogDir(): string;
|
|
|
2
2
|
export declare function initLogger(sessionId: string): void;
|
|
3
3
|
/** @internal Reset logger state for testing */
|
|
4
4
|
export declare function resetLogger(): void;
|
|
5
|
+
/** @internal Wait for queued log writes in tests. */
|
|
6
|
+
export declare function flushLoggerForTesting(): Promise<void>;
|
|
5
7
|
export { getLogDir };
|
|
6
8
|
export declare function log(message: string, data?: unknown): void;
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import type { AgentName } from '../config';
|
|
2
|
+
export interface ContextFile {
|
|
3
|
+
path: string;
|
|
4
|
+
lineCount: number;
|
|
5
|
+
lineNumbers?: number[];
|
|
6
|
+
lastReadAt: number;
|
|
7
|
+
}
|
|
2
8
|
export interface RememberedTaskSession {
|
|
3
9
|
alias: string;
|
|
4
10
|
taskId: string;
|
|
5
11
|
agentType: AgentName;
|
|
6
12
|
label: string;
|
|
13
|
+
contextFiles: ContextFile[];
|
|
7
14
|
createdAt: number;
|
|
8
15
|
lastUsedAt: number;
|
|
9
16
|
}
|
|
17
|
+
interface SessionManagerOptions {
|
|
18
|
+
readContextMinLines?: number;
|
|
19
|
+
readContextMaxFiles?: number;
|
|
20
|
+
}
|
|
10
21
|
export declare function deriveTaskSessionLabel(input: {
|
|
11
22
|
description?: string;
|
|
12
23
|
prompt?: string;
|
|
@@ -14,10 +25,12 @@ export declare function deriveTaskSessionLabel(input: {
|
|
|
14
25
|
}): string;
|
|
15
26
|
export declare class SessionManager {
|
|
16
27
|
private readonly maxSessionsPerAgent;
|
|
28
|
+
private readonly readContextMinLines;
|
|
29
|
+
private readonly readContextMaxFiles;
|
|
17
30
|
private readonly sessionsByParent;
|
|
18
31
|
private readonly nextAliasIndexByParent;
|
|
19
32
|
private orderCounter;
|
|
20
|
-
constructor(maxSessionsPerAgent: number);
|
|
33
|
+
constructor(maxSessionsPerAgent: number, options?: SessionManagerOptions);
|
|
21
34
|
remember(input: {
|
|
22
35
|
parentSessionId: string;
|
|
23
36
|
taskId: string;
|
|
@@ -28,11 +41,15 @@ export declare class SessionManager {
|
|
|
28
41
|
resolve(parentSessionId: string, agentType: AgentName, key: string): RememberedTaskSession | undefined;
|
|
29
42
|
drop(parentSessionId: string, agentType: AgentName, key: string): void;
|
|
30
43
|
dropTask(taskId: string): void;
|
|
44
|
+
taskIds(): Set<string>;
|
|
45
|
+
addContext(taskId: string, files: ContextFile[]): void;
|
|
31
46
|
clearParent(parentSessionId: string): void;
|
|
32
47
|
formatForPrompt(parentSessionId: string): string | undefined;
|
|
33
48
|
private getAgentGroup;
|
|
34
49
|
private setAgentGroup;
|
|
35
50
|
private nextAlias;
|
|
36
51
|
private trimGroup;
|
|
52
|
+
private trimContextFiles;
|
|
37
53
|
private nextOrder;
|
|
38
54
|
}
|
|
55
|
+
export {};
|
|
@@ -498,6 +498,18 @@
|
|
|
498
498
|
"type": "integer",
|
|
499
499
|
"minimum": 1,
|
|
500
500
|
"maximum": 10
|
|
501
|
+
},
|
|
502
|
+
"readContextMinLines": {
|
|
503
|
+
"default": 10,
|
|
504
|
+
"type": "integer",
|
|
505
|
+
"minimum": 0,
|
|
506
|
+
"maximum": 1000
|
|
507
|
+
},
|
|
508
|
+
"readContextMaxFiles": {
|
|
509
|
+
"default": 8,
|
|
510
|
+
"type": "integer",
|
|
511
|
+
"minimum": 0,
|
|
512
|
+
"maximum": 50
|
|
501
513
|
}
|
|
502
514
|
}
|
|
503
515
|
},
|
package/package.json
CHANGED