oh-my-opencode-slim 1.0.6 → 1.0.7
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 +22 -14
- package/dist/cli/divoom.d.ts +23 -0
- package/dist/cli/doctor.d.ts +38 -0
- package/dist/cli/index.js +306 -13
- package/dist/cli/providers.d.ts +3 -0
- package/dist/config/council-schema.d.ts +2 -2
- package/dist/config/index.d.ts +1 -1
- package/dist/config/loader.d.ts +46 -1
- package/dist/config/schema.d.ts +23 -0
- package/dist/divoom/council.gif +0 -0
- package/dist/divoom/designer.gif +0 -0
- package/dist/divoom/explorer.gif +0 -0
- package/dist/divoom/fixer.gif +0 -0
- package/dist/divoom/input.gif +0 -0
- package/dist/divoom/intro.gif +0 -0
- package/dist/divoom/librarian.gif +0 -0
- package/dist/divoom/manager.d.ts +57 -0
- package/dist/divoom/oracle.gif +0 -0
- package/dist/divoom/orchestrator.gif +0 -0
- package/dist/index.js +722 -237
- package/dist/integrations/divoom/index.d.ts +3 -0
- package/dist/integrations/divoom/status-manager.d.ts +31 -0
- package/dist/integrations/divoom/swift-helper-source.d.ts +1 -0
- package/dist/integrations/divoom/swift-transport.d.ts +26 -0
- package/dist/integrations/divoom/types.d.ts +41 -0
- package/dist/multiplexer/tmux/index.d.ts +5 -0
- package/dist/tools/council.d.ts +2 -2
- package/dist/tui.d.ts +1 -0
- package/dist/tui.js +623 -11
- package/dist/utils/session.d.ts +10 -4
- package/oh-my-opencode-slim.schema.json +59 -0
- package/package.json +3 -2
package/dist/tui.js
CHANGED
|
@@ -75,17 +75,592 @@ var TMUX_SPAWN_DELAY_MS = 500;
|
|
|
75
75
|
var COUNCILLOR_STAGGER_MS = 250;
|
|
76
76
|
var DEFAULT_DISABLED_AGENTS = ["observer"];
|
|
77
77
|
|
|
78
|
-
// src/
|
|
78
|
+
// src/config/loader.ts
|
|
79
79
|
import * as fs from "node:fs";
|
|
80
|
-
import * as os from "node:os";
|
|
81
80
|
import * as path from "node:path";
|
|
81
|
+
|
|
82
|
+
// src/cli/paths.ts
|
|
83
|
+
import { homedir } from "node:os";
|
|
84
|
+
import { dirname, join } from "node:path";
|
|
85
|
+
function getDefaultOpenCodeConfigDir() {
|
|
86
|
+
const userConfigDir = process.env.XDG_CONFIG_HOME ? process.env.XDG_CONFIG_HOME : join(homedir(), ".config");
|
|
87
|
+
return join(userConfigDir, "opencode");
|
|
88
|
+
}
|
|
89
|
+
function getCustomOpenCodeConfigDir() {
|
|
90
|
+
const configDir = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
91
|
+
return configDir || undefined;
|
|
92
|
+
}
|
|
93
|
+
function getConfigSearchDirs() {
|
|
94
|
+
const dirs = [getCustomOpenCodeConfigDir(), getDefaultOpenCodeConfigDir()];
|
|
95
|
+
return dirs.filter((dir, index) => {
|
|
96
|
+
return Boolean(dir) && dirs.indexOf(dir) === index;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function getOpenCodeConfigPaths() {
|
|
100
|
+
const configDir = getDefaultOpenCodeConfigDir();
|
|
101
|
+
return [join(configDir, "opencode.json"), join(configDir, "opencode.jsonc")];
|
|
102
|
+
}
|
|
103
|
+
// src/config/council-schema.ts
|
|
104
|
+
import { z } from "zod";
|
|
105
|
+
var ModelIdSchema = z.string().regex(/^[^/\s]+\/[^\s]+$/, 'Expected provider/model format (e.g. "openai/gpt-5.4-mini")');
|
|
106
|
+
var CouncillorConfigSchema = z.object({
|
|
107
|
+
model: ModelIdSchema.describe('Model ID in provider/model format (e.g. "openai/gpt-5.4-mini")'),
|
|
108
|
+
variant: z.string().optional(),
|
|
109
|
+
prompt: z.string().optional().describe("Optional role/guidance injected into the councillor user prompt")
|
|
110
|
+
});
|
|
111
|
+
var CouncilPresetSchema = z.record(z.string(), z.record(z.string(), z.unknown())).transform((entries, ctx) => {
|
|
112
|
+
const councillors = {};
|
|
113
|
+
for (const [key, raw] of Object.entries(entries)) {
|
|
114
|
+
if (key === "master")
|
|
115
|
+
continue;
|
|
116
|
+
if (key === "councillors" && typeof raw === "object" && raw !== null) {
|
|
117
|
+
for (const [innerKey, innerRaw] of Object.entries(raw)) {
|
|
118
|
+
const innerParsed = CouncillorConfigSchema.safeParse(innerRaw);
|
|
119
|
+
if (!innerParsed.success) {
|
|
120
|
+
ctx.addIssue({
|
|
121
|
+
code: z.ZodIssueCode.custom,
|
|
122
|
+
message: `Invalid councillor "${innerKey}" (nested under legacy "councillors" key): ${innerParsed.error.issues.map((i) => i.message).join(", ")}`
|
|
123
|
+
});
|
|
124
|
+
return z.NEVER;
|
|
125
|
+
}
|
|
126
|
+
councillors[innerKey] = innerParsed.data;
|
|
127
|
+
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const parsed = CouncillorConfigSchema.safeParse(raw);
|
|
131
|
+
if (!parsed.success) {
|
|
132
|
+
ctx.addIssue({
|
|
133
|
+
code: z.ZodIssueCode.custom,
|
|
134
|
+
message: `Invalid councillor "${key}": ${parsed.error.issues.map((i) => i.message).join(", ")}`
|
|
135
|
+
});
|
|
136
|
+
return z.NEVER;
|
|
137
|
+
}
|
|
138
|
+
councillors[key] = parsed.data;
|
|
139
|
+
}
|
|
140
|
+
return councillors;
|
|
141
|
+
});
|
|
142
|
+
var CouncillorExecutionModeSchema = z.enum(["parallel", "serial"]).default("parallel").describe('Execution mode for councillors. Use "serial" for single-model systems to avoid conflicts. ' + 'Use "parallel" for multi-model systems for faster execution.');
|
|
143
|
+
var CouncilConfigSchema = z.object({
|
|
144
|
+
presets: z.record(z.string(), CouncilPresetSchema),
|
|
145
|
+
timeout: z.number().min(0).default(180000),
|
|
146
|
+
default_preset: z.string().default("default"),
|
|
147
|
+
councillor_execution_mode: CouncillorExecutionModeSchema.describe('Execution mode for councillors. "serial" runs them one at a time (required for single-model systems). "parallel" runs them concurrently (default, faster for multi-model systems).'),
|
|
148
|
+
councillor_retries: z.number().int().min(0).max(5).default(3).describe("Number of retry attempts for councillors that return empty responses " + "(e.g. due to provider rate limiting). Default: 3 retries."),
|
|
149
|
+
master: z.unknown().optional().describe("DEPRECATED — ignored. Council agent synthesizes directly."),
|
|
150
|
+
master_timeout: z.unknown().optional().describe('DEPRECATED — ignored. Use "timeout" instead.'),
|
|
151
|
+
master_fallback: z.unknown().optional().describe("DEPRECATED — ignored. No separate master session.")
|
|
152
|
+
}).transform((data) => {
|
|
153
|
+
const deprecated = [];
|
|
154
|
+
if (data.master !== undefined)
|
|
155
|
+
deprecated.push("master");
|
|
156
|
+
if (data.master_timeout !== undefined)
|
|
157
|
+
deprecated.push("master_timeout");
|
|
158
|
+
if (data.master_fallback !== undefined)
|
|
159
|
+
deprecated.push("master_fallback");
|
|
160
|
+
const legacyMasterModel = typeof data.master === "object" && data.master !== null && "model" in data.master && typeof data.master.model === "string" ? data.master.model : undefined;
|
|
161
|
+
return {
|
|
162
|
+
presets: data.presets,
|
|
163
|
+
timeout: data.timeout,
|
|
164
|
+
default_preset: data.default_preset,
|
|
165
|
+
councillor_execution_mode: data.councillor_execution_mode,
|
|
166
|
+
councillor_retries: data.councillor_retries,
|
|
167
|
+
_deprecated: deprecated.length > 0 ? deprecated : undefined,
|
|
168
|
+
_legacyMasterModel: legacyMasterModel
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
// src/config/schema.ts
|
|
172
|
+
import { z as z2 } from "zod";
|
|
173
|
+
var ProviderModelIdSchema = z2.string().regex(/^[^/\s]+\/[^\s]+$/, "Expected provider/model format (provider/.../model)");
|
|
174
|
+
var ManualAgentPlanSchema = z2.object({
|
|
175
|
+
primary: ProviderModelIdSchema,
|
|
176
|
+
fallback1: ProviderModelIdSchema,
|
|
177
|
+
fallback2: ProviderModelIdSchema,
|
|
178
|
+
fallback3: ProviderModelIdSchema
|
|
179
|
+
}).superRefine((value, ctx) => {
|
|
180
|
+
const unique = new Set([
|
|
181
|
+
value.primary,
|
|
182
|
+
value.fallback1,
|
|
183
|
+
value.fallback2,
|
|
184
|
+
value.fallback3
|
|
185
|
+
]);
|
|
186
|
+
if (unique.size !== 4) {
|
|
187
|
+
ctx.addIssue({
|
|
188
|
+
code: z2.ZodIssueCode.custom,
|
|
189
|
+
message: "primary and fallbacks must be unique per agent"
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
var ManualPlanSchema = z2.object({
|
|
194
|
+
orchestrator: ManualAgentPlanSchema,
|
|
195
|
+
oracle: ManualAgentPlanSchema,
|
|
196
|
+
designer: ManualAgentPlanSchema,
|
|
197
|
+
explorer: ManualAgentPlanSchema,
|
|
198
|
+
librarian: ManualAgentPlanSchema,
|
|
199
|
+
fixer: ManualAgentPlanSchema
|
|
200
|
+
}).strict();
|
|
201
|
+
var AgentModelChainSchema = z2.array(z2.string()).min(1);
|
|
202
|
+
var FallbackChainsSchema = z2.object({
|
|
203
|
+
orchestrator: AgentModelChainSchema.optional(),
|
|
204
|
+
oracle: AgentModelChainSchema.optional(),
|
|
205
|
+
designer: AgentModelChainSchema.optional(),
|
|
206
|
+
explorer: AgentModelChainSchema.optional(),
|
|
207
|
+
librarian: AgentModelChainSchema.optional(),
|
|
208
|
+
fixer: AgentModelChainSchema.optional()
|
|
209
|
+
}).catchall(AgentModelChainSchema);
|
|
210
|
+
var AgentOverrideConfigSchema = z2.object({
|
|
211
|
+
model: z2.union([
|
|
212
|
+
z2.string(),
|
|
213
|
+
z2.array(z2.union([
|
|
214
|
+
z2.string(),
|
|
215
|
+
z2.object({
|
|
216
|
+
id: z2.string(),
|
|
217
|
+
variant: z2.string().optional()
|
|
218
|
+
})
|
|
219
|
+
])).min(1)
|
|
220
|
+
]).optional(),
|
|
221
|
+
temperature: z2.number().min(0).max(2).optional(),
|
|
222
|
+
variant: z2.string().optional().catch(undefined),
|
|
223
|
+
skills: z2.array(z2.string()).optional(),
|
|
224
|
+
mcps: z2.array(z2.string()).optional(),
|
|
225
|
+
prompt: z2.string().min(1).optional(),
|
|
226
|
+
orchestratorPrompt: z2.string().min(1).optional(),
|
|
227
|
+
options: z2.record(z2.string(), z2.unknown()).optional(),
|
|
228
|
+
displayName: z2.string().min(1).optional()
|
|
229
|
+
}).strict();
|
|
230
|
+
var MultiplexerTypeSchema = z2.enum(["auto", "tmux", "zellij", "none"]);
|
|
231
|
+
var MultiplexerLayoutSchema = z2.enum([
|
|
232
|
+
"main-horizontal",
|
|
233
|
+
"main-vertical",
|
|
234
|
+
"tiled",
|
|
235
|
+
"even-horizontal",
|
|
236
|
+
"even-vertical"
|
|
237
|
+
]);
|
|
238
|
+
var TmuxLayoutSchema = MultiplexerLayoutSchema;
|
|
239
|
+
var MultiplexerConfigSchema = z2.object({
|
|
240
|
+
type: MultiplexerTypeSchema.default("none"),
|
|
241
|
+
layout: MultiplexerLayoutSchema.default("main-vertical"),
|
|
242
|
+
main_pane_size: z2.number().min(20).max(80).default(60)
|
|
243
|
+
});
|
|
244
|
+
var TmuxConfigSchema = z2.object({
|
|
245
|
+
enabled: z2.boolean().default(false),
|
|
246
|
+
layout: TmuxLayoutSchema.default("main-vertical"),
|
|
247
|
+
main_pane_size: z2.number().min(20).max(80).default(60)
|
|
248
|
+
});
|
|
249
|
+
var PresetSchema = z2.record(z2.string(), AgentOverrideConfigSchema);
|
|
250
|
+
var WebsearchConfigSchema = z2.object({
|
|
251
|
+
provider: z2.enum(["exa", "tavily"]).default("exa")
|
|
252
|
+
});
|
|
253
|
+
var McpNameSchema = z2.enum(["websearch", "context7", "grep_app"]);
|
|
254
|
+
var InterviewConfigSchema = z2.object({
|
|
255
|
+
maxQuestions: z2.number().int().min(1).max(10).default(2),
|
|
256
|
+
outputFolder: z2.string().min(1).default("interview"),
|
|
257
|
+
autoOpenBrowser: z2.boolean().default(true).describe("Automatically open the interview UI in your default browser during interactive runs. Disabled automatically in tests and CI."),
|
|
258
|
+
port: z2.number().int().min(0).max(65535).default(0),
|
|
259
|
+
dashboard: z2.boolean().default(false)
|
|
260
|
+
});
|
|
261
|
+
var SessionManagerConfigSchema = z2.object({
|
|
262
|
+
maxSessionsPerAgent: z2.number().int().min(1).max(10).default(2),
|
|
263
|
+
readContextMinLines: z2.number().int().min(0).max(1000).default(10),
|
|
264
|
+
readContextMaxFiles: z2.number().int().min(0).max(50).default(8)
|
|
265
|
+
});
|
|
266
|
+
var DivoomConfigSchema = z2.object({
|
|
267
|
+
enabled: z2.boolean().default(false),
|
|
268
|
+
python: z2.string().min(1).default("/Applications/Divoom MiniToo.app/Contents/Resources/.venv/bin/python"),
|
|
269
|
+
script: z2.string().min(1).default("/Applications/Divoom MiniToo.app/Contents/Resources/tools/divoom_send.py"),
|
|
270
|
+
size: z2.number().int().min(1).max(1024).default(128),
|
|
271
|
+
fps: z2.number().int().min(1).max(60).default(8),
|
|
272
|
+
speed: z2.number().int().min(1).max(1e4).default(125),
|
|
273
|
+
maxFrames: z2.number().int().min(1).max(500).default(24),
|
|
274
|
+
posterizeBits: z2.number().int().min(1).max(8).default(3),
|
|
275
|
+
gifs: z2.record(z2.string(), z2.string().min(1)).optional()
|
|
276
|
+
});
|
|
277
|
+
var TodoContinuationConfigSchema = z2.object({
|
|
278
|
+
maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
|
|
279
|
+
cooldownMs: z2.number().int().min(0).max(30000).default(3000).describe("Delay in ms before auto-continuing (gives user time to abort)"),
|
|
280
|
+
autoEnable: z2.boolean().default(false).describe("Automatically enable auto-continue when the orchestrator session has enough todos"),
|
|
281
|
+
autoEnableThreshold: z2.number().int().min(1).max(50).default(4).describe("Number of todos that triggers auto-enable (only used when autoEnable is true)")
|
|
282
|
+
});
|
|
283
|
+
var FailoverConfigSchema = z2.object({
|
|
284
|
+
enabled: z2.boolean().default(true),
|
|
285
|
+
timeoutMs: z2.number().min(0).default(15000),
|
|
286
|
+
retryDelayMs: z2.number().min(0).default(500),
|
|
287
|
+
chains: FallbackChainsSchema.default({}),
|
|
288
|
+
retry_on_empty: z2.boolean().default(true).describe("When true (default), empty provider responses are treated as failures, " + "triggering fallback/retry. Set to false to treat them as successes.")
|
|
289
|
+
});
|
|
290
|
+
function validateCustomOnlyPromptFields(overrides, ctx, pathPrefix) {
|
|
291
|
+
for (const [name, override] of Object.entries(overrides)) {
|
|
292
|
+
const isBuiltInOrAlias = ALL_AGENT_NAMES.includes(name) || AGENT_ALIASES[name] !== undefined;
|
|
293
|
+
if (!isBuiltInOrAlias) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (override.prompt !== undefined) {
|
|
297
|
+
ctx.addIssue({
|
|
298
|
+
code: z2.ZodIssueCode.custom,
|
|
299
|
+
path: [...pathPrefix, name, "prompt"],
|
|
300
|
+
message: "prompt is only supported for custom agents"
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (override.orchestratorPrompt !== undefined) {
|
|
304
|
+
ctx.addIssue({
|
|
305
|
+
code: z2.ZodIssueCode.custom,
|
|
306
|
+
path: [...pathPrefix, name, "orchestratorPrompt"],
|
|
307
|
+
message: "orchestratorPrompt is only supported for custom agents"
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
var PluginConfigSchema = z2.object({
|
|
313
|
+
preset: z2.string().optional(),
|
|
314
|
+
setDefaultAgent: z2.boolean().optional(),
|
|
315
|
+
scoringEngineVersion: z2.enum(["v1", "v2-shadow", "v2"]).optional(),
|
|
316
|
+
balanceProviderUsage: z2.boolean().optional(),
|
|
317
|
+
autoUpdate: z2.boolean().optional().describe("Disable automatic installation of plugin updates when false. Defaults to true."),
|
|
318
|
+
manualPlan: ManualPlanSchema.optional(),
|
|
319
|
+
presets: z2.record(z2.string(), PresetSchema).optional(),
|
|
320
|
+
agents: z2.record(z2.string(), AgentOverrideConfigSchema).optional(),
|
|
321
|
+
disabled_agents: z2.array(z2.string()).optional().describe("Agent names to disable completely. " + "Disabled agents are not instantiated and cannot be delegated to. " + "Orchestrator and council internal agents (councillor) cannot be disabled. " + "By default, 'observer' is disabled. Remove it from this list and configure a vision-capable model to enable."),
|
|
322
|
+
disabled_mcps: z2.array(z2.string()).optional(),
|
|
323
|
+
multiplexer: MultiplexerConfigSchema.optional(),
|
|
324
|
+
tmux: TmuxConfigSchema.optional(),
|
|
325
|
+
websearch: WebsearchConfigSchema.optional(),
|
|
326
|
+
interview: InterviewConfigSchema.optional(),
|
|
327
|
+
sessionManager: SessionManagerConfigSchema.optional(),
|
|
328
|
+
divoom: DivoomConfigSchema.optional(),
|
|
329
|
+
todoContinuation: TodoContinuationConfigSchema.optional(),
|
|
330
|
+
fallback: FailoverConfigSchema.optional(),
|
|
331
|
+
council: CouncilConfigSchema.optional()
|
|
332
|
+
}).superRefine((value, ctx) => {
|
|
333
|
+
if (value.agents) {
|
|
334
|
+
validateCustomOnlyPromptFields(value.agents, ctx, ["agents"]);
|
|
335
|
+
}
|
|
336
|
+
if (value.presets) {
|
|
337
|
+
for (const [presetName, preset] of Object.entries(value.presets)) {
|
|
338
|
+
validateCustomOnlyPromptFields(preset, ctx, ["presets", presetName]);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
// src/config/utils.ts
|
|
343
|
+
function getAgentOverride(config, name) {
|
|
344
|
+
const overrides = config?.agents ?? {};
|
|
345
|
+
return overrides[name] ?? overrides[Object.keys(AGENT_ALIASES).find((k) => AGENT_ALIASES[k] === name) ?? ""];
|
|
346
|
+
}
|
|
347
|
+
function getCustomAgentNames(config) {
|
|
348
|
+
const overrides = config?.agents ?? {};
|
|
349
|
+
return Object.keys(overrides).filter((name) => {
|
|
350
|
+
if (AGENT_ALIASES[name] !== undefined) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
return !ALL_AGENT_NAMES.includes(name);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
// src/config/agent-mcps.ts
|
|
357
|
+
var DEFAULT_AGENT_MCPS = {
|
|
358
|
+
orchestrator: ["*", "!context7"],
|
|
359
|
+
designer: [],
|
|
360
|
+
oracle: [],
|
|
361
|
+
librarian: ["websearch", "context7", "grep_app"],
|
|
362
|
+
explorer: [],
|
|
363
|
+
fixer: [],
|
|
364
|
+
observer: [],
|
|
365
|
+
council: [],
|
|
366
|
+
councillor: []
|
|
367
|
+
};
|
|
368
|
+
function parseList(items, allAvailable) {
|
|
369
|
+
if (!items || items.length === 0) {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
372
|
+
const allow = items.filter((i) => !i.startsWith("!"));
|
|
373
|
+
const deny = items.filter((i) => i.startsWith("!")).map((i) => i.slice(1));
|
|
374
|
+
if (deny.includes("*")) {
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
if (allow.includes("*")) {
|
|
378
|
+
return allAvailable.filter((item) => !deny.includes(item));
|
|
379
|
+
}
|
|
380
|
+
return allow.filter((item) => !deny.includes(item) && allAvailable.includes(item));
|
|
381
|
+
}
|
|
382
|
+
function getAgentMcpList(agentName, config) {
|
|
383
|
+
const agentConfig = getAgentOverride(config, agentName);
|
|
384
|
+
if (agentConfig?.mcps !== undefined) {
|
|
385
|
+
return agentConfig.mcps;
|
|
386
|
+
}
|
|
387
|
+
const defaultMcps = DEFAULT_AGENT_MCPS[agentName];
|
|
388
|
+
return defaultMcps ?? [];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/cli/custom-skills.ts
|
|
392
|
+
var CUSTOM_SKILLS = [
|
|
393
|
+
{
|
|
394
|
+
name: "simplify",
|
|
395
|
+
description: "Code simplification and readability-focused refactoring",
|
|
396
|
+
allowedAgents: ["oracle"],
|
|
397
|
+
sourcePath: "src/skills/simplify"
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
name: "codemap",
|
|
401
|
+
description: "Repository understanding and hierarchical codemap generation",
|
|
402
|
+
allowedAgents: ["orchestrator"],
|
|
403
|
+
sourcePath: "src/skills/codemap"
|
|
404
|
+
}
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
// src/cli/skills.ts
|
|
408
|
+
var RECOMMENDED_SKILLS = [
|
|
409
|
+
{
|
|
410
|
+
name: "agent-browser",
|
|
411
|
+
repo: "https://github.com/vercel-labs/agent-browser",
|
|
412
|
+
skillName: "agent-browser",
|
|
413
|
+
allowedAgents: ["designer"],
|
|
414
|
+
description: "High-performance browser automation",
|
|
415
|
+
postInstallCommands: [
|
|
416
|
+
"npm install -g agent-browser",
|
|
417
|
+
"agent-browser install"
|
|
418
|
+
]
|
|
419
|
+
}
|
|
420
|
+
];
|
|
421
|
+
var PERMISSION_ONLY_SKILLS = [
|
|
422
|
+
{
|
|
423
|
+
name: "requesting-code-review",
|
|
424
|
+
allowedAgents: ["oracle"],
|
|
425
|
+
description: "Code review template for reviewer subagents in multi-step workflows"
|
|
426
|
+
}
|
|
427
|
+
];
|
|
428
|
+
function getSkillPermissionsForAgent(agentName, skillList) {
|
|
429
|
+
const permissions = {
|
|
430
|
+
"*": agentName === "orchestrator" ? "allow" : "deny"
|
|
431
|
+
};
|
|
432
|
+
if (skillList) {
|
|
433
|
+
permissions["*"] = "deny";
|
|
434
|
+
for (const name of skillList) {
|
|
435
|
+
if (name === "*") {
|
|
436
|
+
permissions["*"] = "allow";
|
|
437
|
+
} else if (name.startsWith("!")) {
|
|
438
|
+
permissions[name.slice(1)] = "deny";
|
|
439
|
+
} else {
|
|
440
|
+
permissions[name] = "allow";
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return permissions;
|
|
444
|
+
}
|
|
445
|
+
for (const skill of RECOMMENDED_SKILLS) {
|
|
446
|
+
const isAllowed = skill.allowedAgents.includes("*") || skill.allowedAgents.includes(agentName);
|
|
447
|
+
if (isAllowed) {
|
|
448
|
+
permissions[skill.skillName] = "allow";
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
for (const skill of CUSTOM_SKILLS) {
|
|
452
|
+
const isAllowed = skill.allowedAgents.includes("*") || skill.allowedAgents.includes(agentName);
|
|
453
|
+
if (isAllowed) {
|
|
454
|
+
permissions[skill.name] = "allow";
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
for (const skill of PERMISSION_ONLY_SKILLS) {
|
|
458
|
+
const isAllowed = skill.allowedAgents.includes("*") || skill.allowedAgents.includes(agentName);
|
|
459
|
+
if (isAllowed) {
|
|
460
|
+
permissions[skill.name] = "allow";
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return permissions;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/cli/config-io.ts
|
|
467
|
+
function stripJsonComments(json) {
|
|
468
|
+
const commentPattern = /\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g;
|
|
469
|
+
const trailingCommaPattern = /\\"|"(?:\\"|[^"])*"|(,)(\s*[}\]])/g;
|
|
470
|
+
return json.replace(commentPattern, (match, commentGroup) => commentGroup ? "" : match).replace(trailingCommaPattern, (match, comma, closing) => comma ? closing : match);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/config/loader.ts
|
|
474
|
+
var PROMPTS_DIR_NAME = "oh-my-opencode-slim";
|
|
475
|
+
function loadConfigFromPath(configPath, options) {
|
|
476
|
+
try {
|
|
477
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
478
|
+
let rawConfig;
|
|
479
|
+
try {
|
|
480
|
+
rawConfig = JSON.parse(stripJsonComments(content));
|
|
481
|
+
} catch (error) {
|
|
482
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
483
|
+
options?.onWarning?.({
|
|
484
|
+
path: configPath,
|
|
485
|
+
kind: "invalid-json",
|
|
486
|
+
message
|
|
487
|
+
});
|
|
488
|
+
if (!options?.silent) {
|
|
489
|
+
console.warn(`[oh-my-opencode-slim] Invalid JSON in ${configPath}:`, message);
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
const result = PluginConfigSchema.safeParse(rawConfig);
|
|
494
|
+
if (!result.success) {
|
|
495
|
+
options?.onWarning?.({
|
|
496
|
+
path: configPath,
|
|
497
|
+
kind: "invalid-schema",
|
|
498
|
+
message: "Config does not match schema",
|
|
499
|
+
formatted: result.error.format()
|
|
500
|
+
});
|
|
501
|
+
if (!options?.silent) {
|
|
502
|
+
console.warn(`[oh-my-opencode-slim] Invalid config at ${configPath}:`);
|
|
503
|
+
console.warn(result.error.format());
|
|
504
|
+
}
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
return result.data;
|
|
508
|
+
} catch (error) {
|
|
509
|
+
if (error instanceof Error && "code" in error && error.code !== "ENOENT") {
|
|
510
|
+
options?.onWarning?.({
|
|
511
|
+
path: configPath,
|
|
512
|
+
kind: "read-error",
|
|
513
|
+
message: error.message
|
|
514
|
+
});
|
|
515
|
+
if (!options?.silent) {
|
|
516
|
+
console.warn(`[oh-my-opencode-slim] Error reading config from ${configPath}:`, error.message);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function findConfigPath(basePath) {
|
|
523
|
+
const jsoncPath = `${basePath}.jsonc`;
|
|
524
|
+
const jsonPath = `${basePath}.json`;
|
|
525
|
+
if (fs.existsSync(jsoncPath)) {
|
|
526
|
+
return jsoncPath;
|
|
527
|
+
}
|
|
528
|
+
if (fs.existsSync(jsonPath)) {
|
|
529
|
+
return jsonPath;
|
|
530
|
+
}
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
function findConfigPathInDirs(configDirs, baseName) {
|
|
534
|
+
for (const configDir of configDirs) {
|
|
535
|
+
const configPath = findConfigPath(path.join(configDir, baseName));
|
|
536
|
+
if (configPath) {
|
|
537
|
+
return configPath;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
function findPluginConfigPaths(directory) {
|
|
543
|
+
const userConfigPath = findConfigPathInDirs(getConfigSearchDirs(), "oh-my-opencode-slim");
|
|
544
|
+
const projectConfigBasePath = path.join(directory, ".opencode", "oh-my-opencode-slim");
|
|
545
|
+
const projectConfigPath = findConfigPath(projectConfigBasePath);
|
|
546
|
+
return { userConfigPath, projectConfigPath };
|
|
547
|
+
}
|
|
548
|
+
function mergePluginConfigs(base, override) {
|
|
549
|
+
return {
|
|
550
|
+
...base,
|
|
551
|
+
...override,
|
|
552
|
+
agents: deepMerge(base.agents, override.agents),
|
|
553
|
+
tmux: deepMerge(base.tmux, override.tmux),
|
|
554
|
+
multiplexer: deepMerge(base.multiplexer, override.multiplexer),
|
|
555
|
+
interview: deepMerge(base.interview, override.interview),
|
|
556
|
+
sessionManager: deepMerge(base.sessionManager, override.sessionManager),
|
|
557
|
+
divoom: deepMerge(base.divoom, override.divoom),
|
|
558
|
+
fallback: deepMerge(base.fallback, override.fallback),
|
|
559
|
+
council: deepMerge(base.council, override.council)
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function deepMerge(base, override) {
|
|
563
|
+
if (!base)
|
|
564
|
+
return override;
|
|
565
|
+
if (!override)
|
|
566
|
+
return base;
|
|
567
|
+
const result = { ...base };
|
|
568
|
+
for (const key of Object.keys(override)) {
|
|
569
|
+
const baseVal = base[key];
|
|
570
|
+
const overrideVal = override[key];
|
|
571
|
+
if (typeof baseVal === "object" && baseVal !== null && typeof overrideVal === "object" && overrideVal !== null && !Array.isArray(baseVal) && !Array.isArray(overrideVal)) {
|
|
572
|
+
result[key] = deepMerge(baseVal, overrideVal);
|
|
573
|
+
} else {
|
|
574
|
+
result[key] = overrideVal;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return result;
|
|
578
|
+
}
|
|
579
|
+
function loadPluginConfig(directory, options) {
|
|
580
|
+
const { userConfigPath, projectConfigPath } = findPluginConfigPaths(directory);
|
|
581
|
+
let config = userConfigPath ? loadConfigFromPath(userConfigPath, options) ?? {} : {};
|
|
582
|
+
const projectConfig = projectConfigPath ? loadConfigFromPath(projectConfigPath, options) : null;
|
|
583
|
+
if (projectConfig) {
|
|
584
|
+
config = mergePluginConfigs(config, projectConfig);
|
|
585
|
+
}
|
|
586
|
+
config = migrateTmuxToMultiplexer(config);
|
|
587
|
+
const envPreset = process.env.OH_MY_OPENCODE_SLIM_PRESET;
|
|
588
|
+
if (envPreset) {
|
|
589
|
+
config.preset = envPreset;
|
|
590
|
+
}
|
|
591
|
+
if (config.preset) {
|
|
592
|
+
const preset = config.presets?.[config.preset];
|
|
593
|
+
if (preset) {
|
|
594
|
+
config.agents = deepMerge(preset, config.agents);
|
|
595
|
+
} else {
|
|
596
|
+
const presetSource = envPreset === config.preset ? "environment variable" : "config file";
|
|
597
|
+
const availablePresets = config.presets ? Object.keys(config.presets).join(", ") : "none";
|
|
598
|
+
const message = `Preset "${config.preset}" not found (from ${presetSource}). Available presets: ${availablePresets}`;
|
|
599
|
+
options?.onWarning?.({
|
|
600
|
+
path: projectConfigPath ?? userConfigPath ?? "",
|
|
601
|
+
kind: "missing-preset",
|
|
602
|
+
message
|
|
603
|
+
});
|
|
604
|
+
if (!options?.silent) {
|
|
605
|
+
console.warn(`[oh-my-opencode-slim] ${message}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return config;
|
|
610
|
+
}
|
|
611
|
+
function loadAgentPrompt(agentName, preset) {
|
|
612
|
+
const presetDirName = preset && /^[a-zA-Z0-9_-]+$/.test(preset) ? preset : undefined;
|
|
613
|
+
const promptSearchDirs = getConfigSearchDirs().flatMap((configDir) => {
|
|
614
|
+
const promptsDir = path.join(configDir, PROMPTS_DIR_NAME);
|
|
615
|
+
return presetDirName ? [path.join(promptsDir, presetDirName), promptsDir] : [promptsDir];
|
|
616
|
+
});
|
|
617
|
+
const result = {};
|
|
618
|
+
const readFirstPrompt = (fileName, errorPrefix) => {
|
|
619
|
+
for (const dir of promptSearchDirs) {
|
|
620
|
+
const promptPath = path.join(dir, fileName);
|
|
621
|
+
if (!fs.existsSync(promptPath)) {
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
return fs.readFileSync(promptPath, "utf-8");
|
|
626
|
+
} catch (error) {
|
|
627
|
+
console.warn(`[oh-my-opencode-slim] ${errorPrefix} ${promptPath}:`, error instanceof Error ? error.message : String(error));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return;
|
|
631
|
+
};
|
|
632
|
+
result.prompt = readFirstPrompt(`${agentName}.md`, "Error reading prompt file");
|
|
633
|
+
result.appendPrompt = readFirstPrompt(`${agentName}_append.md`, "Error reading append prompt file");
|
|
634
|
+
return result;
|
|
635
|
+
}
|
|
636
|
+
function migrateTmuxToMultiplexer(config) {
|
|
637
|
+
if (config.multiplexer?.type && config.multiplexer.type !== "none") {
|
|
638
|
+
return config;
|
|
639
|
+
}
|
|
640
|
+
if (config.tmux?.enabled) {
|
|
641
|
+
return {
|
|
642
|
+
...config,
|
|
643
|
+
multiplexer: {
|
|
644
|
+
type: "tmux",
|
|
645
|
+
layout: config.tmux.layout ?? "main-vertical",
|
|
646
|
+
main_pane_size: config.tmux.main_pane_size ?? 60
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
return config;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/tui-state.ts
|
|
654
|
+
import * as fs2 from "node:fs";
|
|
655
|
+
import * as os from "node:os";
|
|
656
|
+
import * as path2 from "node:path";
|
|
82
657
|
var STATE_DIR = "oh-my-opencode-slim";
|
|
83
658
|
var STATE_FILE = "tui-state.json";
|
|
84
659
|
function dataDir() {
|
|
85
|
-
return process.env.XDG_DATA_HOME ??
|
|
660
|
+
return process.env.XDG_DATA_HOME ?? path2.join(os.homedir(), ".local", "share");
|
|
86
661
|
}
|
|
87
662
|
function getTuiStatePath() {
|
|
88
|
-
return
|
|
663
|
+
return path2.join(dataDir(), "opencode", "storage", STATE_DIR, STATE_FILE);
|
|
89
664
|
}
|
|
90
665
|
function emptySnapshot() {
|
|
91
666
|
return {
|
|
@@ -106,14 +681,14 @@ function parseSnapshot(value) {
|
|
|
106
681
|
}
|
|
107
682
|
function readTuiSnapshot() {
|
|
108
683
|
try {
|
|
109
|
-
return parseSnapshot(
|
|
684
|
+
return parseSnapshot(fs2.readFileSync(getTuiStatePath(), "utf8"));
|
|
110
685
|
} catch {
|
|
111
686
|
return emptySnapshot();
|
|
112
687
|
}
|
|
113
688
|
}
|
|
114
689
|
async function readTuiSnapshotAsync() {
|
|
115
690
|
try {
|
|
116
|
-
return parseSnapshot(await
|
|
691
|
+
return parseSnapshot(await fs2.promises.readFile(getTuiStatePath(), "utf8"));
|
|
117
692
|
} catch {
|
|
118
693
|
return emptySnapshot();
|
|
119
694
|
}
|
|
@@ -121,8 +696,8 @@ async function readTuiSnapshotAsync() {
|
|
|
121
696
|
function writeTuiSnapshot(snapshot) {
|
|
122
697
|
try {
|
|
123
698
|
const filePath = getTuiStatePath();
|
|
124
|
-
|
|
125
|
-
|
|
699
|
+
fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
|
|
700
|
+
fs2.writeFileSync(filePath, `${JSON.stringify(snapshot)}
|
|
126
701
|
`);
|
|
127
702
|
} catch {}
|
|
128
703
|
}
|
|
@@ -145,6 +720,7 @@ function recordTuiAgentModel(input) {
|
|
|
145
720
|
|
|
146
721
|
// src/tui.ts
|
|
147
722
|
var PLUGIN_NAME = "oh-my-opencode-slim";
|
|
723
|
+
var CONFIG_WARNING_COLOR = "orange";
|
|
148
724
|
var FALLBACK_SIDEBAR_AGENTS = SUBAGENT_NAMES.filter((agent) => agent !== "councillor" && agent !== "council" && !DEFAULT_DISABLED_AGENTS.includes(agent));
|
|
149
725
|
var BORDER = { type: "single" };
|
|
150
726
|
async function readPackageVersion() {
|
|
@@ -177,6 +753,9 @@ function box(props, children = []) {
|
|
|
177
753
|
function truncate(value, max = 24) {
|
|
178
754
|
return value.length > max ? `${value.slice(0, max - 1)}…` : value;
|
|
179
755
|
}
|
|
756
|
+
function getTuiDirectory(api) {
|
|
757
|
+
return api.state?.path?.directory ?? process.cwd();
|
|
758
|
+
}
|
|
180
759
|
function formatSidebarModelName(model) {
|
|
181
760
|
const lastSlash = model.lastIndexOf("/");
|
|
182
761
|
return lastSlash === -1 ? model : model.slice(lastSlash + 1);
|
|
@@ -191,7 +770,8 @@ function row(label, value, theme, valueColor) {
|
|
|
191
770
|
text({ fg: valueColor ?? theme.text }, [value])
|
|
192
771
|
]);
|
|
193
772
|
}
|
|
194
|
-
function renderSidebar(snapshot, version, theme) {
|
|
773
|
+
function renderSidebar(snapshot, version, theme, configInvalid) {
|
|
774
|
+
const configStatusRow = buildConfigStatusRow(configInvalid, theme);
|
|
195
775
|
return box({
|
|
196
776
|
width: "100%",
|
|
197
777
|
flexDirection: "column",
|
|
@@ -208,9 +788,10 @@ function renderSidebar(snapshot, version, theme) {
|
|
|
208
788
|
justifyContent: "space-between",
|
|
209
789
|
alignItems: "center"
|
|
210
790
|
}, [
|
|
211
|
-
box({ paddingLeft: 1, paddingRight: 1, backgroundColor: theme.accent }, [text({ fg: theme.background }, ["
|
|
791
|
+
box({ paddingLeft: 1, paddingRight: 1, backgroundColor: theme.accent }, [text({ fg: theme.background }, ["OMO-Slim"])]),
|
|
212
792
|
text({ fg: theme.textMuted }, [`v${version}`])
|
|
213
793
|
]),
|
|
794
|
+
configStatusRow,
|
|
214
795
|
box({ width: "100%", marginTop: 1 }, [
|
|
215
796
|
text({ fg: theme.text }, ["Agents"])
|
|
216
797
|
]),
|
|
@@ -220,14 +801,44 @@ function renderSidebar(snapshot, version, theme) {
|
|
|
220
801
|
})
|
|
221
802
|
]);
|
|
222
803
|
}
|
|
804
|
+
function buildConfigStatusRow(configInvalid, theme) {
|
|
805
|
+
if (!configInvalid)
|
|
806
|
+
return null;
|
|
807
|
+
return box({
|
|
808
|
+
width: "100%",
|
|
809
|
+
flexDirection: "column",
|
|
810
|
+
marginTop: 1,
|
|
811
|
+
marginBottom: 1
|
|
812
|
+
}, [
|
|
813
|
+
text({ fg: CONFIG_WARNING_COLOR }, ["Config invalid"]),
|
|
814
|
+
text({ fg: theme.textMuted }, ["Run doctor for details"])
|
|
815
|
+
]);
|
|
816
|
+
}
|
|
817
|
+
function readConfigInvalid(directory) {
|
|
818
|
+
let configInvalid = false;
|
|
819
|
+
loadPluginConfig(directory, {
|
|
820
|
+
silent: true,
|
|
821
|
+
onWarning: () => {
|
|
822
|
+
configInvalid = true;
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
return configInvalid;
|
|
826
|
+
}
|
|
223
827
|
var plugin = {
|
|
224
828
|
id: `${PLUGIN_NAME}:tui`,
|
|
225
829
|
tui: async (api, _options, meta) => {
|
|
226
830
|
const version = meta.version ?? await readPackageVersion() ?? "dev";
|
|
831
|
+
let configDirectory = getTuiDirectory(api);
|
|
832
|
+
let configInvalid = readConfigInvalid(configDirectory);
|
|
227
833
|
let snapshot = readTuiSnapshot();
|
|
228
834
|
const renderTimer = setInterval(async () => {
|
|
229
835
|
try {
|
|
230
836
|
snapshot = await readTuiSnapshotAsync();
|
|
837
|
+
const currentDirectory = getTuiDirectory(api);
|
|
838
|
+
if (currentDirectory !== configDirectory) {
|
|
839
|
+
configDirectory = currentDirectory;
|
|
840
|
+
configInvalid = readConfigInvalid(configDirectory);
|
|
841
|
+
}
|
|
231
842
|
api.renderer.requestRender();
|
|
232
843
|
} catch {}
|
|
233
844
|
}, 1000);
|
|
@@ -238,7 +849,7 @@ var plugin = {
|
|
|
238
849
|
order: 900,
|
|
239
850
|
slots: {
|
|
240
851
|
sidebar_content() {
|
|
241
|
-
return renderSidebar(snapshot, version, api.theme.current);
|
|
852
|
+
return renderSidebar(snapshot, version, api.theme.current, configInvalid);
|
|
242
853
|
}
|
|
243
854
|
}
|
|
244
855
|
});
|
|
@@ -246,6 +857,7 @@ var plugin = {
|
|
|
246
857
|
};
|
|
247
858
|
var tui_default = plugin;
|
|
248
859
|
export {
|
|
860
|
+
readConfigInvalid,
|
|
249
861
|
getSidebarAgentNames,
|
|
250
862
|
formatSidebarModelName,
|
|
251
863
|
tui_default as default
|