oh-my-opencode-slim 1.0.6 → 1.1.0

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