opencode-froggy 0.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.
- package/README.md +440 -0
- package/agent/code-reviewer.md +89 -0
- package/agent/code-simplifier.md +77 -0
- package/agent/doc-writer.md +101 -0
- package/command/commit.md +18 -0
- package/command/review-changes.md +28 -0
- package/command/review-pr.md +29 -0
- package/command/simplify-changes.md +26 -0
- package/command/tests-coverage.md +7 -0
- package/dist/bash-executor.d.ts +15 -0
- package/dist/bash-executor.js +45 -0
- package/dist/code-files.d.ts +3 -0
- package/dist/code-files.js +50 -0
- package/dist/code-files.test.d.ts +1 -0
- package/dist/code-files.test.js +22 -0
- package/dist/config-paths.d.ts +11 -0
- package/dist/config-paths.js +32 -0
- package/dist/config-paths.test.d.ts +1 -0
- package/dist/config-paths.test.js +101 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +288 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +808 -0
- package/dist/loaders.d.ts +80 -0
- package/dist/loaders.js +135 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +15 -0
- package/package.json +51 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export interface AgentFrontmatter {
|
|
2
|
+
description: string;
|
|
3
|
+
mode?: "subagent" | "agent";
|
|
4
|
+
temperature?: number;
|
|
5
|
+
tools?: Record<string, boolean>;
|
|
6
|
+
permission?: Record<string, unknown>;
|
|
7
|
+
permissions?: Record<string, unknown>;
|
|
8
|
+
}
|
|
9
|
+
export interface SkillFrontmatter {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
license?: string;
|
|
13
|
+
compatibility?: string;
|
|
14
|
+
metadata?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
export interface CommandFrontmatter {
|
|
17
|
+
description: string;
|
|
18
|
+
agent?: string;
|
|
19
|
+
model?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface CommandConfig {
|
|
22
|
+
template: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
agent?: string;
|
|
25
|
+
model?: string;
|
|
26
|
+
subtask?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface LoadedSkill {
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
path: string;
|
|
32
|
+
body: string;
|
|
33
|
+
}
|
|
34
|
+
export type HookEvent = "session.idle" | "session.created" | "session.deleted" | `tool.before.${string}` | `tool.after.${string}`;
|
|
35
|
+
export type HookCondition = "isMainSession" | "hasCodeChange";
|
|
36
|
+
export interface HookActionCommand {
|
|
37
|
+
command: string | {
|
|
38
|
+
name: string;
|
|
39
|
+
args: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export interface HookActionSkill {
|
|
43
|
+
skill: string;
|
|
44
|
+
}
|
|
45
|
+
export interface HookActionTool {
|
|
46
|
+
tool: {
|
|
47
|
+
name: string;
|
|
48
|
+
args: Record<string, unknown>;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export interface HookActionBash {
|
|
52
|
+
bash: string | {
|
|
53
|
+
command: string;
|
|
54
|
+
timeout?: number;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export type HookAction = HookActionCommand | HookActionSkill | HookActionTool | HookActionBash;
|
|
58
|
+
export interface HookConfig {
|
|
59
|
+
event: HookEvent;
|
|
60
|
+
conditions?: HookCondition[];
|
|
61
|
+
actions: HookAction[];
|
|
62
|
+
}
|
|
63
|
+
export interface AgentConfigOutput {
|
|
64
|
+
description: string;
|
|
65
|
+
mode: "subagent" | "primary" | "all";
|
|
66
|
+
temperature?: number;
|
|
67
|
+
tools?: Record<string, boolean>;
|
|
68
|
+
permissions?: Record<string, unknown>;
|
|
69
|
+
prompt: string;
|
|
70
|
+
[key: string]: unknown;
|
|
71
|
+
}
|
|
72
|
+
export declare function parseFrontmatter<T>(content: unknown): {
|
|
73
|
+
data: T;
|
|
74
|
+
body: string;
|
|
75
|
+
};
|
|
76
|
+
export declare function loadAgents(agentDir: string): Record<string, AgentConfigOutput>;
|
|
77
|
+
export declare function loadSkills(skillDir: string): LoadedSkill[];
|
|
78
|
+
export declare function loadCommands(commandDir: string): Record<string, CommandConfig>;
|
|
79
|
+
export declare function loadHooks(hookDir: string): Map<HookEvent, HookConfig[]>;
|
|
80
|
+
export declare function mergeHooks(...hookMaps: Map<HookEvent, HookConfig[]>[]): Map<HookEvent, HookConfig[]>;
|
package/dist/loaders.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join, basename } from "node:path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
const SESSION_HOOK_EVENTS = ["session.idle", "session.created", "session.deleted"];
|
|
5
|
+
const TOOL_HOOK_PATTERN = /^tool\.(before|after)\..+$/;
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// UTILITIES
|
|
8
|
+
// ============================================================================
|
|
9
|
+
export function parseFrontmatter(content) {
|
|
10
|
+
if (typeof content !== "string") {
|
|
11
|
+
return { data: {}, body: "" };
|
|
12
|
+
}
|
|
13
|
+
const match = content.match(/^---\r?\n([\s\S]*?)(?:\r?\n)?---(?:\r?\n)?([\s\S]*)$/);
|
|
14
|
+
if (!match) {
|
|
15
|
+
return { data: {}, body: content };
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const yamlContent = match[1].trim();
|
|
19
|
+
const parsed = yamlContent ? yaml.load(yamlContent) : null;
|
|
20
|
+
return { data: parsed ?? {}, body: match[2] };
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { data: {}, body: content };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// LOADERS
|
|
28
|
+
// ============================================================================
|
|
29
|
+
export function loadAgents(agentDir) {
|
|
30
|
+
if (!existsSync(agentDir))
|
|
31
|
+
return {};
|
|
32
|
+
const agents = {};
|
|
33
|
+
for (const file of readdirSync(agentDir)) {
|
|
34
|
+
if (!file.endsWith(".md"))
|
|
35
|
+
continue;
|
|
36
|
+
const filePath = join(agentDir, file);
|
|
37
|
+
const content = readFileSync(filePath, "utf-8");
|
|
38
|
+
const { data, body } = parseFrontmatter(content);
|
|
39
|
+
const agentName = basename(file, ".md");
|
|
40
|
+
const mode = data.mode === "agent" ? "primary" : "subagent";
|
|
41
|
+
const permissions = data.permissions ?? data.permission;
|
|
42
|
+
agents[agentName] = {
|
|
43
|
+
description: data.description || "",
|
|
44
|
+
mode,
|
|
45
|
+
prompt: body.trim(),
|
|
46
|
+
...(data.temperature !== undefined && { temperature: data.temperature }),
|
|
47
|
+
...(data.tools !== undefined && { tools: data.tools }),
|
|
48
|
+
...(permissions !== undefined && { permissions }),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return agents;
|
|
52
|
+
}
|
|
53
|
+
export function loadSkills(skillDir) {
|
|
54
|
+
if (!existsSync(skillDir))
|
|
55
|
+
return [];
|
|
56
|
+
const skills = [];
|
|
57
|
+
for (const entry of readdirSync(skillDir, { withFileTypes: true })) {
|
|
58
|
+
if (!entry.isDirectory())
|
|
59
|
+
continue;
|
|
60
|
+
const skillPath = join(skillDir, entry.name, "SKILL.md");
|
|
61
|
+
if (!existsSync(skillPath))
|
|
62
|
+
continue;
|
|
63
|
+
const content = readFileSync(skillPath, "utf-8");
|
|
64
|
+
const { data, body } = parseFrontmatter(content);
|
|
65
|
+
skills.push({
|
|
66
|
+
name: data.name || entry.name,
|
|
67
|
+
description: data.description || "",
|
|
68
|
+
path: skillPath,
|
|
69
|
+
body: body.trim(),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return skills;
|
|
73
|
+
}
|
|
74
|
+
export function loadCommands(commandDir) {
|
|
75
|
+
if (!existsSync(commandDir))
|
|
76
|
+
return {};
|
|
77
|
+
const commands = {};
|
|
78
|
+
for (const file of readdirSync(commandDir)) {
|
|
79
|
+
if (!file.endsWith(".md"))
|
|
80
|
+
continue;
|
|
81
|
+
const filePath = join(commandDir, file);
|
|
82
|
+
const content = readFileSync(filePath, "utf-8");
|
|
83
|
+
const { data, body } = parseFrontmatter(content);
|
|
84
|
+
const commandName = basename(file, ".md");
|
|
85
|
+
commands[commandName] = {
|
|
86
|
+
description: data.description || "",
|
|
87
|
+
agent: data.agent,
|
|
88
|
+
model: data.model,
|
|
89
|
+
template: body.trim(),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return commands;
|
|
93
|
+
}
|
|
94
|
+
function isValidHookEvent(event) {
|
|
95
|
+
return SESSION_HOOK_EVENTS.includes(event) || TOOL_HOOK_PATTERN.test(event);
|
|
96
|
+
}
|
|
97
|
+
export function loadHooks(hookDir) {
|
|
98
|
+
const hooks = new Map();
|
|
99
|
+
const hooksFilePath = join(hookDir, "hooks.md");
|
|
100
|
+
if (!existsSync(hooksFilePath))
|
|
101
|
+
return hooks;
|
|
102
|
+
const content = readFileSync(hooksFilePath, "utf-8");
|
|
103
|
+
const { data } = parseFrontmatter(content);
|
|
104
|
+
if (!data.hooks || !Array.isArray(data.hooks))
|
|
105
|
+
return hooks;
|
|
106
|
+
for (const hookDef of data.hooks) {
|
|
107
|
+
if (!hookDef.event || !isValidHookEvent(hookDef.event))
|
|
108
|
+
continue;
|
|
109
|
+
if (!hookDef.actions || !Array.isArray(hookDef.actions))
|
|
110
|
+
continue;
|
|
111
|
+
const hookConfig = {
|
|
112
|
+
event: hookDef.event,
|
|
113
|
+
conditions: Array.isArray(hookDef.conditions) ? hookDef.conditions : undefined,
|
|
114
|
+
actions: hookDef.actions,
|
|
115
|
+
};
|
|
116
|
+
const existing = hooks.get(hookDef.event);
|
|
117
|
+
if (existing) {
|
|
118
|
+
existing.push(hookConfig);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
hooks.set(hookDef.event, [hookConfig]);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return hooks;
|
|
125
|
+
}
|
|
126
|
+
export function mergeHooks(...hookMaps) {
|
|
127
|
+
const merged = new Map();
|
|
128
|
+
for (const hookMap of hookMaps) {
|
|
129
|
+
for (const [event, configs] of hookMap) {
|
|
130
|
+
const existing = merged.get(event) ?? [];
|
|
131
|
+
merged.set(event, [...existing, ...configs]);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return merged;
|
|
135
|
+
}
|
package/dist/logger.d.ts
ADDED
package/dist/logger.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { appendFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
const LOG_FILE = join(tmpdir(), "opencode-froggy.log");
|
|
5
|
+
export function log(message, data) {
|
|
6
|
+
try {
|
|
7
|
+
const timestamp = new Date().toISOString();
|
|
8
|
+
const entry = `[${timestamp}] ${message} ${data ? JSON.stringify(data) : ""}\n`;
|
|
9
|
+
appendFileSync(LOG_FILE, entry);
|
|
10
|
+
}
|
|
11
|
+
catch { }
|
|
12
|
+
}
|
|
13
|
+
export function getLogFilePath() {
|
|
14
|
+
return LOG_FILE;
|
|
15
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-froggy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode plugin with a hook layer (tool.before.*, session.idle...), agents (code-reviewer, doc-writer), and commands (/review-pr, /commit)",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"agent",
|
|
11
|
+
"command",
|
|
12
|
+
"skill"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"test": "vitest run"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"opencode",
|
|
21
|
+
"opencode-plugin",
|
|
22
|
+
"plugin",
|
|
23
|
+
"hooks",
|
|
24
|
+
"code-review",
|
|
25
|
+
"code-simplification",
|
|
26
|
+
"ai-agent",
|
|
27
|
+
"llm",
|
|
28
|
+
"automation",
|
|
29
|
+
"developer-tools",
|
|
30
|
+
"cli"
|
|
31
|
+
],
|
|
32
|
+
"author": "Fred",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/smartfrog/opencode-froggy"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"js-yaml": "^4.1.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@opencode-ai/plugin": "latest",
|
|
43
|
+
"@types/bun": "latest",
|
|
44
|
+
"@types/js-yaml": "^4.0.9",
|
|
45
|
+
"typescript": "^5.7.0",
|
|
46
|
+
"vitest": "^4.0.16"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@opencode-ai/plugin": "*"
|
|
50
|
+
}
|
|
51
|
+
}
|