opencode-froggy 0.1.0 → 0.2.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.
@@ -113,16 +113,39 @@ Content`;
113
113
  expect(result["minimal"]).not.toHaveProperty("tools");
114
114
  expect(result["minimal"]).not.toHaveProperty("permissions");
115
115
  });
116
- it("should convert agent mode to primary", () => {
117
- const agentContent = `---
116
+ it("should use mode value directly (primary, subagent, all)", () => {
117
+ const primaryContent = `---
118
118
  description: Primary agent
119
- mode: agent
119
+ mode: primary
120
120
  ---
121
121
 
122
122
  Content`;
123
- writeFileSync(join(testDir, "primary.md"), agentContent);
123
+ const subagentContent = `---
124
+ description: Subagent
125
+ mode: subagent
126
+ ---
127
+
128
+ Content`;
129
+ const allContent = `---
130
+ description: All modes agent
131
+ mode: all
132
+ ---
133
+
134
+ Content`;
135
+ const noModeContent = `---
136
+ description: No mode specified
137
+ ---
138
+
139
+ Content`;
140
+ writeFileSync(join(testDir, "primary.md"), primaryContent);
141
+ writeFileSync(join(testDir, "subagent.md"), subagentContent);
142
+ writeFileSync(join(testDir, "all.md"), allContent);
143
+ writeFileSync(join(testDir, "nomode.md"), noModeContent);
124
144
  const result = loadAgents(testDir);
125
145
  expect(result["primary"].mode).toBe("primary");
146
+ expect(result["subagent"].mode).toBe("subagent");
147
+ expect(result["all"].mode).toBe("all");
148
+ expect(result["nomode"].mode).toBe("all");
126
149
  });
127
150
  it("should ignore non-markdown files", () => {
128
151
  writeFileSync(join(testDir, "not-agent.txt"), "some content");
@@ -286,7 +309,6 @@ hooks:
286
309
  conditions: [isMainSession]
287
310
  actions:
288
311
  - command: simplify-changes
289
- - skill: post-change-code-simplification
290
312
  - tool:
291
313
  name: bash
292
314
  args:
@@ -296,10 +318,9 @@ hooks:
296
318
  const result = loadHooks(testDir);
297
319
  const hooks = result.get("session.idle");
298
320
  expect(hooks).toHaveLength(1);
299
- expect(hooks[0].actions).toHaveLength(3);
321
+ expect(hooks[0].actions).toHaveLength(2);
300
322
  expect(hooks[0].actions[0]).toEqual({ command: "simplify-changes" });
301
- expect(hooks[0].actions[1]).toEqual({ skill: "post-change-code-simplification" });
302
- expect(hooks[0].actions[2]).toEqual({
323
+ expect(hooks[0].actions[1]).toEqual({
303
324
  tool: { name: "bash", args: { command: "echo done" } }
304
325
  });
305
326
  });
package/dist/loaders.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  export interface AgentFrontmatter {
2
2
  description: string;
3
- mode?: "subagent" | "agent";
3
+ mode?: "primary" | "subagent" | "all";
4
+ model?: string;
4
5
  temperature?: number;
6
+ maxSteps?: number;
7
+ disable?: boolean;
5
8
  tools?: Record<string, boolean>;
6
9
  permission?: Record<string, unknown>;
7
10
  permissions?: Record<string, unknown>;
@@ -17,6 +20,7 @@ export interface CommandFrontmatter {
17
20
  description: string;
18
21
  agent?: string;
19
22
  model?: string;
23
+ subtask?: boolean;
20
24
  }
21
25
  export interface CommandConfig {
22
26
  template: string;
@@ -39,9 +43,6 @@ export interface HookActionCommand {
39
43
  args: string;
40
44
  };
41
45
  }
42
- export interface HookActionSkill {
43
- skill: string;
44
- }
45
46
  export interface HookActionTool {
46
47
  tool: {
47
48
  name: string;
@@ -54,7 +55,7 @@ export interface HookActionBash {
54
55
  timeout?: number;
55
56
  };
56
57
  }
57
- export type HookAction = HookActionCommand | HookActionSkill | HookActionTool | HookActionBash;
58
+ export type HookAction = HookActionCommand | HookActionTool | HookActionBash;
58
59
  export interface HookConfig {
59
60
  event: HookEvent;
60
61
  conditions?: HookCondition[];
@@ -63,7 +64,10 @@ export interface HookConfig {
63
64
  export interface AgentConfigOutput {
64
65
  description: string;
65
66
  mode: "subagent" | "primary" | "all";
67
+ model?: string;
66
68
  temperature?: number;
69
+ maxSteps?: number;
70
+ disable?: boolean;
67
71
  tools?: Record<string, boolean>;
68
72
  permissions?: Record<string, unknown>;
69
73
  prompt: string;
package/dist/loaders.js CHANGED
@@ -37,13 +37,16 @@ export function loadAgents(agentDir) {
37
37
  const content = readFileSync(filePath, "utf-8");
38
38
  const { data, body } = parseFrontmatter(content);
39
39
  const agentName = basename(file, ".md");
40
- const mode = data.mode === "agent" ? "primary" : "subagent";
40
+ const mode = data.mode ?? "all";
41
41
  const permissions = data.permissions ?? data.permission;
42
42
  agents[agentName] = {
43
43
  description: data.description || "",
44
44
  mode,
45
45
  prompt: body.trim(),
46
+ ...(data.model !== undefined && { model: data.model }),
46
47
  ...(data.temperature !== undefined && { temperature: data.temperature }),
48
+ ...(data.maxSteps !== undefined && { maxSteps: data.maxSteps }),
49
+ ...(data.disable !== undefined && { disable: data.disable }),
47
50
  ...(data.tools !== undefined && { tools: data.tools }),
48
51
  ...(permissions !== undefined && { permissions }),
49
52
  };
@@ -86,6 +89,7 @@ export function loadCommands(commandDir) {
86
89
  description: data.description || "",
87
90
  agent: data.agent,
88
91
  model: data.model,
92
+ subtask: data.subtask,
89
93
  template: body.trim(),
90
94
  };
91
95
  }
@@ -0,0 +1,20 @@
1
+ import { type ToolContext } from "@opencode-ai/plugin";
2
+ export interface DiffSummaryArgs {
3
+ source?: string;
4
+ target?: string;
5
+ remote?: string;
6
+ }
7
+ export declare function diffSummary(args: DiffSummaryArgs, cwd: string): Promise<string>;
8
+ export declare function createDiffSummaryTool(directory: string): {
9
+ description: string;
10
+ args: {
11
+ source: import("zod").ZodOptional<import("zod").ZodString>;
12
+ target: import("zod").ZodOptional<import("zod").ZodString>;
13
+ remote: import("zod").ZodOptional<import("zod").ZodString>;
14
+ };
15
+ execute(args: {
16
+ source?: string | undefined;
17
+ target?: string | undefined;
18
+ remote?: string | undefined;
19
+ }, context: ToolContext): Promise<string>;
20
+ };
@@ -0,0 +1,111 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { tool } from "@opencode-ai/plugin";
4
+ import { log } from "../logger";
5
+ const execFileAsync = promisify(execFile);
6
+ const DIFF_CONTEXT_LINES = 5;
7
+ async function git(args, cwd) {
8
+ try {
9
+ const result = await execFileAsync("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 });
10
+ return result.stdout;
11
+ }
12
+ catch (error) {
13
+ const execError = error;
14
+ if (execError.stdout)
15
+ return execError.stdout;
16
+ throw error;
17
+ }
18
+ }
19
+ async function getDiffSet(cwd, extraArgs = []) {
20
+ const [stats, files, diff] = await Promise.all([
21
+ git(["diff", ...extraArgs, "--stat"], cwd),
22
+ git(["diff", ...extraArgs, "--name-status"], cwd),
23
+ git(["diff", ...extraArgs, `-U${DIFF_CONTEXT_LINES}`, "--function-context"], cwd),
24
+ ]);
25
+ return { stats, files, diff };
26
+ }
27
+ async function getBranchesDiff(source, target, remote, cwd) {
28
+ const refSource = remote ? `${remote}/${source}` : source;
29
+ const refTarget = remote ? `${remote}/${target}` : target;
30
+ const range = `${refTarget}...${refSource}`;
31
+ const rangeLog = `${refTarget}..${refSource}`;
32
+ if (remote) {
33
+ await git(["fetch", remote, source, target, "--prune"], cwd);
34
+ }
35
+ const [stats, commits, files, diff] = await Promise.all([
36
+ git(["diff", "--stat", range], cwd),
37
+ git(["log", "--oneline", "--no-merges", rangeLog], cwd),
38
+ git(["diff", "--name-only", range], cwd),
39
+ git(["diff", `-U${DIFF_CONTEXT_LINES}`, "--function-context", range], cwd),
40
+ ]);
41
+ return [
42
+ "## Stats Overview", "```", stats.trim(), "```",
43
+ "",
44
+ "## Commits to Review", "```", commits.trim(), "```",
45
+ "",
46
+ "## Files Changed", "```", files.trim(), "```",
47
+ "",
48
+ "## Full Diff", "```diff", diff.trim(), "```",
49
+ ].join("\n\n");
50
+ }
51
+ async function getWorkingTreeDiff(cwd) {
52
+ const sections = [];
53
+ const [status, staged, unstaged, untrackedList] = await Promise.all([
54
+ git(["status", "--porcelain=v1", "-uall"], cwd),
55
+ getDiffSet(cwd, ["--cached"]),
56
+ getDiffSet(cwd),
57
+ git(["ls-files", "--others", "--exclude-standard"], cwd),
58
+ ]);
59
+ sections.push("## Status Overview", "```", status.trim() || "(no changes)", "```");
60
+ if (staged.stats.trim() || staged.files.trim()) {
61
+ sections.push("## Staged Changes", "### Stats", "```", staged.stats.trim() || "(none)", "```", "### Files", "```", staged.files.trim() || "(none)", "```", "### Diff", "```diff", staged.diff.trim() || "(none)", "```");
62
+ }
63
+ if (unstaged.stats.trim() || unstaged.files.trim()) {
64
+ sections.push("## Unstaged Changes", "### Stats", "```", unstaged.stats.trim() || "(none)", "```", "### Files", "```", unstaged.files.trim() || "(none)", "```", "### Diff", "```diff", unstaged.diff.trim() || "(none)", "```");
65
+ }
66
+ const untrackedFiles = untrackedList.trim().split("\n").filter(Boolean);
67
+ if (untrackedFiles.length > 0) {
68
+ const untrackedDiffs = [];
69
+ for (const file of untrackedFiles) {
70
+ try {
71
+ const fileDiff = await git(["diff", "--no-index", `-U${DIFF_CONTEXT_LINES}`, "--function-context", "--", "/dev/null", file], cwd);
72
+ untrackedDiffs.push(`=== NEW: ${file} ===\n${fileDiff.trim()}`);
73
+ }
74
+ catch (error) {
75
+ log("[diff-summary] failed to diff untracked file", { file, error: String(error) });
76
+ untrackedDiffs.push(`=== NEW: ${file} === (could not diff)`);
77
+ }
78
+ }
79
+ sections.push("## Untracked Files", "```", untrackedFiles.join("\n"), "```", "### Diffs", "```diff", untrackedDiffs.join("\n\n"), "```");
80
+ }
81
+ return sections.join("\n\n");
82
+ }
83
+ export async function diffSummary(args, cwd) {
84
+ const { source, target = "main", remote = "origin" } = args;
85
+ if (source) {
86
+ return getBranchesDiff(source, target, remote, cwd);
87
+ }
88
+ return getWorkingTreeDiff(cwd);
89
+ }
90
+ export function createDiffSummaryTool(directory) {
91
+ return tool({
92
+ description: "Generate a structured summary of git diffs. Use for reviewing branches comparison or working tree changes. Returns stats, commits, files changed, and full diff.",
93
+ args: {
94
+ source: tool.schema
95
+ .string()
96
+ .optional()
97
+ .describe("Source branch to compare (e.g., 'feature-branch'). If omitted, analyzes working tree changes."),
98
+ target: tool.schema
99
+ .string()
100
+ .optional()
101
+ .describe("Target branch to compare against (default: 'main')"),
102
+ remote: tool.schema
103
+ .string()
104
+ .optional()
105
+ .describe("Git remote name (default: 'origin')"),
106
+ },
107
+ async execute(args, _context) {
108
+ return diffSummary(args, directory);
109
+ },
110
+ });
111
+ }
@@ -0,0 +1,26 @@
1
+ import { type ToolContext } from "@opencode-ai/plugin";
2
+ export interface GitingestArgs {
3
+ url: string;
4
+ maxFileSize?: number;
5
+ pattern?: string;
6
+ patternType?: "include" | "exclude";
7
+ }
8
+ export declare function fetchGitingest(args: GitingestArgs): Promise<string>;
9
+ export declare const gitingestTool: {
10
+ description: string;
11
+ args: {
12
+ url: import("zod").ZodString;
13
+ maxFileSize: import("zod").ZodOptional<import("zod").ZodNumber>;
14
+ pattern: import("zod").ZodOptional<import("zod").ZodString>;
15
+ patternType: import("zod").ZodOptional<import("zod").ZodEnum<{
16
+ include: "include";
17
+ exclude: "exclude";
18
+ }>>;
19
+ };
20
+ execute(args: {
21
+ url: string;
22
+ maxFileSize?: number | undefined;
23
+ pattern?: string | undefined;
24
+ patternType?: "include" | "exclude" | undefined;
25
+ }, context: ToolContext): Promise<string>;
26
+ };
@@ -0,0 +1,41 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ export async function fetchGitingest(args) {
3
+ const response = await fetch("https://gitingest.com/api/ingest", {
4
+ method: "POST",
5
+ headers: { "Content-Type": "application/json" },
6
+ body: JSON.stringify({
7
+ input_text: args.url,
8
+ max_file_size: args.maxFileSize ?? 50000,
9
+ pattern: args.pattern ?? "",
10
+ pattern_type: args.patternType ?? "exclude",
11
+ }),
12
+ });
13
+ if (!response.ok) {
14
+ throw new Error(`gitingest API error: ${response.status} ${response.statusText}`);
15
+ }
16
+ const data = (await response.json());
17
+ return `${data.summary}\n\n${data.tree}\n\n${data.content}`;
18
+ }
19
+ export const gitingestTool = tool({
20
+ description: "Fetch a GitHub repository's full content via gitingest.com. Returns summary, directory tree, and file contents optimized for LLM analysis. Use when you need to understand an external repository's structure or code.",
21
+ args: {
22
+ url: tool.schema
23
+ .string()
24
+ .describe("GitHub repository URL (e.g., https://github.com/owner/repo)"),
25
+ maxFileSize: tool.schema
26
+ .number()
27
+ .optional()
28
+ .describe("Maximum file size in bytes to include (default: 50000)"),
29
+ pattern: tool.schema
30
+ .string()
31
+ .optional()
32
+ .describe("Glob pattern to filter files (e.g., '*.py' or 'src/*')"),
33
+ patternType: tool.schema
34
+ .enum(["include", "exclude"])
35
+ .optional()
36
+ .describe("Whether pattern includes or excludes matching files (default: exclude)"),
37
+ },
38
+ async execute(args, _context) {
39
+ return fetchGitingest(args);
40
+ },
41
+ });
@@ -0,0 +1,4 @@
1
+ export { gitingestTool, fetchGitingest, type GitingestArgs } from "./gitingest";
2
+ export { createDiffSummaryTool, diffSummary, type DiffSummaryArgs } from "./diff-summary";
3
+ export { createPromptSessionTool, type PromptSessionArgs } from "./prompt-session";
4
+ export { createListChildSessionsTool } from "./list-child-sessions";
@@ -0,0 +1,4 @@
1
+ export { gitingestTool, fetchGitingest } from "./gitingest";
2
+ export { createDiffSummaryTool, diffSummary } from "./diff-summary";
3
+ export { createPromptSessionTool } from "./prompt-session";
4
+ export { createListChildSessionsTool } from "./list-child-sessions";
@@ -0,0 +1,9 @@
1
+ import { type ToolContext } from "@opencode-ai/plugin";
2
+ import type { createOpencodeClient } from "@opencode-ai/sdk";
3
+ type Client = ReturnType<typeof createOpencodeClient>;
4
+ export declare function createListChildSessionsTool(client: Client): {
5
+ description: string;
6
+ args: {};
7
+ execute(args: Record<string, never>, context: ToolContext): Promise<string>;
8
+ };
9
+ export {};
@@ -0,0 +1,24 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { log } from "../logger";
3
+ export function createListChildSessionsTool(client) {
4
+ return tool({
5
+ description: "List all child sessions (subagents) of the current session",
6
+ args: {},
7
+ async execute(_args, context) {
8
+ const children = await client.session.children({
9
+ path: { id: context.sessionID },
10
+ });
11
+ const childList = children.data ?? [];
12
+ if (childList.length === 0) {
13
+ return "No child sessions found";
14
+ }
15
+ log("[list-child-sessions] Found child sessions", { count: childList.length });
16
+ const formatted = childList.map((child, index) => {
17
+ const created = new Date(child.time.created).toISOString();
18
+ const updated = new Date(child.time.updated).toISOString();
19
+ return `${index + 1}. [${child.id}] ${child.title}\n Created: ${created} | Updated: ${updated}`;
20
+ }).join("\n\n");
21
+ return `Child sessions (${childList.length}):\n\n${formatted}`;
22
+ },
23
+ });
24
+ }
@@ -0,0 +1,19 @@
1
+ import { type ToolContext } from "@opencode-ai/plugin";
2
+ import type { createOpencodeClient } from "@opencode-ai/sdk";
3
+ type Client = ReturnType<typeof createOpencodeClient>;
4
+ export interface PromptSessionArgs {
5
+ message: string;
6
+ sessionId?: string;
7
+ }
8
+ export declare function createPromptSessionTool(client: Client): {
9
+ description: string;
10
+ args: {
11
+ message: import("zod").ZodString;
12
+ sessionId: import("zod").ZodOptional<import("zod").ZodString>;
13
+ };
14
+ execute(args: {
15
+ message: string;
16
+ sessionId?: string | undefined;
17
+ }, context: ToolContext): Promise<string>;
18
+ };
19
+ export {};
@@ -0,0 +1,39 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { log } from "../logger";
3
+ export function createPromptSessionTool(client) {
4
+ return tool({
5
+ description: "Send a message to a child session (subagent) to continue the conversation",
6
+ args: {
7
+ message: tool.schema.string().describe("The message to send to the child session"),
8
+ sessionId: tool.schema.string().optional().describe("The child session ID to target (optional - uses last child if not provided)"),
9
+ },
10
+ async execute(args, context) {
11
+ let targetSessionId = args.sessionId;
12
+ if (!targetSessionId) {
13
+ const children = await client.session.children({
14
+ path: { id: context.sessionID },
15
+ });
16
+ const lastChild = (children.data ?? []).at(-1);
17
+ if (!lastChild) {
18
+ return "Error: No child session found for current session";
19
+ }
20
+ targetSessionId = lastChild.id;
21
+ }
22
+ log("[prompt-session] Sending message to child session", {
23
+ parentSessionID: context.sessionID,
24
+ childSessionID: targetSessionId,
25
+ messagePreview: args.message.slice(0, 100),
26
+ });
27
+ const response = await client.session.prompt({
28
+ path: { id: targetSessionId },
29
+ body: { parts: [{ type: "text", text: args.message }] },
30
+ });
31
+ const parts = response.data?.parts ?? [];
32
+ const textContent = parts
33
+ .filter((p) => p.type === "text" && p.text)
34
+ .map((p) => p.text)
35
+ .join("\n");
36
+ return textContent || "Message sent to child session";
37
+ },
38
+ });
39
+ }
@@ -0,0 +1,19 @@
1
+ import { type ToolContext } from "@opencode-ai/plugin";
2
+ import type { createOpencodeClient } from "@opencode-ai/sdk";
3
+ type Client = ReturnType<typeof createOpencodeClient>;
4
+ export interface ReplyChildArgs {
5
+ message: string;
6
+ sessionId?: string;
7
+ }
8
+ export declare function createReplyChildTool(client: Client): {
9
+ description: string;
10
+ args: {
11
+ message: import("zod").ZodString;
12
+ sessionId: import("zod").ZodOptional<import("zod").ZodString>;
13
+ };
14
+ execute(args: {
15
+ message: string;
16
+ sessionId?: string | undefined;
17
+ }, context: ToolContext): Promise<string>;
18
+ };
19
+ export {};
@@ -0,0 +1,42 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+ import { log } from "../logger";
3
+ export function createReplyChildTool(client) {
4
+ return tool({
5
+ description: "Send a message to the last child session (subagent) to continue the conversation",
6
+ args: {
7
+ message: tool.schema.string().describe("The message to send to the child session"),
8
+ sessionId: tool.schema.string().optional().describe("The child session ID to target (optional - uses last child if not provided)"),
9
+ },
10
+ async execute(args, context) {
11
+ let targetSessionId = args.sessionId;
12
+ if (!targetSessionId) {
13
+ const children = await client.session.children({
14
+ path: { id: context.sessionID },
15
+ });
16
+ const lastChild = (children.data ?? []).at(-1);
17
+ if (!lastChild) {
18
+ return "Error: No child session found for current session";
19
+ }
20
+ targetSessionId = lastChild.id;
21
+ }
22
+ log("[reply-child] Sending message to child session", {
23
+ parentSessionID: context.sessionID,
24
+ childSessionID: targetSessionId,
25
+ message: args.message.slice(0, 100),
26
+ });
27
+ const response = await client.session.prompt({
28
+ path: { id: targetSessionId },
29
+ body: { parts: [{ type: "text", text: args.message }] },
30
+ });
31
+ log("[reply-child] Response received", {
32
+ childSessionID: targetSessionId,
33
+ });
34
+ const parts = response.data?.parts ?? [];
35
+ const textContent = parts
36
+ .filter((p) => p.type === "text" && p.text)
37
+ .map((p) => p.text)
38
+ .join("\n");
39
+ return textContent || "Message sent to child session";
40
+ },
41
+ });
42
+ }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-froggy",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "OpenCode plugin with a hook layer (tool.before.*, session.idle...), agents (code-reviewer, doc-writer), and commands (/review-pr, /commit)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -9,7 +9,9 @@
9
9
  "dist",
10
10
  "agent",
11
11
  "command",
12
- "skill"
12
+ "skill",
13
+ "images",
14
+ "README.md"
13
15
  ],
14
16
  "scripts": {
15
17
  "build": "tsc",
package/command/commit.md DELETED
@@ -1,18 +0,0 @@
1
- ---
2
- description: Commit and push
3
- agent: build
4
- ---
5
-
6
- ## Context
7
-
8
- - Current git status: !`git status`
9
- - Current git diff (staged and unstaged changes): !`git diff HEAD`
10
- - Current branch: !`git branch --show-current`
11
-
12
- ## Your task
13
-
14
- Based on the above changes:
15
- 1. Create a new branch if on main or master
16
- 2. Create a single commit with an appropriate message
17
- 3. Push the branch to origin
18
- 4. You have the capability to call multiple tools in a single response. You MUST do all of the above in a single message. Do not use any other tools or do anything else. Do not send any other text or messages besides these tool calls.