opencode-froggy 0.12.0 → 1.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.
Files changed (45) hide show
  1. package/README.md +10 -9
  2. package/command/diff-summary.md +27 -8
  3. package/command/doc-changes.md +1 -0
  4. package/command/review-changes.md +1 -0
  5. package/command/review-pr.md +1 -0
  6. package/command/simplify-changes.md +1 -0
  7. package/dist/command-installer.d.ts +6 -0
  8. package/dist/command-installer.js +49 -0
  9. package/dist/command-installer.test.d.ts +1 -0
  10. package/dist/command-installer.test.js +58 -0
  11. package/dist/config-paths.d.ts +2 -0
  12. package/dist/config-paths.js +6 -0
  13. package/dist/config-paths.test.js +16 -0
  14. package/dist/index.d.ts +3 -3
  15. package/dist/index.js +326 -259
  16. package/dist/index.test.js +3 -3
  17. package/dist/loaders.d.ts +2 -0
  18. package/dist/loaders.js +1 -0
  19. package/dist/session-children.d.ts +14 -0
  20. package/dist/session-children.js +27 -0
  21. package/dist/session-children.test.d.ts +1 -0
  22. package/dist/session-children.test.js +25 -0
  23. package/dist/skill-activation.js +1 -1
  24. package/dist/tools/agent-promote.d.ts +38 -11
  25. package/dist/tools/agent-promote.js +35 -33
  26. package/dist/tools/blockchain/eth-address-balance.d.ts +18 -8
  27. package/dist/tools/blockchain/eth-address-balance.js +15 -14
  28. package/dist/tools/blockchain/eth-address-txs.d.ts +22 -10
  29. package/dist/tools/blockchain/eth-address-txs.js +19 -18
  30. package/dist/tools/blockchain/eth-token-transfers.d.ts +22 -10
  31. package/dist/tools/blockchain/eth-token-transfers.js +19 -18
  32. package/dist/tools/blockchain/eth-transaction.d.ts +30 -14
  33. package/dist/tools/blockchain/eth-transaction.js +24 -21
  34. package/dist/tools/gitingest.d.ts +27 -15
  35. package/dist/tools/gitingest.js +28 -21
  36. package/dist/tools/index.d.ts +1 -1
  37. package/dist/tools/index.js +1 -1
  38. package/dist/tools/list-child-sessions.d.ts +11 -7
  39. package/dist/tools/list-child-sessions.js +22 -17
  40. package/dist/tools/pdf-to-markdown.d.ts +18 -8
  41. package/dist/tools/pdf-to-markdown.js +20 -13
  42. package/dist/tools/prompt-session.d.ts +26 -11
  43. package/dist/tools/prompt-session.js +23 -28
  44. package/package.json +13 -5
  45. package/tui.ts +24 -0
package/README.md CHANGED
@@ -56,16 +56,18 @@ Add the plugin to your OpenCode configuration file (`opencode.json`):
56
56
  ```json
57
57
  {
58
58
  "$schema": "https://opencode.ai/config.json",
59
- "plugin": ["opencode-froggy"]
59
+ "plugins": ["opencode-froggy"]
60
60
  }
61
61
  ```
62
62
 
63
+ Requires OpenCode V2.
64
+
63
65
  ### From local files
64
66
 
65
67
  Alternatively, clone or copy the plugin files to one of these directories:
66
68
 
67
- - **Project-local**: `.opencode/plugin/opencode-froggy/`
68
- - **Global**: `~/.config/opencode/plugin/opencode-froggy/`
69
+ - **Project-local**: `.opencode/plugins/opencode-froggy/`
70
+ - **Global**: `~/.config/opencode/plugins/opencode-froggy/`
69
71
 
70
72
  ---
71
73
 
@@ -140,7 +142,7 @@ It does not modify Linear issues, add comments, or update project files.
140
142
 
141
143
  ## Skills
142
144
 
143
- Skills are contextual instructions loaded on demand via the `skill` tool. The agent invokes `skill({ name: "skill-name" })` to load the instructions when needed.
145
+ Skills are contextual instructions loaded on demand via the `skill` tool. The agent invokes `skill({ id: "skill-name" })` to load the instructions when needed.
144
146
 
145
147
  ### Overview
146
148
 
@@ -270,7 +272,7 @@ A skill without `name` or `description` will be ignored.
270
272
  If a skill defines `use_when`, a directive is injected into the system prompt:
271
273
 
272
274
  ```
273
- MANDATORY: Call skill({ name: "my-skill" }) <use_when content>
275
+ MANDATORY: Call skill({ id: "my-skill" }) <use_when content>
274
276
  ```
275
277
 
276
278
  This instructs the agent to load the skill when the specified condition is met.
@@ -361,8 +363,8 @@ promptSession({
361
363
 
362
364
  #### Behavior
363
365
 
364
- - If `sessionId` is not provided, the tool automatically targets the most recently created child session
365
- - Returns the text response from the child session
366
+ - If `sessionId` is not provided, the tool targets the most recently created child session tracked since the plugin loaded (pass `sessionId` explicitly for older sessions)
367
+ - Returns a confirmation once the message is admitted to the child session
366
368
  - Returns an error message if no child session exists for the current session
367
369
 
368
370
  ---
@@ -468,8 +470,7 @@ Promote an agent to primary (default) or specify a grade.
468
470
  #### Notes
469
471
 
470
472
  - Only agents from this plugin can be promoted (see [Agents](#agents) table)
471
- - Changes persist in memory until OpenCode restarts
472
- - After promotion, use `Tab` or `<leader>a` to select the agent
473
+ - Changes apply immediately and persist across restarts via plugin storage
473
474
 
474
475
  ---
475
476
 
@@ -4,30 +4,49 @@ description: Show working tree changes or diff between branches ($1=source, $2=t
4
4
 
5
5
  # Diff Summary
6
6
 
7
+ Branch arguments (if provided): source=$1 target=$2
8
+
7
9
  !`bash -c '
8
10
  SOURCE="$1"
9
11
  TARGET="$2"
10
12
  TARGET="${TARGET:-HEAD}"
11
13
 
14
+ valid_ref() {
15
+ case "$REF" in
16
+ ""|-*) return 1 ;;
17
+ esac
18
+ git rev-parse --verify --quiet "$REF^{commit}" >/dev/null
19
+ }
20
+
12
21
  if [ -n "$SOURCE" ]; then
22
+ REF="$SOURCE"
23
+ if ! valid_ref; then
24
+ echo "Invalid source ref: $SOURCE"
25
+ exit 0
26
+ fi
27
+ REF="$TARGET"
28
+ if ! valid_ref; then
29
+ echo "Invalid target ref: $TARGET"
30
+ exit 0
31
+ fi
13
32
  echo "## Branch Comparison: $SOURCE → $TARGET"
14
33
  echo ""
15
34
  git fetch --all --prune 2>/dev/null || true
16
-
35
+
17
36
  echo "### Stats Overview"
18
- git diff --stat "$TARGET"..."$SOURCE"
19
-
37
+ git diff --stat "$TARGET"..."$SOURCE" --
38
+
20
39
  echo ""
21
40
  echo "### Commits"
22
- git log --oneline --no-merges "$TARGET".."$SOURCE"
23
-
41
+ git log --oneline --no-merges "$TARGET".."$SOURCE" --
42
+
24
43
  echo ""
25
44
  echo "### Files Changed"
26
- git diff --name-only "$TARGET"..."$SOURCE"
27
-
45
+ git diff --name-only "$TARGET"..."$SOURCE" --
46
+
28
47
  echo ""
29
48
  echo "### Full Diff"
30
- git diff "$TARGET"..."$SOURCE"
49
+ git diff "$TARGET"..."$SOURCE" --
31
50
  else
32
51
  echo "## Status"
33
52
  git status --porcelain
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: Update documentation based on uncommitted changes (new features only)
3
3
  agent: doc-writer
4
+ subagent: true
4
5
  ---
5
6
 
6
7
  ## Analysis Phase
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: Review uncommitted changes (staged + unstaged, incl. untracked diffs)
3
3
  agent: code-reviewer
4
+ subagent: true
4
5
  ---
5
6
 
6
7
  # Review: Working Tree → HEAD
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: Review changes from source branch into target branch
3
3
  agent: code-reviewer
4
+ subagent: true
4
5
  ---
5
6
 
6
7
  # Review: $1 → $2
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  description: Simplify uncommitted changes (staged + unstaged, incl. untracked diffs)
3
3
  agent: code-simplifier
4
+ subagent: true
4
5
  ---
5
6
 
6
7
  # Simplify: Working Tree → HEAD
@@ -0,0 +1,6 @@
1
+ export interface InstallFilesResult {
2
+ installed: string[];
3
+ skipped: string[];
4
+ updated: string[];
5
+ }
6
+ export declare function installBundledFiles(sourceDir: string, targetDir: string): InstallFilesResult;
@@ -0,0 +1,49 @@
1
+ import { constants, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { log } from "./logger";
4
+ export function installBundledFiles(sourceDir, targetDir) {
5
+ const installed = [];
6
+ const skipped = [];
7
+ const updated = [];
8
+ if (!existsSync(sourceDir))
9
+ return { installed, skipped, updated };
10
+ try {
11
+ mkdirSync(targetDir, { recursive: true });
12
+ }
13
+ catch (error) {
14
+ log("[commands] failed to prepare target dir", { targetDir, error: String(error) });
15
+ return { installed, skipped, updated };
16
+ }
17
+ let files;
18
+ try {
19
+ files = readdirSync(sourceDir);
20
+ }
21
+ catch (error) {
22
+ log("[commands] failed to read bundled files", { sourceDir, error: String(error) });
23
+ return { installed, skipped, updated };
24
+ }
25
+ for (const file of files) {
26
+ if (!file.endsWith(".md"))
27
+ continue;
28
+ const source = join(sourceDir, file);
29
+ const target = join(targetDir, file);
30
+ try {
31
+ const content = readFileSync(source, "utf-8");
32
+ if (!existsSync(target)) {
33
+ copyFileSync(source, target, constants.COPYFILE_EXCL);
34
+ installed.push(file);
35
+ continue;
36
+ }
37
+ if (readFileSync(target, "utf-8") !== content) {
38
+ copyFileSync(source, target);
39
+ updated.push(file);
40
+ continue;
41
+ }
42
+ skipped.push(file);
43
+ }
44
+ catch (error) {
45
+ log("[commands] failed to install bundled file", { file, error: String(error) });
46
+ }
47
+ }
48
+ return { installed, skipped, updated };
49
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,58 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { installBundledFiles } from "./command-installer";
6
+ describe("installBundledFiles", () => {
7
+ let sourceDir;
8
+ let targetDir;
9
+ beforeEach(() => {
10
+ const base = join(tmpdir(), `froggy-files-${Date.now()}-${Math.random().toString(16).slice(2)}`);
11
+ sourceDir = join(base, "source");
12
+ targetDir = join(base, "target");
13
+ mkdirSync(sourceDir, { recursive: true });
14
+ });
15
+ afterEach(() => {
16
+ rmSync(join(sourceDir, ".."), { recursive: true, force: true });
17
+ });
18
+ it("installs missing files", () => {
19
+ writeFileSync(join(sourceDir, "review-pr.md"), "# review");
20
+ const result = installBundledFiles(sourceDir, targetDir);
21
+ expect(result.installed).toEqual(["review-pr.md"]);
22
+ expect(readFileSync(join(targetDir, "review-pr.md"), "utf-8")).toBe("# review");
23
+ });
24
+ it("skips existing files with identical content", () => {
25
+ writeFileSync(join(sourceDir, "review-pr.md"), "# bundled");
26
+ mkdirSync(targetDir, { recursive: true });
27
+ writeFileSync(join(targetDir, "review-pr.md"), "# bundled");
28
+ const result = installBundledFiles(sourceDir, targetDir);
29
+ expect(result.skipped).toEqual(["review-pr.md"]);
30
+ expect(readFileSync(join(targetDir, "review-pr.md"), "utf-8")).toBe("# bundled");
31
+ });
32
+ it("updates an existing file when the bundled content differs", () => {
33
+ writeFileSync(join(sourceDir, "review-pr.md"), "# v2");
34
+ mkdirSync(targetDir, { recursive: true });
35
+ writeFileSync(join(targetDir, "review-pr.md"), "# v1");
36
+ const result = installBundledFiles(sourceDir, targetDir);
37
+ expect(result.updated).toEqual(["review-pr.md"]);
38
+ expect(readFileSync(join(targetDir, "review-pr.md"), "utf-8")).toBe("# v2");
39
+ });
40
+ it("never deletes a user-customized file", () => {
41
+ writeFileSync(join(sourceDir, "review-pr.md"), "# bundled");
42
+ mkdirSync(targetDir, { recursive: true });
43
+ writeFileSync(join(targetDir, "review-pr.md"), "# user customization");
44
+ const result = installBundledFiles(sourceDir, targetDir);
45
+ expect(result.updated).toEqual(["review-pr.md"]);
46
+ expect(existsSync(join(targetDir, "review-pr.md"))).toBe(true);
47
+ });
48
+ it("ignores non-markdown files", () => {
49
+ writeFileSync(join(sourceDir, "notes.txt"), "notes");
50
+ const result = installBundledFiles(sourceDir, targetDir);
51
+ expect(result.installed).toEqual([]);
52
+ expect(existsSync(join(targetDir, "notes.txt"))).toBe(false);
53
+ });
54
+ it("returns empty result for missing source dir", () => {
55
+ const result = installBundledFiles(join(sourceDir, "missing"), targetDir);
56
+ expect(result).toEqual({ installed: [], skipped: [], updated: [] });
57
+ });
58
+ });
@@ -9,3 +9,5 @@
9
9
  export declare function getUserConfigDir(): string;
10
10
  export declare function getGlobalHookDir(): string;
11
11
  export declare function getProjectHookDir(directory: string): string;
12
+ export declare function getGlobalCommandDir(): string;
13
+ export declare function getGlobalAgentDir(): string;
@@ -30,3 +30,9 @@ export function getGlobalHookDir() {
30
30
  export function getProjectHookDir(directory) {
31
31
  return join(directory, ".opencode", "hook");
32
32
  }
33
+ export function getGlobalCommandDir() {
34
+ return join(getUserConfigDir(), "opencode", "commands");
35
+ }
36
+ export function getGlobalAgentDir() {
37
+ return join(getUserConfigDir(), "opencode", "agents");
38
+ }
@@ -86,6 +86,22 @@ describe("config-paths", () => {
86
86
  expect(getGlobalHookDir()).toBe("/custom/config/opencode/hook");
87
87
  });
88
88
  });
89
+ describe("getGlobalCommandDir", () => {
90
+ it("should return correct path on Linux", async () => {
91
+ Object.defineProperty(process, "platform", { value: "linux" });
92
+ delete process.env.XDG_CONFIG_HOME;
93
+ const { getGlobalCommandDir } = await import("./config-paths");
94
+ expect(getGlobalCommandDir()).toBe(join(homedir(), ".config", "opencode", "commands"));
95
+ });
96
+ });
97
+ describe("getGlobalAgentDir", () => {
98
+ it("should return correct path on Linux", async () => {
99
+ Object.defineProperty(process, "platform", { value: "linux" });
100
+ delete process.env.XDG_CONFIG_HOME;
101
+ const { getGlobalAgentDir } = await import("./config-paths");
102
+ expect(getGlobalAgentDir()).toBe(join(homedir(), ".config", "opencode", "agents"));
103
+ });
104
+ });
89
105
  describe("getProjectHookDir", () => {
90
106
  it("should return correct path for project directory", async () => {
91
107
  const { getProjectHookDir } = await import("./config-paths");
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { type Plugin } from "@opencode-ai/plugin";
1
+ import { Plugin } from "@opencode/plugin";
2
2
  export { parseFrontmatter, loadAgents, loadCommands, type LoadedSkill } from "./loaders";
3
3
  export { buildSkillActivationBlock } from "./skill-activation";
4
- declare const SmartfrogPlugin: Plugin;
5
- export default SmartfrogPlugin;
4
+ declare const _default: Plugin.Plugin;
5
+ export default _default;