@cloudy-app/opencode-plugin 0.0.1

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 ADDED
@@ -0,0 +1,129 @@
1
+ # @cloudy-app/opencode-plugin
2
+
3
+ OpenCode plugin for Cloudy — idea management tools with automatic session tracking and file protection.
4
+
5
+ ## Features
6
+
7
+ - **Idea CRUD tools** — `idea_list`, `idea_create`, `idea_update`, `idea_remove` exposed as OpenCode tools
8
+ - **File protection** — prevents destructive commands on the idea directory
9
+ - **Auto touch** — automatically syncs idea metadata when idea files are written/edited
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ # bun (recommended)
15
+ bun add @cloudy-app/opencode-plugin
16
+
17
+ # npm
18
+ npm install @cloudy-app/opencode-plugin
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Add the plugin to your `opencode.json`:
24
+
25
+ ```json
26
+ {
27
+ "$schema": "https://opencode.ai/config.json",
28
+ "plugin": ["@cloudy-app/opencode-plugin"]
29
+ }
30
+ ```
31
+
32
+ ### Environment Variables
33
+
34
+ | Variable | Default | Description |
35
+ | ---------------------------- | ----------------------- | ------------------------------- |
36
+ | `CLOUDY_ASSISTANT_BASE_PATH` | `./base-path` | Base directory for idea storage |
37
+ | `CLOUDY_API_BASE_PATH` | `http://localhost:3000` | Cloudy API server URL |
38
+
39
+ ### Tools
40
+
41
+ | Tool | Description |
42
+ | ------------- | ----------------------------------------------------------------- |
43
+ | `idea_list` | List ideas with optional filters (status, priority, tags, search) |
44
+ | `idea_create` | Create a new idea with title and metadata |
45
+ | `idea_update` | Update idea metadata (title, tags, status, priority) |
46
+ | `idea_remove` | Delete an idea by path |
47
+
48
+ ### Guards
49
+
50
+ The plugin automatically blocks:
51
+
52
+ - Destructive bash commands targeting the idea directory (`rm`, `mv`, `cp`, etc.)
53
+ - Overwriting `index.md` inside idea directories
54
+
55
+ ## Development
56
+
57
+ ```bash
58
+ bun install
59
+
60
+ # Build
61
+ bun run build
62
+
63
+ # Test
64
+ bun test
65
+
66
+ # Lint
67
+ bun run lint:write
68
+ ```
69
+
70
+ ## Testing Locally Before Publish
71
+
72
+ ### Option 1: `bun link` (recommended)
73
+
74
+ ```bash
75
+ # Setup — build + create global link
76
+ bun run build
77
+ bun link
78
+
79
+ # Use in another project
80
+ bun link @cloudy-app/opencode-plugin
81
+
82
+ # Cleanup when done
83
+ bun unlink @cloudy-app/opencode-plugin
84
+ ```
85
+
86
+ ### Option 2: Direct tarball
87
+
88
+ ```bash
89
+ bun run build
90
+ npm pack --dry-run
91
+ ```
92
+
93
+ ## Publishing
94
+
95
+ ### Prerequisites
96
+
97
+ - npm account with publish permission to `@cloudy` org
98
+ - Logged in via `npm login`
99
+ - All changes committed and pushed
100
+
101
+ ### Steps
102
+
103
+ ```bash
104
+ # 1. Build and test
105
+ bun run build
106
+ bun test
107
+
108
+ # 2. Verify the tarball contents
109
+ npm pack --dry-run
110
+
111
+ # 3. Bump version
112
+ npm version patch # 1.0.0 → 1.0.1
113
+ npm version minor # 1.0.0 → 1.1.0
114
+ npm version major # 1.0.0 → 2.0.0
115
+
116
+ # 4. Publish
117
+ npm publish
118
+ ```
119
+
120
+ ### CI/CD (Optional)
121
+
122
+ ```yaml
123
+ - run: bun install
124
+ - run: bun run build
125
+ - run: bun test
126
+ - run: npm publish
127
+ env:
128
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
129
+ ```
@@ -0,0 +1,10 @@
1
+ import { Plugin, ToolDefinition } from '@opencode-ai/plugin';
2
+
3
+ declare const IdeaPlugin: Plugin;
4
+
5
+ declare const list: ToolDefinition;
6
+ declare const create: ToolDefinition;
7
+ declare const update: ToolDefinition;
8
+ declare const remove: ToolDefinition;
9
+
10
+ export { IdeaPlugin, create, list, remove, update };
package/dist/index.js ADDED
@@ -0,0 +1,188 @@
1
+ // src/tools/idea.ts
2
+ import { tool } from "@opencode-ai/plugin";
3
+
4
+ // src/lib/env.ts
5
+ var env = {
6
+ get CLOUDY_ASSISTANT_BASE_PATH() {
7
+ return process.env.CLOUDY_ASSISTANT_BASE_PATH || "./base-path";
8
+ },
9
+ get CLOUDY_API_BASE_PATH() {
10
+ return process.env.CLOUDY_ASSISTANT_BASE_PATH || "http://localhost:3000";
11
+ }
12
+ };
13
+
14
+ // src/lib/api.ts
15
+ async function ideaApi(path2, options) {
16
+ const res = await fetch(`${env.CLOUDY_API_BASE_PATH}/api/idea${path2}`, {
17
+ headers: { "Content-Type": "application/json" },
18
+ ...options
19
+ });
20
+ if (!res.ok) {
21
+ throw new Error(`API error ${res.status}: ${await res.text()}`);
22
+ }
23
+ return res.json();
24
+ }
25
+ async function listIdeas(query) {
26
+ return ideaApi(query ? `?${query}` : "");
27
+ }
28
+ async function createIdea(body) {
29
+ return ideaApi("", { method: "POST", body: JSON.stringify(body) });
30
+ }
31
+ async function updateIdeaMeta(ideaPath, body) {
32
+ return ideaApi(`/${encodeURIComponent(ideaPath)}`, {
33
+ method: "PATCH",
34
+ body: JSON.stringify(body)
35
+ });
36
+ }
37
+ async function deleteIdea(ideaPath) {
38
+ return ideaApi(`/${encodeURIComponent(ideaPath)}`, { method: "DELETE" });
39
+ }
40
+ async function touchIdea(ideaPath) {
41
+ return ideaApi(`/${encodeURIComponent(ideaPath)}/touch`, { method: "PATCH" });
42
+ }
43
+
44
+ // src/tools/idea.ts
45
+ var list = tool({
46
+ description: "List all ideas with optional filters. Returns idea path, title, status, priority, tags, and content.",
47
+ args: {
48
+ q: tool.schema.string().optional().describe("Search query to filter ideas by title/content"),
49
+ status: tool.schema.enum(["draft", "in-progress", "completed", "archived"]).optional().describe("Filter by status"),
50
+ priority: tool.schema.enum(["low", "medium", "high"]).optional().describe("Filter by priority"),
51
+ tags: tool.schema.array(tool.schema.string()).optional().describe("Filter by tags")
52
+ },
53
+ async execute(args) {
54
+ const params = new URLSearchParams();
55
+ if (args.q) params.set("q", args.q);
56
+ if (args.status) params.set("status", args.status);
57
+ if (args.priority) params.set("priority", args.priority);
58
+ if (args.tags?.length) args.tags.forEach((t) => params.append("tags", t));
59
+ const ideas = await listIdeas(params.toString());
60
+ return JSON.stringify(ideas, null, 2);
61
+ }
62
+ });
63
+ var create = tool({
64
+ description: "Create a new idea with title and optional metadata. Returns the created idea detail.",
65
+ args: {
66
+ title: tool.schema.string().describe("Title of the idea"),
67
+ content: tool.schema.string().optional().describe("Initial content for the idea (markdown)"),
68
+ tags: tool.schema.array(tool.schema.string()).optional().describe("Tags for categorization"),
69
+ status: tool.schema.enum(["draft", "in-progress", "completed", "archived"]).optional().describe("Status (default: draft)"),
70
+ priority: tool.schema.enum(["low", "medium", "high"]).optional().describe("Priority (default: medium)")
71
+ },
72
+ async execute(args) {
73
+ const result = await createIdea(args);
74
+ return `Idea created successfully:
75
+ ${JSON.stringify(result, null, 2)}`;
76
+ }
77
+ });
78
+ var update = tool({
79
+ description: "Update idea metadata (title, tags, status, priority). Use the idea 'path' value as identifier.",
80
+ args: {
81
+ path: tool.schema.string().describe("The idea path (identifier, e.g. '1743123456_my-idea')"),
82
+ title: tool.schema.string().optional().describe("New title"),
83
+ tags: tool.schema.array(tool.schema.string()).optional().describe("Replace tags"),
84
+ status: tool.schema.enum(["draft", "in-progress", "completed", "archived"]).optional().describe("New status"),
85
+ priority: tool.schema.enum(["low", "medium", "high"]).optional().describe("New priority")
86
+ },
87
+ async execute(args) {
88
+ const { path: ideaPath, ...body } = args;
89
+ const result = await updateIdeaMeta(ideaPath, body);
90
+ return `Idea updated successfully:
91
+ ${JSON.stringify(result, null, 2)}`;
92
+ }
93
+ });
94
+ var remove = tool({
95
+ description: "Delete an idea by its path. This removes the idea and all its files permanently.",
96
+ args: {
97
+ path: tool.schema.string().describe("The idea path to delete (e.g. '1743123456_my-idea')")
98
+ },
99
+ async execute(args) {
100
+ await deleteIdea(args.path);
101
+ return `Idea '${args.path}' deleted successfully.`;
102
+ }
103
+ });
104
+
105
+ // src/plugins/idea-plugins.ts
106
+ import path from "path";
107
+ function getIdeaDir() {
108
+ return path.resolve(env.CLOUDY_ASSISTANT_BASE_PATH, "idea");
109
+ }
110
+ var DESTRUCTIVE_COMMANDS = ["rm", "mv", "cp", "rmdir", "chmod", "chown", "ln"];
111
+ function isDestructiveBashOnIdea(command, ideaDir) {
112
+ const tokens = command.trim().split(/\s+/);
113
+ const cmd = tokens[0];
114
+ if (!DESTRUCTIVE_COMMANDS.includes(cmd ?? "")) return false;
115
+ const normalizedIdeaDir = path.resolve(ideaDir);
116
+ return tokens.slice(1).some((t) => {
117
+ const resolved = path.resolve(t);
118
+ return resolved.startsWith(normalizedIdeaDir + path.sep) || resolved === normalizedIdeaDir;
119
+ });
120
+ }
121
+ function isFileIdeaFile(filePath, ideaDir = getIdeaDir()) {
122
+ const normalizedIdeaPath = path.resolve(ideaDir);
123
+ const normalizedFile = path.resolve(filePath);
124
+ return normalizedFile.startsWith(normalizedIdeaPath + path.sep);
125
+ }
126
+ function isIdeaIndexMd(filePath, ideaDir = getIdeaDir()) {
127
+ if (!isFileIdeaFile(filePath, ideaDir)) return false;
128
+ return path.basename(filePath) === "index.md";
129
+ }
130
+ function extractIdeaPath(filePath, ideaDir = getIdeaDir()) {
131
+ const normalizedIdeaPath = path.resolve(ideaDir);
132
+ const normalizedFile = path.resolve(filePath);
133
+ const relative = normalizedFile.slice(normalizedIdeaPath.length + 1);
134
+ return relative.split(path.sep)[0] ?? "";
135
+ }
136
+
137
+ // src/plugins/index.ts
138
+ var IdeaPlugin = async ({
139
+ project,
140
+ client,
141
+ $,
142
+ directory,
143
+ worktree
144
+ }) => {
145
+ return {
146
+ tool: {
147
+ idea_list: list,
148
+ idea_create: create,
149
+ idea_update: update,
150
+ idea_remove: remove
151
+ },
152
+ "tool.execute.before": async (input, output) => {
153
+ if (input.tool === "bash") {
154
+ const command = output.args.command ?? "";
155
+ if (isDestructiveBashOnIdea(command, getIdeaDir())) {
156
+ throw new Error(`Cannot run destructive command on idea directory`);
157
+ }
158
+ }
159
+ if (input.tool === "write") {
160
+ const filePath = output.args.filePath;
161
+ if (isIdeaIndexMd(filePath)) {
162
+ throw new Error("Cannot overwrite index.md in idea directory");
163
+ }
164
+ if (isFileIdeaFile(filePath)) {
165
+ const ideaPath = extractIdeaPath(filePath);
166
+ await touchIdea(ideaPath);
167
+ }
168
+ }
169
+ if (input.tool === "edit") {
170
+ const filePath = output.args.filePath;
171
+ if (isIdeaIndexMd(filePath)) {
172
+ throw new Error("Cannot modify index.md in idea directory");
173
+ }
174
+ if (isFileIdeaFile(filePath)) {
175
+ const ideaPath = extractIdeaPath(filePath);
176
+ await touchIdea(ideaPath);
177
+ }
178
+ }
179
+ }
180
+ };
181
+ };
182
+ export {
183
+ IdeaPlugin,
184
+ create,
185
+ list,
186
+ remove,
187
+ update
188
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@cloudy-app/opencode-plugin",
3
+ "version": "0.0.1",
4
+ "description": "OpenCode plugin for Cloudy — idea management tools and session tracking",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "require": "./dist/index.js"
21
+ }
22
+ },
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "prepublishOnly": "bun run build",
26
+ "test": "bun test"
27
+ },
28
+ "keywords": [
29
+ "opencode",
30
+ "plugin",
31
+ "cloudy",
32
+ "idea",
33
+ "ai",
34
+ "llm"
35
+ ],
36
+ "license": "MIT",
37
+ "peerDependencies": {
38
+ "@opencode-ai/plugin": ">=0.15.0"
39
+ },
40
+ "devDependencies": {
41
+ "@opencode-ai/plugin": "1.2.23",
42
+ "@types/bun": "^1.3.1",
43
+ "tsup": "^8.5.1",
44
+ "typescript": "^5.9.3",
45
+ "zod": "^4.3.6"
46
+ }
47
+ }