@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 +129 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +188 -0
- package/package.json +47 -0
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
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|