clikit-plugin 0.2.20 → 0.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,11 @@
1
1
  #!/usr/bin/env bun
2
+ interface ScaffoldStats {
3
+ copied: number;
4
+ skipped: number;
5
+ missingSources: string[];
6
+ }
7
+ export declare function resolveProjectDir(env?: NodeJS.ProcessEnv, cwd?: string): string;
8
+ export declare function upsertPluginEntry(existingPlugins: string[], pluginName: string, version: string): string[];
9
+ export declare function scaffoldProjectOpencode(projectDir: string, packageRoot?: string): ScaffoldStats;
2
10
  export {};
3
11
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AASA,UAAU,aAAa;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AA4BD,wBAAgB,iBAAiB,CAC/B,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,GAAG,GAAE,MAAsB,GAC1B,MAAM,CAiBR;AAED,wBAAgB,iBAAiB,CAAC,eAAe,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAM1G;AA0CD,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,SAAmB,GAAG,aAAa,CAoDzG"}
package/dist/cli.js CHANGED
@@ -1,12 +1,126 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __toESM = (mod, isNodeMode, target) => {
9
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
10
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
+ for (let key of __getOwnPropNames(mod))
12
+ if (!__hasOwnProp.call(to, key))
13
+ __defProp(to, key, {
14
+ get: () => mod[key],
15
+ enumerable: true
16
+ });
17
+ return to;
18
+ };
19
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
20
+ var __require = import.meta.require;
3
21
 
4
22
  // src/cli.ts
5
23
  import * as fs from "fs";
6
24
  import * as path from "path";
7
25
  import * as os from "os";
26
+ import { fileURLToPath } from "url";
8
27
  var PLUGIN_NAME = "clikit-plugin";
9
- var VERSION = "0.2.12";
28
+ var VERSION = "0.2.20";
29
+ function getPackageRoot() {
30
+ const currentFile = fileURLToPath(import.meta.url);
31
+ const currentDir = path.dirname(currentFile);
32
+ if (path.basename(currentDir) === "dist") {
33
+ return path.dirname(currentDir);
34
+ }
35
+ return path.dirname(currentDir);
36
+ }
37
+ function getPackageVersion() {
38
+ const packageJsonPath = path.join(getPackageRoot(), "package.json");
39
+ try {
40
+ const raw = fs.readFileSync(packageJsonPath, "utf-8");
41
+ const parsed = JSON.parse(raw);
42
+ return parsed.version || VERSION;
43
+ } catch {
44
+ return VERSION;
45
+ }
46
+ }
47
+ function getPluginEntry() {
48
+ return `${PLUGIN_NAME}@${getPackageVersion()}`;
49
+ }
50
+ function resolveProjectDir(env = process.env, cwd = process.cwd()) {
51
+ const candidates = [env.INIT_CWD, env.PWD, cwd].filter((value) => typeof value === "string" && value.trim().length > 0);
52
+ for (const candidate of candidates) {
53
+ const resolved = path.resolve(candidate);
54
+ try {
55
+ if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
56
+ return resolved;
57
+ }
58
+ } catch {}
59
+ }
60
+ return cwd;
61
+ }
62
+ function upsertPluginEntry(existingPlugins, pluginName, version) {
63
+ const filteredPlugins = existingPlugins.filter((p) => p !== pluginName && !p.startsWith(`${pluginName}@`));
64
+ filteredPlugins.push(`${pluginName}@${version}`);
65
+ return filteredPlugins;
66
+ }
67
+ function copyFileIfMissing(sourcePath, targetPath, stats) {
68
+ if (!fs.existsSync(sourcePath)) {
69
+ stats.missingSources.push(sourcePath);
70
+ return;
71
+ }
72
+ if (fs.existsSync(targetPath)) {
73
+ stats.skipped += 1;
74
+ return;
75
+ }
76
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
77
+ fs.copyFileSync(sourcePath, targetPath);
78
+ stats.copied += 1;
79
+ }
80
+ function copyDirectoryFilesIfMissing(sourceDir, targetDir, stats) {
81
+ if (!fs.existsSync(sourceDir)) {
82
+ stats.missingSources.push(sourceDir);
83
+ return;
84
+ }
85
+ fs.mkdirSync(targetDir, { recursive: true });
86
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
87
+ for (const entry of entries) {
88
+ const sourcePath = path.join(sourceDir, entry.name);
89
+ const targetPath = path.join(targetDir, entry.name);
90
+ if (entry.isDirectory()) {
91
+ copyDirectoryFilesIfMissing(sourcePath, targetPath, stats);
92
+ continue;
93
+ }
94
+ if (entry.isFile()) {
95
+ copyFileIfMissing(sourcePath, targetPath, stats);
96
+ }
97
+ }
98
+ }
99
+ function scaffoldProjectOpencode(projectDir, packageRoot = getPackageRoot()) {
100
+ const projectOpencodeDir = path.join(projectDir, ".opencode");
101
+ const stats = {
102
+ copied: 0,
103
+ skipped: 0,
104
+ missingSources: []
105
+ };
106
+ fs.mkdirSync(projectOpencodeDir, { recursive: true });
107
+ copyFileIfMissing(path.join(packageRoot, "AGENTS.md"), path.join(projectOpencodeDir, "AGENTS.md"), stats);
108
+ copyDirectoryFilesIfMissing(path.join(packageRoot, "command"), path.join(projectOpencodeDir, "command"), stats);
109
+ copyDirectoryFilesIfMissing(path.join(packageRoot, "skill"), path.join(projectOpencodeDir, "skill"), stats);
110
+ copyDirectoryFilesIfMissing(path.join(packageRoot, "memory", "_templates"), path.join(projectOpencodeDir, "memory", "_templates"), stats);
111
+ copyFileIfMissing(path.join(packageRoot, "README.md"), path.join(projectOpencodeDir, "README-clikit.md"), stats);
112
+ const indexPath = path.join(projectOpencodeDir, "index.ts");
113
+ if (!fs.existsSync(indexPath)) {
114
+ fs.writeFileSync(indexPath, `import CliKitPlugin from "${PLUGIN_NAME}";
115
+
116
+ export default CliKitPlugin;
117
+ `);
118
+ stats.copied += 1;
119
+ } else {
120
+ stats.skipped += 1;
121
+ }
122
+ return stats;
123
+ }
10
124
  function getRealHome() {
11
125
  if (process.env.SNAP_REAL_HOME) {
12
126
  return process.env.SNAP_REAL_HOME;
@@ -74,7 +188,8 @@ async function install() {
74
188
  CliKit Installer
75
189
  ================
76
190
  `);
77
- console.log("[1/3] Adding CliKit plugin to OpenCode config...");
191
+ const pluginVersion = getPackageVersion();
192
+ console.log("[1/4] Adding CliKit plugin to OpenCode config...");
78
193
  try {
79
194
  ensureConfigDir();
80
195
  } catch (err) {
@@ -91,22 +206,38 @@ async function install() {
91
206
  return 1;
92
207
  }
93
208
  const config = result.config;
94
- const existingPlugins = Array.isArray(config.plugin) ? config.plugin : [];
209
+ const existingPlugins = Array.isArray(config.plugin) ? config.plugin.filter((p) => typeof p === "string") : [];
95
210
  const preservedKeys = Object.keys(config).filter((k) => k !== "plugin");
96
211
  if (preservedKeys.length > 0) {
97
212
  console.log(` Preserving existing config: ${preservedKeys.join(", ")}`);
98
213
  }
99
- const filteredPlugins = existingPlugins.filter((p) => p !== PLUGIN_NAME && !p.startsWith(`${PLUGIN_NAME}@`));
100
- filteredPlugins.push(PLUGIN_NAME);
214
+ const filteredPlugins = upsertPluginEntry(existingPlugins, PLUGIN_NAME, pluginVersion);
101
215
  const newConfig = { ...config, plugin: filteredPlugins };
102
216
  writeConfig(configPath, newConfig);
103
- console.log(`\u2713 Plugin added to ${configPath}`);
217
+ console.log(`\u2713 Plugin added to ${configPath} as ${getPluginEntry()}`);
104
218
  } catch (err) {
105
219
  console.error(`\u2717 Failed to update OpenCode config: ${err}`);
106
220
  return 1;
107
221
  }
108
222
  console.log(`
109
- [2/3] Creating memory directories...`);
223
+ [2/4] Scaffolding project .opencode assets...`);
224
+ try {
225
+ const projectDir = resolveProjectDir();
226
+ const stats = scaffoldProjectOpencode(projectDir);
227
+ console.log(`\u2713 Project assets ready in ${path.join(projectDir, ".opencode")}`);
228
+ console.log(` Copied: ${stats.copied}, Skipped existing: ${stats.skipped}`);
229
+ if (stats.missingSources.length > 0) {
230
+ console.log(" Missing bundled sources (skipped):");
231
+ for (const missing of stats.missingSources) {
232
+ console.log(` - ${missing}`);
233
+ }
234
+ }
235
+ } catch (err) {
236
+ console.error(`\u2717 Failed to scaffold project assets: ${err}`);
237
+ return 1;
238
+ }
239
+ console.log(`
240
+ [3/4] Creating memory directories...`);
110
241
  const memoryDir = path.join(getConfigDir(), "memory");
111
242
  const memorySubdirs = ["specs", "plans", "research", "reviews", "handoffs", "beads", "prds"];
112
243
  for (const subdir of memorySubdirs) {
@@ -117,7 +248,7 @@ async function install() {
117
248
  }
118
249
  console.log(`\u2713 Memory directories created in ${memoryDir}`);
119
250
  console.log(`
120
- [3/3] Creating CliKit config...`);
251
+ [4/4] Creating CliKit config...`);
121
252
  const clikitConfigPath = path.join(getConfigDir(), "clikit.config.json");
122
253
  if (!fs.existsSync(clikitConfigPath)) {
123
254
  const defaultConfig = {
@@ -165,7 +296,7 @@ Examples:
165
296
  `);
166
297
  }
167
298
  function version() {
168
- console.log(`clikit-plugin v${VERSION}`);
299
+ console.log(`clikit-plugin v${getPackageVersion()}`);
169
300
  }
170
301
  async function main() {
171
302
  const args = process.argv.slice(2);
@@ -193,7 +324,14 @@ async function main() {
193
324
  }
194
325
  process.exit(exitCode);
195
326
  }
196
- main().catch((err) => {
197
- console.error(err);
198
- process.exit(1);
199
- });
327
+ if (import.meta.main) {
328
+ main().catch((err) => {
329
+ console.error(err);
330
+ process.exit(1);
331
+ });
332
+ }
333
+ export {
334
+ upsertPluginEntry,
335
+ scaffoldProjectOpencode,
336
+ resolveProjectDir
337
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.test.d.ts","sourceRoot":"","sources":["../src/cli.test.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"session-notification.d.ts","sourceRoot":"","sources":["../../src/hooks/session-notification.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,UAAU,CAAC;CACzC;AA0BD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAsBtE;AAED,wBAAgB,qBAAqB,CACnC,SAAS,CAAC,EAAE,OAAO,EACnB,MAAM,CAAC,EAAE,MAAM,GACd,mBAAmB,CAUrB;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,OAAO,EACd,SAAS,CAAC,EAAE,OAAO,EACnB,MAAM,CAAC,EAAE,MAAM,GACd,mBAAmB,CAerB;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,mBAAmB,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAIzF"}
1
+ {"version":3,"file":"session-notification.d.ts","sourceRoot":"","sources":["../../src/hooks/session-notification.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,UAAU,CAAC;CACzC;AA4BD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAsBtE;AAED,wBAAgB,qBAAqB,CACnC,SAAS,CAAC,EAAE,OAAO,EACnB,MAAM,CAAC,EAAE,MAAM,GACd,mBAAmB,CAUrB;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,OAAO,EACd,SAAS,CAAC,EAAE,OAAO,EACnB,MAAM,CAAC,EAAE,MAAM,GACd,mBAAmB,CAerB;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,mBAAmB,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAIzF"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=session-notification.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-notification.test.d.ts","sourceRoot":"","sources":["../../src/hooks/session-notification.test.ts"],"names":[],"mappings":""}
package/dist/index.js CHANGED
@@ -4499,15 +4499,17 @@ function escapeSingleQuotes(str2) {
4499
4499
  }
4500
4500
  function getNotifyCommand(payload) {
4501
4501
  const { title, body, urgency } = payload;
4502
- const escapedTitle = title.replace(/"/g, "\\\"");
4503
- const escapedBody = body.replace(/"/g, "\\\"");
4502
+ const safeTitle = typeof title === "string" ? title : "Notification";
4503
+ const safeBody = typeof body === "string" ? body : "";
4504
+ const escapedTitle = safeTitle.replace(/"/g, "\\\"");
4505
+ const escapedBody = safeBody.replace(/"/g, "\\\"");
4504
4506
  switch (process.platform) {
4505
4507
  case "linux":
4506
4508
  return `notify-send "${escapedTitle}" "${escapedBody}" --urgency=${urgency || "normal"}`;
4507
4509
  case "darwin":
4508
4510
  return `osascript -e 'display notification "${escapedBody}" with title "${escapedTitle}"'`;
4509
4511
  case "win32":
4510
- return `powershell -command "[void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); $n = New-Object System.Windows.Forms.NotifyIcon; $n.Icon = [System.Drawing.SystemIcons]::Information; $n.Visible = $true; $n.ShowBalloonTip(5000, '${escapeSingleQuotes(title)}', '${escapeSingleQuotes(body)}', [System.Windows.Forms.ToolTipIcon]::Info)"`;
4512
+ return `powershell -command "[void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); $n = New-Object System.Windows.Forms.NotifyIcon; $n.Icon = [System.Drawing.SystemIcons]::Information; $n.Visible = $true; $n.ShowBalloonTip(5000, '${escapeSingleQuotes(safeTitle)}', '${escapeSingleQuotes(safeBody)}', [System.Windows.Forms.ToolTipIcon]::Info)"`;
4511
4513
  default:
4512
4514
  return null;
4513
4515
  }
@@ -4522,8 +4524,8 @@ function sendNotification(payload) {
4522
4524
  } catch {
4523
4525
  if (process.platform === "linux") {
4524
4526
  try {
4525
- const wslTitle = escapeSingleQuotes(payload.title);
4526
- const wslBody = escapeSingleQuotes(payload.body);
4527
+ const wslTitle = escapeSingleQuotes(typeof payload.title === "string" ? payload.title : "Notification");
4528
+ const wslBody = escapeSingleQuotes(typeof payload.body === "string" ? payload.body : "");
4527
4529
  const wslCmd = `powershell.exe -command "[void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); $n = New-Object System.Windows.Forms.NotifyIcon; $n.Icon = [System.Drawing.SystemIcons]::Information; $n.Visible = $true; $n.ShowBalloonTip(5000, '${wslTitle}', '${wslBody}', [System.Windows.Forms.ToolTipIcon]::Info)"`;
4528
4530
  execSync4(wslCmd, { timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
4529
4531
  return true;
@@ -62,6 +62,7 @@ dist/index.js ← import.meta.dir points here
62
62
  | 0.2.10 | `__dirname` → `import.meta.dir` (paths still wrong) |
63
63
  | 0.2.11 | Correct relative paths from `dist/` directory |
64
64
  | 0.2.19 | Retry `getObservationsByType` when SQLite reports datatype mismatch |
65
+ | 0.2.20 | Guard `memoryRead` against non-string paths |
65
66
 
66
67
  ---
67
68
 
@@ -0,0 +1,195 @@
1
+ # Implementation Plan: Align CliKit Plugin Installation with OpenCode
2
+
3
+ **Date:** 2026-02-16
4
+ **Author:** Plan Agent
5
+ **Status:** Draft
6
+ **bead_id:** clikit-plugin
7
+
8
+ ---
9
+
10
+ ## Overview
11
+
12
+ Define and implement an installation and configuration workflow for CliKit that mirrors Oh My OpenCode’s opinionated setup. This includes documenting the desired behavior, creating a CLI installer that automates plugin registration and config scaffolding, and updating guidance so users can reliably install via npm/local files following OpenCode’s documented plugin system.
13
+
14
+ ## References
15
+
16
+ - **Spec:** *(not provided)*
17
+ - **PRD:** *(not provided)*
18
+ - **Research:** `.opencode/memory/research/2026-02-16-opencode-plugin-alignment.md` *(planned; current knowledge from repo/docs review)*
19
+
20
+ ---
21
+
22
+ ## Tasks
23
+
24
+ ### Task 1: Capture Target Installation Workflow
25
+
26
+ | Field | Value |
27
+ |-------|-------|
28
+ | **task_id** | T-001 |
29
+ | **type** | task |
30
+ | **assignee** | build |
31
+ | **effort** | S |
32
+ | **priority** | P1 |
33
+ | **status** | not_started |
34
+ | **dependencies** | none |
35
+
36
+ **Description:**
37
+ Summarize Oh My OpenCode’s installation behavior (CLI prompts, subscription flags, config injection) and map each requirement to CliKit. Produce a short design note describing which steps we will replicate and which remain optional.
38
+
39
+ **Input:**
40
+ - README + docs from `code-yeongyu/oh-my-opencode`
41
+ - OpenCode plugin docs (`opencode.ai/docs/plugins`)
42
+
43
+ **Output:**
44
+ - Design note (e.g., `.opencode/memory/research/2026-02-16-opencode-plugin-alignment.md`)
45
+ - Checklist of actions installer must perform
46
+
47
+ **Acceptance Criteria:**
48
+ - [ ] AC-01: Clearly list each step Oh My OpenCode installer performs.
49
+ - [ ] AC-02: Define corresponding CliKit behavior (adopt/skip) with rationale.
50
+
51
+ **Boundaries:**
52
+ - Do not implement code changes here—documentation/research only.
53
+
54
+ ---
55
+
56
+ ### Task 2: Implement CliKit Installer CLI
57
+
58
+ | Field | Value |
59
+ |-------|-------|
60
+ | **task_id** | T-002 |
61
+ | **type** | feature |
62
+ | **assignee** | build |
63
+ | **effort** | M |
64
+ | **priority** | P0 |
65
+ | **status** | not_started |
66
+ | **dependencies** | [T-001] |
67
+
68
+ **Description:**
69
+ Create a `bunx clikit-plugin install` command paralleling Oh My OpenCode: accept provider flags, update `opencode.json` plugin array, scaffold `.opencode/clikit.config.json`, and emit post-install instructions. Include validation (prevent duplicates) and support non-interactive defaults.
70
+
71
+ **Input:**
72
+ - Output of Task 1 design note
73
+ - Existing `.opencode/plugin.ts`, `.opencode/src/config.ts`
74
+
75
+ **Output:**
76
+ - CLI script (likely under `.opencode/script/install.ts` or package bin entry)
77
+ - Updated package metadata so command is published
78
+ - Tests or manual verification steps documenting expected behavior
79
+
80
+ **Acceptance Criteria:**
81
+ - [ ] AC-01: Running installer registers CliKit in `plugin` array when absent and is idempotent.
82
+ - [ ] AC-02: Installer writes/merges config template without overwriting user overrides.
83
+ - [ ] AC-03: Help text documents supported flags modeled after Oh My OpenCode.
84
+
85
+ **Boundaries:**
86
+ - Do not modify runtime hook logic; focus on installer.
87
+
88
+ ---
89
+
90
+ ### Task 3: Update Documentation and Verification Guide
91
+
92
+ | Field | Value |
93
+ |-------|-------|
94
+ | **task_id** | T-003 |
95
+ | **type** | chore |
96
+ | **assignee** | build |
97
+ | **effort** | S |
98
+ | **priority** | P1 |
99
+ | **status** | not_started |
100
+ | **dependencies** | [T-002] |
101
+
102
+ **Description:**
103
+ Revise `.opencode/README.md` and root `README.md` to outline installation paths (npm + installer, manual config). Include verification steps (opencode version check, config inspection) and troubleshooting tips inspired by Oh My OpenCode docs.
104
+
105
+ **Input:**
106
+ - Completed installer features
107
+ - Task 1 research checklist
108
+
109
+ **Output:**
110
+ - Updated documentation referencing new CLI command, load order, and auth reminders
111
+
112
+ **Acceptance Criteria:**
113
+ - [ ] AC-01: README contains explicit step-by-step instructions mirroring OpenCode plugin docs.
114
+ - [ ] AC-02: Adds warning about not disabling core features unless required.
115
+ - [ ] AC-03: Describes verification commands post-install.
116
+
117
+ **Boundaries:**
118
+ - Do not duplicate entire Oh My OpenCode README—only relevant sections.
119
+
120
+ ---
121
+
122
+ ## File Impact
123
+
124
+ **Files to CREATE:**
125
+ - `.opencode/memory/research/2026-02-16-opencode-plugin-alignment.md` — research note from Task 1
126
+ - `script/install.ts` (or equivalent CLI entry) — new installer command logic
127
+
128
+ **Files to MODIFY:**
129
+ - `package.json` — add bin entry & dependencies for installer
130
+ - `.opencode/README.md` — refreshed installation instructions
131
+ - `README.md` — high-level install overview referencing CLI
132
+ - `.opencode/src/config.ts` — ensure config loader supports templates/merging if needed by installer
133
+ - `.opencode/script/` helpers (if reused) — support CLI utilities
134
+
135
+ **Files to DELETE:**
136
+ - (none)
137
+
138
+ ---
139
+
140
+ ## Dependencies
141
+
142
+ ```mermaid
143
+ graph TD
144
+ T001[T-001: Capture Target Installation Workflow] --> T002[T-002: Implement CliKit Installer CLI]
145
+ T002 --> T003[T-003: Update Documentation and Verification Guide]
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Effort Summary
151
+
152
+ | Task | Effort | Priority |
153
+ |------|--------|----------|
154
+ | T-001 | S | P1 |
155
+ | T-002 | M | P0 |
156
+ | T-003 | S | P1 |
157
+
158
+ **Total Estimated Effort:** ~3-4 days (including testing & docs)
159
+
160
+ ---
161
+
162
+ ## Risk Assessment
163
+
164
+ | Risk | Probability | Impact | Mitigation |
165
+ |------|-------------|--------|------------|
166
+ | Installer overwrites user config | M | M | Implement merge logic with backups + prompts |
167
+ | CLI divergence from OpenCode expectations | M | H | Validate against official plugin docs; add tests for load order |
168
+ | Users skip auth steps leading to failures | H | M | Add prominent warnings & post-install checklist |
169
+
170
+ ---
171
+
172
+ ## Quick Mode Eligibility
173
+
174
+ Tasks eligible for Quick Mode (no full plan needed):
175
+ - [ ] T-001 (requires research + documentation > 3 files)
176
+ - [ ] T-002 (installer touches multiple files, so not eligible)
177
+ - [ ] T-003 (docs updates only but depends on T-002 completion)
178
+
179
+ ---
180
+
181
+ ## Rollout Plan
182
+
183
+ | Phase | Tasks | Milestone |
184
+ |-------|-------|-----------|
185
+ | Phase 1 | T-001 | Approved installation blueprint |
186
+ | Phase 2 | T-002 | Installer CLI ready & manually verified |
187
+ | Phase 3 | T-003 | Documentation updated & user walkthrough published |
188
+
189
+ ---
190
+
191
+ ## Approval
192
+
193
+ - [ ] Plan reviewed
194
+ - [ ] User approved
195
+ - [ ] Ready for implementation
@@ -0,0 +1,128 @@
1
+ ---
2
+ topic: "Oh My OpenCode Installer Behavior"
3
+ date: 2026-02-16
4
+ confidence: high
5
+ depth: deep
6
+ versions:
7
+ - library: "oh-my-opencode"
8
+ version: "3.5.5"
9
+ bead_id: clikit-plugin
10
+ ---
11
+
12
+ # Research: Oh My OpenCode Installer
13
+
14
+ **Question:** How does oh-my-opencode's installer configure OpenCode and what pieces should CliKit replicate?
15
+
16
+ ---
17
+
18
+ ## Summary
19
+
20
+ Oh My OpenCode's CLI installer (`bunx oh-my-opencode install`) performs a six-step workflow: it validates CLI args, checks for the OpenCode binary/version, ensures the plugin entry exists in `opencode.json`, optionally installs auth/provider configs (Gemini et al.), writes/merges a dedicated `oh-my-opencode` config file, and prints guidance/warnings. Config files live under both the global config dir (`~/.config/opencode`) and per-project `.opencode`. CliKit's installer should mirror the config detection/merge logic and idempotent plugin registration.
21
+
22
+ ---
23
+
24
+ ## Key Findings
25
+
26
+ ### Finding 1: Installer Flow Mirrors Documented Steps
27
+ - `runCliInstaller` (dist/cli/index.js) orchestrates 6 numbered steps: detect OpenCode binary/version, add plugin entry, add auth plugins (conditional), add provider config (conditional), and write final `oh-my-opencode` config before printing summary boxes.
28
+ - Non-interactive mode requires explicit provider flags (`--claude`, `--gemini`, `--copilot`, etc.); otherwise TUI handles prompts.
29
+ - Failure at any sub-step aborts with contextual error text (`formatErrorWithSuggestion`).
30
+
31
+ ### Finding 2: Plugin Registration Logic is Idempotent
32
+ - `addPluginToOpenCodeConfig(version)` ensures `~/.config/opencode/opencode.json[c]` exists, parses JSON/JSONC, removes prior `oh-my-opencode` entries, and writes the latest pinned version string (e.g., `oh-my-opencode@3.5.5`).
33
+ - JSONC files are updated by regex replacement to preserve comments/formatting; JSON files are re-serialized.
34
+ - This mirrors what CliKit's installer must do for `clikit-plugin`, including detection of config dir via `getOpenCodeConfigDir` semantics.
35
+
36
+ ### Finding 3: Config Merge Targets Global + Project Paths
37
+ - `writeOmoConfig` writes `oh-my-opencode.json` (or `.jsonc`) inside `~/.config/opencode/oh-my-opencode.*`, using `deepMergeRecord` against existing JSONC.
38
+ - Loader functions (e.g., `loadPluginConfig`) look at both global config dir and per-project `.opencode/oh-my-opencode.*`, merging project overrides atop global defaults.
39
+ - Ensuring CliKit's installer writes `~/.config/opencode/clikit.config.json` (global) while respecting `.opencode/clikit.config.json` matches OpenCode expectations.
40
+
41
+ ### Finding 4: Ancillary Setup (Auth, Providers, bun install)
42
+ - When Gemini support is requested, installer calls `addAuthPlugins` and `addProviderConfig` to inject additional packages/config references.
43
+ - After plugin or binary updates, oh-my-opencode optionally runs `bun install` inside the config dir to ensure dependencies exist (`runBunInstall`).
44
+ - CliKit may not need auth/provider hooks yet but should adopt the same structure for future flags, and consider invoking `bun install` when copying template files that introduce dependencies.
45
+
46
+ ---
47
+
48
+ ## Comparison
49
+
50
+ | Aspect | Oh My OpenCode | Current CliKit | Gap |
51
+ |--------|----------------|----------------|-----|
52
+ | Plugin registration | Adds/updates versioned entry in `plugin` array with JSONC-safe merge | Adds bare `clikit-plugin` without version, no JSONC handling | Need versioned entry + JSONC-safe edits |
53
+ | Config scaffolding | Writes `oh-my-opencode.json` w/ deep merge fallback | Writes `clikit.config.json` only if missing, no merge | Implement deep merge + JSONC parse guard |
54
+ | Provider/auth flags | Supports `--claude`, `--gemini`, `--copilot`, `--opencode-zen`, etc. | No flags | Evaluate minimal flag set / defaults |
55
+ | Dependency setup | Runs `bun install` when necessary | None | Optional follow-up |
56
+ | Project overrides | Loader merges global + `.opencode/oh-my-opencode.*` | Loader already merges `~/.config` + project `.opencode` | Already aligned |
57
+
58
+ ---
59
+
60
+ ## Code Examples
61
+
62
+ ### Example 1: Adding Plugin Entry (pseudo-extracted)
63
+ ```ts
64
+ const pluginEntry = await getPluginNameWithVersion(currentVersion);
65
+ const parseResult = parseOpenCodeConfigFileWithError(path);
66
+ const plugins = (parseResult.config.plugin ?? []).filter(
67
+ (p) => !p.startsWith("oh-my-opencode")
68
+ );
69
+ plugins.push(pluginEntry);
70
+ writeConfigPreservingFormat(path, plugins);
71
+ ```
72
+
73
+ ### Example 2: Writing Config with Deep Merge
74
+ ```ts
75
+ const omoConfigPath = getOmoConfigPath();
76
+ const newConfig = generateOmoConfig(installConfig);
77
+ if (existsSync(omoConfigPath)) {
78
+ const existing = parseJsonc(readFileSync(omoConfigPath, "utf8"));
79
+ const merged = deepMergeRecord(existing, newConfig);
80
+ writeFileSync(omoConfigPath, JSON.stringify(merged, null, 2));
81
+ } else {
82
+ writeFileSync(omoConfigPath, JSON.stringify(newConfig, null, 2));
83
+ }
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Recommendation
89
+
90
+ **Recommended approach:** Reimplement CliKit's installer to follow Oh My OpenCode's structure.
91
+
92
+ **Rationale:**
93
+ 1. Users already expect the oh-my-opencode workflow; mirroring it reduces friction.
94
+ 2. JSONC-safe merging and versioned plugin entries prevent config corruption and enable updates.
95
+ 3. Aligning directories and merge logic ensures compatibility with OpenCode's plugin loader (global + project overrides).
96
+
97
+ ---
98
+
99
+ ## Verification Steps
100
+
101
+ - [ ] Validate installer on machines with `opencode.json` and `opencode.jsonc` variants.
102
+ - [ ] Confirm repeated `bun x clikit-plugin install` leaves config unchanged and updates version.
103
+ - [ ] Ensure `.opencode/clikit.config.json` overrides global values after install.
104
+
105
+ ---
106
+
107
+ ## Open Questions
108
+
109
+ - [ ] Which provider flags should CliKit expose initially (Claude-only vs. multi-provider)?
110
+ - [ ] Do we need to run `bun install` inside `~/.config/opencode` after copying CliKit assets?
111
+
112
+ ---
113
+
114
+ ## Sources
115
+
116
+ | Source | Type | Reliability |
117
+ |--------|------|-------------|
118
+ | `oh-my-opencode@3.5.5` package (`dist/cli/index.js`, `postinstall.mjs`) | Source | High |
119
+ | OpenCode config conventions from oh-my-opencode loader | Source | High |
120
+
121
+ ---
122
+
123
+ ## Version Notes
124
+
125
+ | Library/Tool | Version Researched | Current Latest | Notes |
126
+ |--------------|-------------------|----------------|-------|
127
+ | oh-my-opencode | 3.5.5 | 3.5.5 | Latest on npm during research |
128
+ | clikit-plugin | 0.2.20 | 0.2.20 | Installer work in progress |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clikit-plugin",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
4
4
  "description": "OpenCode plugin with 10 agents, 19 commands, 48 skills, 14 hooks",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,7 +25,7 @@
25
25
  "README.md"
26
26
  ],
27
27
  "scripts": {
28
- "build": "bun build src/index.ts src/cli.ts --outdir dist --target bun --format esm && tsc --emitDeclarationOnly && cp src/clikit.schema.json dist/",
28
+ "build": "bun build src/index.ts src/cli.ts --outdir dist --target bun --format esm && tsc --emitDeclarationOnly && cp src/clikit.schema.json dist/ 2>/dev/null || true",
29
29
  "clean": "rm -rf dist",
30
30
  "prepublishOnly": "bun run clean && bun run build",
31
31
  "typecheck": "tsc --noEmit",
@@ -1,423 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "title": "CliKit Plugin Configuration",
4
- "description": "Configuration for CliKit OpenCode plugin",
5
- "type": "object",
6
- "properties": {
7
- "disabled_agents": {
8
- "type": "array",
9
- "items": { "type": "string" },
10
- "description": "List of agent names to disable",
11
- "examples": [["scout"]]
12
- },
13
- "disabled_commands": {
14
- "type": "array",
15
- "items": { "type": "string" },
16
- "description": "List of command names to disable",
17
- "examples": [["debug", "import-plan"]]
18
- },
19
- "agents": {
20
- "type": "object",
21
- "description": "Override settings for specific agents",
22
- "additionalProperties": {
23
- "type": "object",
24
- "properties": {
25
- "model": { "type": "string", "description": "Override model for this agent" },
26
- "temperature": { "type": "number", "minimum": 0, "maximum": 2 },
27
- "top_p": { "type": "number", "minimum": 0, "maximum": 1 },
28
- "mode": { "enum": ["subagent", "primary", "all"] },
29
- "disabled": { "type": "boolean", "description": "Disable this agent" },
30
- "color": { "type": "string", "description": "Agent color in UI" },
31
- "maxSteps": { "type": "integer", "minimum": 1, "description": "Maximum steps for agent" },
32
- "tools": {
33
- "type": "object",
34
- "additionalProperties": { "type": "boolean" }
35
- },
36
- "permission": {
37
- "type": "object",
38
- "description": "Permission overrides for this agent",
39
- "properties": {
40
- "edit": { "enum": ["ask", "allow", "deny"] },
41
- "bash": {
42
- "oneOf": [
43
- { "enum": ["ask", "allow", "deny"] },
44
- { "type": "object", "additionalProperties": { "enum": ["ask", "allow", "deny"] } }
45
- ]
46
- },
47
- "webfetch": { "enum": ["ask", "allow", "deny"] },
48
- "doom_loop": { "enum": ["ask", "allow", "deny"] },
49
- "external_directory": { "enum": ["ask", "allow", "deny"] }
50
- }
51
- }
52
- }
53
- }
54
- },
55
- "commands": {
56
- "type": "object",
57
- "description": "Override settings for specific commands",
58
- "additionalProperties": {
59
- "type": "object",
60
- "properties": {
61
- "agent": { "type": "string", "description": "Agent to use for this command" },
62
- "model": { "type": "string", "description": "Model to use for this command" },
63
- "subtask": { "type": "boolean", "description": "Run as subtask" },
64
- "description": { "type": "string", "description": "Command description" },
65
- "template": { "type": "string", "description": "Command template/prompt" }
66
- }
67
- }
68
- },
69
- "lsp": {
70
- "type": "object",
71
- "description": "LSP server configurations to inject into OpenCode",
72
- "additionalProperties": {
73
- "type": "object",
74
- "required": ["command"],
75
- "properties": {
76
- "command": {
77
- "type": "array",
78
- "items": { "type": "string" },
79
- "description": "Command and arguments to start the LSP server"
80
- },
81
- "extensions": {
82
- "type": "array",
83
- "items": { "type": "string" },
84
- "description": "File extensions this LSP handles (e.g., ['.ts', '.tsx'])"
85
- },
86
- "priority": { "type": "integer", "description": "Priority when multiple LSPs match (higher = preferred)" },
87
- "disabled": { "type": "boolean", "description": "Disable this LSP server" },
88
- "env": {
89
- "type": "object",
90
- "additionalProperties": { "type": "string" },
91
- "description": "Environment variables for the LSP process"
92
- },
93
- "initialization": {
94
- "type": "object",
95
- "description": "LSP initialization options"
96
- }
97
- }
98
- },
99
- "examples": [{
100
- "typescript": {
101
- "command": ["typescript-language-server", "--stdio"],
102
- "extensions": [".ts", ".tsx", ".js", ".jsx"]
103
- }
104
- }]
105
- },
106
- "hooks": {
107
- "type": "object",
108
- "properties": {
109
- "session_logging": {
110
- "type": "boolean",
111
- "default": true,
112
- "description": "Enable session lifecycle logging"
113
- },
114
- "tool_logging": {
115
- "type": "boolean",
116
- "default": false,
117
- "description": "Enable tool execution logging"
118
- },
119
- "todo_enforcer": {
120
- "type": "object",
121
- "description": "Todo continuation enforcer - warns when todos are incomplete",
122
- "properties": {
123
- "enabled": {
124
- "type": "boolean",
125
- "default": true,
126
- "description": "Enable todo completion checking"
127
- },
128
- "warn_on_incomplete": {
129
- "type": "boolean",
130
- "default": true,
131
- "description": "Log warning when todos are incomplete"
132
- }
133
- }
134
- },
135
- "empty_message_sanitizer": {
136
- "type": "object",
137
- "description": "Empty message sanitizer - prevents API errors from empty outputs",
138
- "properties": {
139
- "enabled": {
140
- "type": "boolean",
141
- "default": true,
142
- "description": "Enable empty message sanitization"
143
- },
144
- "log_empty": {
145
- "type": "boolean",
146
- "default": true,
147
- "description": "Log when empty messages are detected"
148
- },
149
- "placeholder": {
150
- "type": "string",
151
- "default": "(No output)",
152
- "description": "Placeholder text for empty outputs"
153
- }
154
- }
155
- },
156
- "git_guard": {
157
- "type": "object",
158
- "description": "Blocks dangerous git commands (force push, hard reset, rm -rf)",
159
- "properties": {
160
- "enabled": {
161
- "type": "boolean",
162
- "default": true,
163
- "description": "Enable git command guard"
164
- },
165
- "allow_force_with_lease": {
166
- "type": "boolean",
167
- "default": true,
168
- "description": "Allow --force-with-lease as a safer alternative"
169
- }
170
- }
171
- },
172
- "security_check": {
173
- "type": "object",
174
- "description": "Scans for secrets and credentials before git commits",
175
- "properties": {
176
- "enabled": {
177
- "type": "boolean",
178
- "default": true,
179
- "description": "Enable secret scanning"
180
- },
181
- "block_commits": {
182
- "type": "boolean",
183
- "default": false,
184
- "description": "Block commits when secrets are detected (vs. warn only)"
185
- }
186
- }
187
- },
188
- "subagent_question_blocker": {
189
- "type": "object",
190
- "description": "Prevents subagents from asking clarifying questions",
191
- "properties": {
192
- "enabled": {
193
- "type": "boolean",
194
- "default": true,
195
- "description": "Enable subagent question blocking"
196
- }
197
- }
198
- },
199
- "comment_checker": {
200
- "type": "object",
201
- "description": "Detects excessive AI-generated comments in code",
202
- "properties": {
203
- "enabled": {
204
- "type": "boolean",
205
- "default": true,
206
- "description": "Enable comment density checking"
207
- },
208
- "threshold": {
209
- "type": "number",
210
- "default": 0.3,
211
- "minimum": 0,
212
- "maximum": 1,
213
- "description": "Comment-to-code ratio threshold (0.3 = 30%)"
214
- }
215
- }
216
- },
217
- "env_context": {
218
- "type": "object",
219
- "description": "Injects project structure, git branch, and build system info",
220
- "properties": {
221
- "enabled": {
222
- "type": "boolean",
223
- "default": true,
224
- "description": "Enable environment context injection"
225
- },
226
- "include_git": {
227
- "type": "boolean",
228
- "default": true,
229
- "description": "Include git branch and status"
230
- },
231
- "include_package": {
232
- "type": "boolean",
233
- "default": true,
234
- "description": "Include package.json info"
235
- },
236
- "include_structure": {
237
- "type": "boolean",
238
- "default": true,
239
- "description": "Include project directory structure"
240
- },
241
- "max_depth": {
242
- "type": "integer",
243
- "default": 2,
244
- "minimum": 1,
245
- "maximum": 5,
246
- "description": "Max depth for directory structure scan"
247
- }
248
- }
249
- },
250
- "auto_format": {
251
- "type": "object",
252
- "description": "Runs project formatter after file edits (prettier, biome, dprint)",
253
- "properties": {
254
- "enabled": {
255
- "type": "boolean",
256
- "default": false,
257
- "description": "Enable auto-formatting (off by default)"
258
- },
259
- "formatter": {
260
- "type": "string",
261
- "enum": ["prettier", "biome", "dprint"],
262
- "description": "Override formatter (auto-detected if not set)"
263
- },
264
- "extensions": {
265
- "type": "array",
266
- "items": { "type": "string" },
267
- "description": "File extensions to format (e.g., ['.ts', '.tsx'])"
268
- },
269
- "log": {
270
- "type": "boolean",
271
- "default": true,
272
- "description": "Log format operations"
273
- }
274
- }
275
- },
276
- "typecheck_gate": {
277
- "type": "object",
278
- "description": "Runs TypeScript type checking after .ts/.tsx file edits",
279
- "properties": {
280
- "enabled": {
281
- "type": "boolean",
282
- "default": false,
283
- "description": "Enable typecheck gate (off by default)"
284
- },
285
- "tsconfig": {
286
- "type": "string",
287
- "description": "Path to tsconfig.json (auto-detected if not set)"
288
- },
289
- "log": {
290
- "type": "boolean",
291
- "default": true,
292
- "description": "Log typecheck results"
293
- },
294
- "block_on_error": {
295
- "type": "boolean",
296
- "default": false,
297
- "description": "Block file writes when type errors are found"
298
- }
299
- }
300
- },
301
- "session_notification": {
302
- "type": "object",
303
- "description": "Desktop notifications when sessions complete or error",
304
- "properties": {
305
- "enabled": {
306
- "type": "boolean",
307
- "default": true,
308
- "description": "Enable desktop notifications"
309
- },
310
- "on_idle": {
311
- "type": "boolean",
312
- "default": true,
313
- "description": "Notify when session goes idle"
314
- },
315
- "on_error": {
316
- "type": "boolean",
317
- "default": true,
318
- "description": "Notify on session errors"
319
- },
320
- "title_prefix": {
321
- "type": "string",
322
- "default": "OpenCode",
323
- "description": "Prefix for notification titles"
324
- }
325
- }
326
- },
327
- "truncator": {
328
- "type": "object",
329
- "description": "Dynamic output truncation to prevent context overflow",
330
- "properties": {
331
- "enabled": {
332
- "type": "boolean",
333
- "default": true,
334
- "description": "Enable output truncation"
335
- },
336
- "max_output_chars": {
337
- "type": "integer",
338
- "default": 30000,
339
- "description": "Maximum output characters before truncation"
340
- },
341
- "max_output_lines": {
342
- "type": "integer",
343
- "default": 500,
344
- "description": "Maximum output lines before truncation"
345
- },
346
- "preserve_head_lines": {
347
- "type": "integer",
348
- "default": 50,
349
- "description": "Lines to preserve from the start"
350
- },
351
- "preserve_tail_lines": {
352
- "type": "integer",
353
- "default": 50,
354
- "description": "Lines to preserve from the end"
355
- },
356
- "log": {
357
- "type": "boolean",
358
- "default": true,
359
- "description": "Log truncation events"
360
- }
361
- }
362
- },
363
- "compaction": {
364
- "type": "object",
365
- "description": "Preserves beads state and memory during context compaction",
366
- "properties": {
367
- "enabled": {
368
- "type": "boolean",
369
- "default": true,
370
- "description": "Enable compaction state preservation"
371
- },
372
- "include_beads_state": {
373
- "type": "boolean",
374
- "default": true,
375
- "description": "Include beads-village task state"
376
- },
377
- "include_memory_refs": {
378
- "type": "boolean",
379
- "default": true,
380
- "description": "Include recent memory references"
381
- },
382
- "include_todo_state": {
383
- "type": "boolean",
384
- "default": true,
385
- "description": "Include current todo list state"
386
- },
387
- "max_state_chars": {
388
- "type": "integer",
389
- "default": 5000,
390
- "description": "Maximum characters for compaction context"
391
- }
392
- }
393
- },
394
- "swarm_enforcer": {
395
- "type": "object",
396
- "description": "Enforces task isolation for multi-agent swarms",
397
- "properties": {
398
- "enabled": {
399
- "type": "boolean",
400
- "default": false,
401
- "description": "Enable swarm enforcement (off by default)"
402
- },
403
- "strict_file_locking": {
404
- "type": "boolean",
405
- "default": true,
406
- "description": "Only allow edits to reserved files"
407
- },
408
- "block_unreserved_edits": {
409
- "type": "boolean",
410
- "default": false,
411
- "description": "Block edits to unreserved files (vs. warn)"
412
- },
413
- "log": {
414
- "type": "boolean",
415
- "default": true,
416
- "description": "Log enforcement decisions"
417
- }
418
- }
419
- }
420
- }
421
- }
422
- }
423
- }