@khalilgharbaoui/opencode-claude-code-plugin 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -553,6 +553,20 @@ Notes:
553
553
 
554
554
  Fully restart opencode after upgrading to load the command and runtime changes. Other providers do not gain Claude's native side-question behavior from this command.
555
555
 
556
+ ## Configuration skill
557
+
558
+ The package includes a `claude-code-plugin` skill so your agent can configure it without asking you to navigate all its options. Ask, for example:
559
+
560
+ ```text
561
+ Use the claude-code-plugin skill to configure a work account and idle worker cleanup.
562
+ ```
563
+
564
+ It covers accounts, models and agent effort, proxy tools, permissions, MCP/skill bridging, timeouts, logging, upgrades and troubleshooting. It directs the agent to preserve JSONC comments, change only requested settings, validate the result, protect credentials and ask before paid probes or broader permissions.
565
+
566
+ The plugin registers the bundled directory with opencode's `skills.paths`, making it available to other providers too on supporting opencode versions. For ordinary headless Claude turns it also loads through Claude's native Skill tool as `opencode-skills:claude-code-plugin`, even when `bridgeOpencodeSkills` is off. This requires CLI `--plugin-dir` support; interactive transport, compaction and direct `doGenerate` calls do not load the native bridge.
567
+
568
+ No separate skill installation or copying is needed. It ships with each package version, so upgrading updates the reference. Fully restart opencode to load it. `test-configure-skill.ts` checks coverage of provider/logging options, model ids, proxy tools and environment variables; maintainers must update behavior and default guidance in the same change as the implementation.
569
+
556
570
  ## Skill bridge
557
571
 
558
572
  opencode and Claude Code use the same on-disk skill format, a `<name>/SKILL.md` whose frontmatter carries `name` and `description`, but they read from different directories. opencode looks in `.opencode/skills/` and `~/.config/opencode/skills/`; the Claude CLI looks in `~/.claude/skills/` and its own plugins. So opencode advertises your skills in the system prompt it forwards, the model calls `Skill("browser-automation")`, and Claude answers `Unknown skill`.
@@ -568,7 +582,7 @@ Claude can invoke them with the Skill tool or as `/opencode-skills:<name>`. `--p
568
582
 
569
583
  Discovery order, first match wins: `.opencode/skills/` walking up from the working directory, then `~/.opencode/skills/`, then `$OPENCODE_CONFIG_DIR/skills/`, then `~/.config/opencode/skills/`. A project skill shadows a global one of the same name. If the skill set is unchanged the staged directory is reused between spawns.
570
584
 
571
- It is **off by default** here, unlike on the fork it came from: every bridged skill is also listed in the system prompt opencode already forwards, so a large skill set is paid for twice on every turn. Turn it on when you see `Unknown skill`. It no-ops on the compaction path and on a Claude CLI without `--plugin-dir` (the plugin probes `claude --help` and logs a notice).
585
+ Bridging **your own skills is off by default** here, unlike on the fork it came from: every bridged skill is also listed in the system prompt opencode already forwards, so a large skill set is paid for twice on every turn. Turn it on when you see `Unknown skill`. The bundled configuration skill is loaded independently of this opt-in. The native bridge is not used for compaction or interactive transport, or on a Claude CLI without `--plugin-dir` (the plugin probes `claude --help` and logs a notice).
572
586
 
573
587
  This bridge was written by [@broskees](https://github.com/broskees) (Joseph Roberts) on his fork and absorbed here with credit; see [Credits](#credits).
574
588
 
package/dist/index.d.ts CHANGED
@@ -78,6 +78,10 @@ type OpenCodeConfig = {
78
78
  models?: Record<string, unknown>;
79
79
  }>;
80
80
  agent?: Record<string, Record<string, unknown>>;
81
+ skills?: {
82
+ paths?: string[];
83
+ urls?: string[];
84
+ };
81
85
  };
82
86
  /**
83
87
  * Bus events surface to plugins. Shape mirrors what opencode core publishes
package/dist/index.js CHANGED
@@ -447,10 +447,13 @@ function detectCliSupportsFlag(cliPath, flag) {
447
447
  if (cached) return cached;
448
448
  const promise = (async () => {
449
449
  try {
450
- const { stdout } = await execFileAsync(cliPath, ["--help"], {
450
+ const execution = execFileAsync(cliPath, ["--help"], {
451
451
  timeout: 5e3,
452
+ killSignal: "SIGKILL",
452
453
  maxBuffer: 4 * 1024 * 1024
453
454
  });
455
+ execution.child.stdin?.end();
456
+ const { stdout } = await execution;
454
457
  return stdout.includes(flag);
455
458
  } catch (err) {
456
459
  log.warn("failed to probe claude cli flag support", {
@@ -3302,7 +3305,17 @@ import * as crypto2 from "crypto";
3302
3305
  import * as fs3 from "fs";
3303
3306
  import * as os2 from "os";
3304
3307
  import * as path4 from "path";
3308
+ import { fileURLToPath } from "url";
3305
3309
  var SKILL_PLUGIN_NAME = "opencode-skills";
3310
+ function bundledSkillsDir() {
3311
+ try {
3312
+ const here = fileURLToPath(import.meta.url);
3313
+ const dir = path4.resolve(path4.dirname(here), "..", "skills");
3314
+ return dirExists(dir) ? dir : null;
3315
+ } catch {
3316
+ return null;
3317
+ }
3318
+ }
3306
3319
  function dirExists(p) {
3307
3320
  try {
3308
3321
  return fs3.statSync(p).isDirectory();
@@ -3341,28 +3354,46 @@ function skillRoots(cwd) {
3341
3354
  if (xdg) push(path4.join(xdg, "opencode", "skills"));
3342
3355
  return roots;
3343
3356
  }
3357
+ function collectSkills(root, claimed, found) {
3358
+ let entries;
3359
+ try {
3360
+ entries = fs3.readdirSync(root, { withFileTypes: true });
3361
+ } catch {
3362
+ return;
3363
+ }
3364
+ for (const entry of entries) {
3365
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
3366
+ const name = entry.name;
3367
+ if (name.startsWith(".")) continue;
3368
+ if (claimed.has(name)) continue;
3369
+ const dir = path4.join(root, name);
3370
+ if (!fileExists(path4.join(dir, "SKILL.md"))) continue;
3371
+ claimed.add(name);
3372
+ found.push({ name, dir });
3373
+ }
3374
+ }
3375
+ var byName = (a, b) => a.name.localeCompare(b.name);
3344
3376
  function discoverOpencodeSkills(cwd) {
3345
3377
  const found = [];
3346
3378
  const claimed = /* @__PURE__ */ new Set();
3347
- for (const root of skillRoots(cwd)) {
3348
- let entries;
3349
- try {
3350
- entries = fs3.readdirSync(root, { withFileTypes: true });
3351
- } catch {
3352
- continue;
3353
- }
3354
- for (const entry of entries) {
3355
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
3356
- const name = entry.name;
3357
- if (name.startsWith(".")) continue;
3358
- if (claimed.has(name)) continue;
3359
- const dir = path4.join(root, name);
3360
- if (!fileExists(path4.join(dir, "SKILL.md"))) continue;
3361
- claimed.add(name);
3362
- found.push({ name, dir });
3363
- }
3364
- }
3365
- return found.sort((a, b) => a.name.localeCompare(b.name));
3379
+ for (const root of skillRoots(cwd)) collectSkills(root, claimed, found);
3380
+ return found.sort(byName);
3381
+ }
3382
+ function discoverBundledSkills() {
3383
+ const root = bundledSkillsDir();
3384
+ if (!root) return [];
3385
+ const found = [];
3386
+ collectSkills(root, /* @__PURE__ */ new Set(), found);
3387
+ return found.sort(byName);
3388
+ }
3389
+ function registerBundledSkillPath(config) {
3390
+ const dir = bundledSkillsDir();
3391
+ if (!dir) return false;
3392
+ config.skills ??= {};
3393
+ const paths = config.skills.paths ??= [];
3394
+ if (paths.some((p) => path4.resolve(p) === dir)) return false;
3395
+ paths.push(dir);
3396
+ return true;
3366
3397
  }
3367
3398
  function linkSkill(source, target) {
3368
3399
  try {
@@ -3408,8 +3439,10 @@ function buildSkillPluginDir(skills) {
3408
3439
  return root;
3409
3440
  }
3410
3441
  async function resolveSkillPluginDirs(opts) {
3411
- if (!opts.enabled) return [];
3412
- const skills = discoverOpencodeSkills(opts.cwd);
3442
+ const user = opts.enabled ? discoverOpencodeSkills(opts.cwd) : [];
3443
+ const claimed = new Set(user.map((s) => s.name));
3444
+ const bundled = discoverBundledSkills().filter((s) => !claimed.has(s.name));
3445
+ const skills = [...user, ...bundled].sort(byName);
3413
3446
  if (skills.length === 0) return [];
3414
3447
  const supported = await detectCliSupportsFlag(opts.cliPath, "--plugin-dir");
3415
3448
  if (!supported) {
@@ -3424,6 +3457,7 @@ async function resolveSkillPluginDirs(opts) {
3424
3457
  log.info("bridged opencode skills into claude", {
3425
3458
  count: skills.length,
3426
3459
  names: skills.map((s) => s.name),
3460
+ bundled: bundled.map((s) => s.name),
3427
3461
  pluginDir: dir
3428
3462
  });
3429
3463
  return [dir];
@@ -7670,7 +7704,7 @@ import {
7670
7704
  } from "fs";
7671
7705
  import { homedir as homedir6 } from "os";
7672
7706
  import { join as join8, resolve as resolve4 } from "path";
7673
- import { fileURLToPath } from "url";
7707
+ import { fileURLToPath as fileURLToPath2 } from "url";
7674
7708
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
7675
7709
  var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
7676
7710
  var alreadyRan = false;
@@ -7702,7 +7736,7 @@ function userIntendsToUseUnscoped() {
7702
7736
  }
7703
7737
  function ourLoadedDir() {
7704
7738
  try {
7705
- const filePath = fileURLToPath(import.meta.url);
7739
+ const filePath = fileURLToPath2(import.meta.url);
7706
7740
  return realpathSync(resolve4(filePath, "..", ".."));
7707
7741
  } catch {
7708
7742
  return null;
@@ -7776,12 +7810,12 @@ import { execFile as execFile2 } from "child_process";
7776
7810
  import * as fs6 from "fs";
7777
7811
  import * as path8 from "path";
7778
7812
  import { promisify as promisify2 } from "util";
7779
- import { fileURLToPath as fileURLToPath2 } from "url";
7813
+ import { fileURLToPath as fileURLToPath3 } from "url";
7780
7814
  var cachedPluginVersion;
7781
7815
  function pluginVersion() {
7782
7816
  if (cachedPluginVersion) return cachedPluginVersion;
7783
7817
  try {
7784
- const here = path8.dirname(fileURLToPath2(import.meta.url));
7818
+ const here = path8.dirname(fileURLToPath3(import.meta.url));
7785
7819
  const raw = fs6.readFileSync(path8.join(here, "..", "package.json"), "utf8");
7786
7820
  const version = JSON.parse(raw).version;
7787
7821
  cachedPluginVersion = typeof version === "string" ? version : "unknown";
@@ -8177,6 +8211,7 @@ var server = async (input) => {
8177
8211
  return {
8178
8212
  config: async (config) => {
8179
8213
  if (registerSideQuestionCommand(config)) ownsSideQuestionCommand = true;
8214
+ registerBundledSkillPath(config);
8180
8215
  config.provider ??= {};
8181
8216
  await buildAgentRegistry(config);
8182
8217
  const expanded = await expandAccountProviders(config);