@weavetab/plugin-github 0.0.0-stage → 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fy2ne
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,137 @@
1
- # Temporary Holding Version
1
+ # `@weavetab/plugin-github`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > Official [Weavetab](https://weavetab.dev) plugin that gives the agent first-class GitHub
4
+ > access — analyze repositories, read raw source, search and inspect issues, and examine pull
5
+ > requests — **without burning browser turns**.
6
+
7
+ [![Trust: Official](https://img.shields.io/badge/trust-official-1e3a8a.svg?style=flat-square)](https://weavetab.dev)
8
+ [![Engine: Weavetab ^2.5.1](https://img.shields.io/badge/engine-weavetab_%5E2.5.1-black.svg?style=flat-square)](https://weavetab.dev)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](LICENSE)
10
+
11
+ ---
12
+
13
+ ## Why
14
+
15
+ GitHub support ships as an official plugin rather than a built-in tool: the core server stays lean
16
+ at **41 built-in tools**, and GitHub is delivered as the **independently versioned**
17
+ `@weavetab/plugin-github` package that can be installed, updated and removed like any other plugin.
18
+ It registers a single `github` tool (actions: `analyze`, `read`, `issues`, `pr`).
19
+
20
+ All GitHub data is fetched directly over HTTP (`api.github.com` / `raw.githubusercontent.com`),
21
+ so the active browser tab is **never navigated or mutated** and no browser turns are consumed.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ weavetab plugin add github # resolves to the official @weavetab/plugin-github
27
+ # or explicitly
28
+ weavetab plugin add @weavetab/plugin-github
29
+ ```
30
+
31
+ Because the package name starts with `@weavetab/plugin-`, it is automatically treated as
32
+ **Official** — fully trusted, no verification prompt.
33
+
34
+ Restart the Weavetab MCP server to activate the tool.
35
+
36
+ ## The `github` tool
37
+
38
+ A single, well-organized action-dispatch tool.
39
+
40
+ | Parameter | Applies to | Description |
41
+ |---|---|---|
42
+ | `action` *(required)* | all | `analyze` · `read` · `issues` · `pr` |
43
+ | `repo` | all | `owner/repo` or a full `github.com` URL. Omit to use the active tab's repo. |
44
+ | `paths` | `read` | File paths relative to the repo root. |
45
+ | `line_range` | `read` | Optional `[start, end]`, 1-indexed. |
46
+ | `number` | `issues`, `pr` | Issue / PR number for full detail + comments. |
47
+ | `search` | `issues` | Free-text query (GitHub Search API, scoped to the repo). |
48
+ | `labels` | `issues` | Restrict to issues carrying these labels. |
49
+ | `state` | `issues` | `open` · `closed` · `all` |
50
+ | `limit` | `issues` | Max issues returned (default `5`). |
51
+
52
+ ### Actions
53
+
54
+ - **`analyze`** — repository metadata, recursive default-branch file tree, and a README snippet.
55
+ Results are cached in the plugin's isolated storage (TTL configurable).
56
+ - **`read`** — raw contents of the requested `paths`, with optional line ranges and automatic
57
+ truncation for large files.
58
+ - **`issues`** — inspect one issue with its comments (`number`), or list / search issues.
59
+ - **`pr`** — inspect one pull request with changed files, diff stats and comments (`number`),
60
+ or list recent pull requests.
61
+
62
+ ### Examples
63
+
64
+ ```jsonc
65
+ // Analyze a repo
66
+ { "action": "analyze", "repo": "weavetab/mcp" }
67
+
68
+ // Read two files, lines 1-40 of the first
69
+ { "action": "read", "repo": "weavetab/mcp", "paths": ["src/index.ts", "README.md"], "line_range": [1, 40] }
70
+
71
+ // Search issues by label
72
+ { "action": "issues", "repo": "weavetab/mcp", "search": "rate limit", "labels": ["bug"], "state": "open" }
73
+
74
+ // Inspect PR #42
75
+ { "action": "pr", "repo": "weavetab/mcp", "number": 42 }
76
+ ```
77
+
78
+ ## Configuration
79
+
80
+ Written to `~/.weavetab/plugins/@weavetab_plugin-github/config.json` on first load:
81
+
82
+ ```json
83
+ {
84
+ "cacheTtlMs": 1800000,
85
+ "maxReadLines": 200,
86
+ "defaultIssueLimit": 5
87
+ }
88
+ ```
89
+
90
+ | Key | Default | Meaning |
91
+ |---|---|---|
92
+ | `cacheTtlMs` | `1800000` (30 min) | How long an `analyze` result stays cached. |
93
+ | `maxReadLines` | `200` | Files longer than this are truncated when no `line_range` is given. |
94
+ | `defaultIssueLimit` | `5` | Default `limit` for the `issues` action. |
95
+
96
+ ## Permissions
97
+
98
+ | Permission | Why |
99
+ |---|---|
100
+ | `config.read` / `config.write` | Read and persist isolated plugin settings. |
101
+ | `cdp.evaluate` | Read the active tab URL **only**, to infer the target repo when `repo` is omitted. |
102
+ | `network: api.github.com`, `raw.githubusercontent.com`, `github.com` | Fetch public repo data over HTTP. |
103
+ | `tools.add` | Register the single `github` tool. |
104
+
105
+ No filesystem access beyond the plugin's own sandbox, no HUD/overlay injection, no tab mutation.
106
+
107
+ ## Development
108
+
109
+ ```bash
110
+ npm install
111
+ npm run build # tsc -> dist/
112
+ npm test # node:test manifest + schema + handler smoke tests
113
+ npm run dev # tsc --watch
114
+ ```
115
+
116
+ ### Project layout
117
+
118
+ ```text
119
+ plugin-github/
120
+ ├── src/index.ts # definePlugin + registerTool("github")
121
+ ├── src/github.ts # action dispatcher
122
+ ├── src/{analyze,read,issues,pr}.ts
123
+ ├── src/{repoContext,cache,rateLimiter,types}.ts
124
+ ├── weavetab.json # manifest (permissions + tool schema)
125
+ ├── test/ # node:test smoke tests
126
+ └── dist/ # compiled output (generated)
127
+ ```
128
+
129
+ > **Local development note.** Inside the Weavetab monorepo this package pins
130
+ > `"@weavetab/sdk": "file:../../sdk"` so it builds against the in-repo SDK. In the standalone
131
+ > published repository, pin the released SDK instead: `"@weavetab/sdk": "^2.5.1"`.
132
+
133
+ ---
134
+
135
+ ## License
136
+
137
+ MIT © [Weavetab](https://github.com/Weavetab) & [fy2ne](https://github.com/fy2ne)
@@ -0,0 +1,6 @@
1
+ import type { GithubError, GithubRepoCache, GithubRunContext } from "./types.js";
2
+ /**
3
+ * `analyze` action — repository metadata, recursive default-branch file tree and
4
+ * a README snippet. Results are cached in the plugin's isolated storage.
5
+ */
6
+ export declare function githubAnalyze(ctx: GithubRunContext, repoSlug?: unknown): Promise<GithubRepoCache | GithubError>;
@@ -0,0 +1,100 @@
1
+ import { getRepoContext, isGithubError } from "./repoContext.js";
2
+ import { loadRepoCache, saveRepoCache } from "./cache.js";
3
+ import { canMakeApiCall, handleApiResponse } from "./rateLimiter.js";
4
+ const README_CANDIDATES = ["README.md", "README", "readme.md", "Readme.md", "README.txt"];
5
+ const API_HEADERS = {
6
+ Accept: "application/vnd.github.v3+json",
7
+ "User-Agent": "Weavetab-GitHub-Plugin",
8
+ };
9
+ /**
10
+ * `analyze` action — repository metadata, recursive default-branch file tree and
11
+ * a README snippet. Results are cached in the plugin's isolated storage.
12
+ */
13
+ export async function githubAnalyze(ctx, repoSlug) {
14
+ const context = await getRepoContext(ctx.session, repoSlug);
15
+ if (isGithubError(context))
16
+ return context;
17
+ const { owner, repo } = context;
18
+ const cached = await loadRepoCache(owner, repo, ctx.store, ctx.cacheTtlMs);
19
+ if (cached)
20
+ return cached;
21
+ const rateLimitCheck = canMakeApiCall();
22
+ if (isGithubError(rateLimitCheck))
23
+ return rateLimitCheck;
24
+ // 1. Repo metadata
25
+ const repoRes = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
26
+ headers: API_HEADERS,
27
+ });
28
+ const handledRepoRes = handleApiResponse(repoRes);
29
+ if (isGithubError(handledRepoRes))
30
+ return handledRepoRes;
31
+ if (repoRes.status === 404) {
32
+ return {
33
+ error: "REPO_NOT_FOUND",
34
+ message: "Repository not found or not accessible. Note: only public repositories are supported.",
35
+ };
36
+ }
37
+ if (!repoRes.ok) {
38
+ return { error: "API_ERROR", message: `GitHub API error: ${repoRes.statusText}` };
39
+ }
40
+ const repoData = await repoRes.json();
41
+ const default_branch = repoData.default_branch || "main";
42
+ const description = repoData.description || null;
43
+ const language = repoData.language || null;
44
+ // 2. Recursive file tree
45
+ const rateLimitCheck2 = canMakeApiCall();
46
+ if (isGithubError(rateLimitCheck2))
47
+ return rateLimitCheck2;
48
+ let file_tree = [];
49
+ const treeRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/${default_branch}?recursive=1`, { headers: API_HEADERS });
50
+ const handledTreeRes = handleApiResponse(treeRes);
51
+ if (isGithubError(handledTreeRes))
52
+ return handledTreeRes;
53
+ if (treeRes.ok) {
54
+ const treeData = await treeRes.json();
55
+ if (Array.isArray(treeData.tree)) {
56
+ // Flat structure — compact and easy to grep.
57
+ file_tree = treeData.tree.map((t) => ({
58
+ path: t.path,
59
+ mode: t.mode,
60
+ type: t.type,
61
+ sha: t.sha,
62
+ size: t.size,
63
+ url: t.url,
64
+ }));
65
+ }
66
+ }
67
+ // 3. README snippet (first 100 lines)
68
+ let readme_snippet = "";
69
+ const readmeEntry = file_tree.find((f) => README_CANDIDATES.includes(f.path));
70
+ if (readmeEntry) {
71
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${default_branch}/${readmeEntry.path}`;
72
+ try {
73
+ const controller = new AbortController();
74
+ const timeout = setTimeout(() => controller.abort(), 10_000);
75
+ const rawRes = await fetch(rawUrl, { signal: controller.signal });
76
+ clearTimeout(timeout);
77
+ if (rawRes.ok) {
78
+ const lines = (await rawRes.text()).split("\n");
79
+ readme_snippet = lines.slice(0, 100).join("\n");
80
+ if (lines.length > 100)
81
+ readme_snippet += "\n\n... (truncated)";
82
+ }
83
+ }
84
+ catch {
85
+ /* best-effort — README is optional */
86
+ }
87
+ }
88
+ const cacheData = {
89
+ owner,
90
+ repo,
91
+ default_branch,
92
+ description,
93
+ language,
94
+ file_tree,
95
+ readme_snippet,
96
+ analyzed_at: Date.now(),
97
+ };
98
+ await saveRepoCache(cacheData, ctx.store);
99
+ return cacheData;
100
+ }
@@ -0,0 +1,9 @@
1
+ import type { PluginStorage } from "@weavetab/sdk";
2
+ import type { GithubRepoCache } from "./types.js";
3
+ /**
4
+ * Load a previously analyzed repository from the plugin's isolated storage.
5
+ * Returns null when absent, malformed, or older than `ttlMs`.
6
+ */
7
+ export declare function loadRepoCache(owner: string, repo: string, store: PluginStorage, ttlMs: number): Promise<GithubRepoCache | null>;
8
+ /** Persist an analysis result into the plugin's isolated storage. */
9
+ export declare function saveRepoCache(data: GithubRepoCache, store: PluginStorage): Promise<void>;
package/dist/cache.js ADDED
@@ -0,0 +1,23 @@
1
+ const CACHE_SUBDIR = "github_cache";
2
+ function safeSegment(value) {
3
+ return value.replace(/[^a-zA-Z0-9.-]/g, "_");
4
+ }
5
+ function cacheFile(owner, repo) {
6
+ return `${CACHE_SUBDIR}/${safeSegment(owner)}_${safeSegment(repo)}.json`;
7
+ }
8
+ /**
9
+ * Load a previously analyzed repository from the plugin's isolated storage.
10
+ * Returns null when absent, malformed, or older than `ttlMs`.
11
+ */
12
+ export async function loadRepoCache(owner, repo, store, ttlMs) {
13
+ const data = await store.getJSON(cacheFile(owner, repo));
14
+ if (!data || typeof data.analyzed_at !== "number")
15
+ return null;
16
+ if (Date.now() - data.analyzed_at > ttlMs)
17
+ return null; // stale
18
+ return data;
19
+ }
20
+ /** Persist an analysis result into the plugin's isolated storage. */
21
+ export async function saveRepoCache(data, store) {
22
+ await store.setJSON(cacheFile(data.owner, data.repo), data);
23
+ }
@@ -0,0 +1,11 @@
1
+ import type { GithubError, GithubRunContext, GithubToolArgs } from "./types.js";
2
+ import { githubAnalyze } from "./analyze.js";
3
+ import { githubRead } from "./read.js";
4
+ import { githubIssues } from "./issues.js";
5
+ import { githubGetPr } from "./pr.js";
6
+ /**
7
+ * Single entry point for the `github` MCP tool. Dispatches to the matching
8
+ * action handler and funnels every result through a plain serializable value.
9
+ */
10
+ export declare function runGithub(ctx: GithubRunContext, args: GithubToolArgs): Promise<any | GithubError>;
11
+ export { githubAnalyze, githubRead, githubIssues, githubGetPr };
package/dist/github.js ADDED
@@ -0,0 +1,41 @@
1
+ import { githubAnalyze } from "./analyze.js";
2
+ import { githubRead } from "./read.js";
3
+ import { githubIssues } from "./issues.js";
4
+ import { githubGetPr } from "./pr.js";
5
+ /**
6
+ * Single entry point for the `github` MCP tool. Dispatches to the matching
7
+ * action handler and funnels every result through a plain serializable value.
8
+ */
9
+ export async function runGithub(ctx, args) {
10
+ switch (args.action) {
11
+ case "analyze":
12
+ return githubAnalyze(ctx, args.repo);
13
+ case "read": {
14
+ const paths = Array.isArray(args.paths) ? args.paths.filter((p) => typeof p === "string") : [];
15
+ if (paths.length === 0) {
16
+ return {
17
+ error: "MISSING_PATHS",
18
+ message: "action 'read' requires a 'paths' array with at least one file path.",
19
+ };
20
+ }
21
+ return githubRead(ctx, { repo: args.repo, paths, line_range: args.line_range });
22
+ }
23
+ case "issues":
24
+ return githubIssues(ctx, {
25
+ repo: args.repo,
26
+ search: args.search,
27
+ number: args.number,
28
+ labels: args.labels,
29
+ state: args.state,
30
+ limit: args.limit,
31
+ });
32
+ case "pr":
33
+ return githubGetPr(ctx, { repo: args.repo, number: args.number, state: args.state, limit: args.limit });
34
+ default:
35
+ return {
36
+ error: "INVALID_ACTION",
37
+ message: `Unknown action: ${args.action}. Must be 'analyze', 'read', 'issues', or 'pr'.`,
38
+ };
39
+ }
40
+ }
41
+ export { githubAnalyze, githubRead, githubIssues, githubGetPr };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @weavetab/plugin-github
3
+ * Official Weavetab GitHub plugin — repository analysis, raw file reads, issue
4
+ * search/inspection and pull request inspection over the GitHub REST API.
5
+ *
6
+ * All GitHub data is fetched over HTTP, so no browser turns are consumed and the
7
+ * active tab is never navigated or mutated.
8
+ *
9
+ * Author: fy2ne
10
+ * License: MIT
11
+ */
12
+ export * from "./types.js";
13
+ export { runGithub } from "./github.js";
14
+ export declare const githubPlugin: import("@weavetab/sdk").WeavetabPluginDefinition;
15
+ export default githubPlugin;
16
+ export declare const plugin: import("@weavetab/sdk").WeavetabPluginDefinition;
package/dist/index.js ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @weavetab/plugin-github
3
+ * Official Weavetab GitHub plugin — repository analysis, raw file reads, issue
4
+ * search/inspection and pull request inspection over the GitHub REST API.
5
+ *
6
+ * All GitHub data is fetched over HTTP, so no browser turns are consumed and the
7
+ * active tab is never navigated or mutated.
8
+ *
9
+ * Author: fy2ne
10
+ * License: MIT
11
+ */
12
+ import { definePlugin, defineTool, z } from "@weavetab/sdk";
13
+ import { runGithub } from "./github.js";
14
+ export * from "./types.js";
15
+ export { runGithub } from "./github.js";
16
+ const DEFAULT_CACHE_TTL_MS = 30 * 60 * 1000;
17
+ const DEFAULT_MAX_READ_LINES = 200;
18
+ const DEFAULT_ISSUE_LIMIT = 5;
19
+ const TOOL_DESCRIPTION = "Interact with GitHub repositories without burning browser turns. " +
20
+ "Analyze a repo's file tree and README, read raw source files, list/search/inspect issues, " +
21
+ "and examine pull request details, changed files and comments — all fetched over the GitHub REST API.";
22
+ /** Coerce a possibly-string config value into a positive integer. */
23
+ function positiveInt(value, fallback) {
24
+ const n = typeof value === "string" ? parseInt(value, 10) : value;
25
+ return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
26
+ }
27
+ export const githubPlugin = definePlugin({
28
+ name: "@weavetab/plugin-github",
29
+ version: "1.0.0",
30
+ description: "Official Weavetab GitHub plugin — analyze repositories, read raw source, inspect issues and pull requests over the GitHub API.",
31
+ defaultConfig: {
32
+ cacheTtlMs: DEFAULT_CACHE_TTL_MS,
33
+ maxReadLines: DEFAULT_MAX_READ_LINES,
34
+ defaultIssueLimit: DEFAULT_ISSUE_LIMIT,
35
+ },
36
+ async onLoad(ctx) {
37
+ const cacheTtlMs = positiveInt(ctx.config?.cacheTtlMs, DEFAULT_CACHE_TTL_MS);
38
+ const maxReadLines = positiveInt(ctx.config?.maxReadLines, DEFAULT_MAX_READ_LINES);
39
+ const defaultIssueLimit = positiveInt(ctx.config?.defaultIssueLimit, DEFAULT_ISSUE_LIMIT);
40
+ ctx.mcp.registerTool(defineTool({
41
+ name: "github",
42
+ description: TOOL_DESCRIPTION,
43
+ requiresSession: false,
44
+ schema: {
45
+ action: z
46
+ .enum(["analyze", "read", "issues", "pr"])
47
+ .describe("The GitHub operation to perform. 'analyze' = repo metadata, file tree & README; 'read' = raw file contents (needs 'paths'); 'issues' = list/search/inspect issues; 'pr' = list/inspect pull requests."),
48
+ repo: z
49
+ .string()
50
+ .optional()
51
+ .describe("Target repository as 'owner/repo' or a full github.com URL. Optional — when omitted, it is inferred from the active browser tab."),
52
+ paths: z
53
+ .array(z.string())
54
+ .optional()
55
+ .describe("READ: file paths relative to the repo root, e.g. ['src/index.ts', 'README.md']."),
56
+ line_range: z
57
+ .array(z.number())
58
+ .length(2)
59
+ .optional()
60
+ .describe("READ: optional [start, end] 1-indexed line range per file."),
61
+ number: z
62
+ .number()
63
+ .optional()
64
+ .describe("ISSUES / PR: issue or pull request number to fetch full detail and comments."),
65
+ search: z
66
+ .string()
67
+ .optional()
68
+ .describe("ISSUES: free-text query to search issues in the target repository."),
69
+ labels: z.array(z.string()).optional().describe("ISSUES: restrict results to these labels."),
70
+ state: z
71
+ .enum(["open", "closed", "all"])
72
+ .optional()
73
+ .describe("ISSUES / PR: filter by state."),
74
+ limit: z
75
+ .number()
76
+ .optional()
77
+ .describe(`ISSUES / PR: max items to return when listing/searching (default ${DEFAULT_ISSUE_LIMIT} for issues, 10 for pull requests).`),
78
+ },
79
+ handler: async (args, session) => {
80
+ const result = await runGithub({
81
+ session: session,
82
+ store: ctx.storage,
83
+ cacheTtlMs,
84
+ maxReadLines,
85
+ defaultIssueLimit,
86
+ }, args);
87
+ return {
88
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
89
+ };
90
+ },
91
+ }));
92
+ ctx.logger?.info(`@weavetab/plugin-github ready — tool "github" registered (cacheTtlMs=${cacheTtlMs}, maxReadLines=${maxReadLines}, defaultIssueLimit=${defaultIssueLimit}).`);
93
+ },
94
+ async onUnload(ctx) {
95
+ ctx.logger?.info("@weavetab/plugin-github unloaded.");
96
+ },
97
+ });
98
+ export default githubPlugin;
99
+ export const plugin = githubPlugin;
@@ -0,0 +1,15 @@
1
+ import type { GithubRunContext } from "./types.js";
2
+ interface IssueArgs {
3
+ repo?: string;
4
+ search?: string;
5
+ number?: number;
6
+ labels?: string[];
7
+ state?: "open" | "closed" | "all";
8
+ limit?: number;
9
+ }
10
+ /**
11
+ * `issues` action — inspect a single issue (with comments) when `number` is
12
+ * given, otherwise list or search issues in the target repository.
13
+ */
14
+ export declare function githubIssues(ctx: GithubRunContext, args: IssueArgs): Promise<any>;
15
+ export {};
package/dist/issues.js ADDED
@@ -0,0 +1,94 @@
1
+ import { getRepoContext, isGithubError } from "./repoContext.js";
2
+ import { canMakeApiCall, handleApiResponse } from "./rateLimiter.js";
3
+ const API_HEADERS = {
4
+ Accept: "application/vnd.github.v3+json",
5
+ "User-Agent": "Weavetab-GitHub-Plugin",
6
+ };
7
+ /**
8
+ * `issues` action — inspect a single issue (with comments) when `number` is
9
+ * given, otherwise list or search issues in the target repository.
10
+ */
11
+ export async function githubIssues(ctx, args) {
12
+ const context = await getRepoContext(ctx.session, args.repo);
13
+ if (isGithubError(context))
14
+ return context;
15
+ const { owner, repo } = context;
16
+ const limit = args.limit || ctx.defaultIssueLimit;
17
+ const rateLimitCheck = canMakeApiCall();
18
+ if (isGithubError(rateLimitCheck))
19
+ return rateLimitCheck;
20
+ // Single issue + comments
21
+ if (args.number) {
22
+ const issueRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues/${args.number}`, { headers: API_HEADERS });
23
+ const handledIssueRes = handleApiResponse(issueRes);
24
+ if (isGithubError(handledIssueRes))
25
+ return handledIssueRes;
26
+ if (!issueRes.ok) {
27
+ return {
28
+ error: "FETCH_FAILED",
29
+ status: issueRes.status,
30
+ message: `Failed to fetch issue #${args.number}`,
31
+ };
32
+ }
33
+ const issueData = await issueRes.json();
34
+ let comments = [];
35
+ if (issueData.comments > 0) {
36
+ const rateLimitCheck2 = canMakeApiCall();
37
+ if (!isGithubError(rateLimitCheck2)) {
38
+ const commentsRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues/${args.number}/comments`, { headers: API_HEADERS });
39
+ const handledCommentsRes = handleApiResponse(commentsRes);
40
+ if (!isGithubError(handledCommentsRes) && commentsRes.ok) {
41
+ const commentsData = await commentsRes.json();
42
+ comments = commentsData.map((c) => ({
43
+ id: c.id,
44
+ user: c.user?.login,
45
+ body: c.body,
46
+ created_at: c.created_at,
47
+ }));
48
+ }
49
+ }
50
+ }
51
+ return {
52
+ number: issueData.number,
53
+ title: issueData.title,
54
+ state: issueData.state,
55
+ user: issueData.user?.login,
56
+ body: issueData.body,
57
+ created_at: issueData.created_at,
58
+ labels: issueData.labels?.map((l) => l.name),
59
+ comments,
60
+ };
61
+ }
62
+ // List or search issues
63
+ let urlStr = `https://api.github.com/repos/${owner}/${repo}/issues?per_page=${limit}`;
64
+ if (args.state)
65
+ urlStr += `&state=${args.state}`;
66
+ if (args.labels && args.labels.length > 0)
67
+ urlStr += `&labels=${args.labels.join(",")}`;
68
+ if (args.search) {
69
+ let q = `repo:${owner}/${repo} is:issue ${args.search}`;
70
+ if (args.state && args.state !== "all")
71
+ q += ` is:${args.state}`;
72
+ if (args.labels)
73
+ args.labels.forEach((l) => (q += ` label:"${l}"`));
74
+ urlStr = `https://api.github.com/search/issues?q=${encodeURIComponent(q)}&per_page=${limit}`;
75
+ }
76
+ const listRes = await fetch(urlStr, { headers: API_HEADERS });
77
+ const handledListRes = handleApiResponse(listRes);
78
+ if (isGithubError(handledListRes))
79
+ return handledListRes;
80
+ if (!listRes.ok) {
81
+ return { error: "FETCH_FAILED", status: listRes.status, message: "Failed to list/search issues" };
82
+ }
83
+ const listData = await listRes.json();
84
+ const items = args.search ? listData.items : listData;
85
+ return items.map((i) => ({
86
+ number: i.number,
87
+ title: i.title,
88
+ state: i.state,
89
+ user: i.user?.login,
90
+ labels: i.labels?.map((l) => l.name),
91
+ created_at: i.created_at,
92
+ comments_count: i.comments,
93
+ }));
94
+ }
package/dist/pr.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { GithubRunContext } from "./types.js";
2
+ interface PrArgs {
3
+ repo?: string;
4
+ number?: number;
5
+ state?: "open" | "closed" | "all";
6
+ limit?: number;
7
+ }
8
+ /**
9
+ * `pr` action — inspect a single pull request (changed files, diff stats and
10
+ * comments) when `number` is given, otherwise list recent pull requests.
11
+ */
12
+ export declare function githubGetPr(ctx: GithubRunContext, args: PrArgs): Promise<any>;
13
+ export {};
package/dist/pr.js ADDED
@@ -0,0 +1,97 @@
1
+ import { getRepoContext, isGithubError } from "./repoContext.js";
2
+ import { canMakeApiCall, handleApiResponse } from "./rateLimiter.js";
3
+ const DEFAULT_PR_LIMIT = 10;
4
+ const API_HEADERS = {
5
+ Accept: "application/vnd.github.v3+json",
6
+ "User-Agent": "Weavetab-GitHub-Plugin",
7
+ };
8
+ /**
9
+ * `pr` action — inspect a single pull request (changed files, diff stats and
10
+ * comments) when `number` is given, otherwise list recent pull requests.
11
+ */
12
+ export async function githubGetPr(ctx, args) {
13
+ const context = await getRepoContext(ctx.session, args.repo);
14
+ if (isGithubError(context))
15
+ return context;
16
+ const { owner, repo } = context;
17
+ const rateLimitCheck = canMakeApiCall();
18
+ if (isGithubError(rateLimitCheck))
19
+ return rateLimitCheck;
20
+ if (args.number) {
21
+ const prRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${args.number}`, {
22
+ headers: API_HEADERS,
23
+ });
24
+ const handledPrRes = handleApiResponse(prRes);
25
+ if (isGithubError(handledPrRes))
26
+ return handledPrRes;
27
+ if (!prRes.ok) {
28
+ return {
29
+ error: "FETCH_FAILED",
30
+ status: prRes.status,
31
+ message: `Failed to fetch PR #${args.number}`,
32
+ };
33
+ }
34
+ const prData = await prRes.json();
35
+ let files_changed = [];
36
+ const rateLimitCheck2 = canMakeApiCall();
37
+ if (!isGithubError(rateLimitCheck2)) {
38
+ const filesRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls/${args.number}/files`, { headers: API_HEADERS });
39
+ const handledFilesRes = handleApiResponse(filesRes);
40
+ if (!isGithubError(handledFilesRes) && filesRes.ok) {
41
+ const filesData = await filesRes.json();
42
+ files_changed = filesData.map((f) => f.filename);
43
+ }
44
+ }
45
+ let comments = [];
46
+ if (prData.comments > 0) {
47
+ const rateLimitCheck3 = canMakeApiCall();
48
+ if (!isGithubError(rateLimitCheck3)) {
49
+ const commentsRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/issues/${args.number}/comments`, { headers: API_HEADERS });
50
+ const handledCommentsRes = handleApiResponse(commentsRes);
51
+ if (!isGithubError(handledCommentsRes) && commentsRes.ok) {
52
+ const commentsData = await commentsRes.json();
53
+ comments = commentsData.map((c) => ({
54
+ id: c.id,
55
+ user: c.user?.login,
56
+ body: c.body,
57
+ created_at: c.created_at,
58
+ }));
59
+ }
60
+ }
61
+ }
62
+ return {
63
+ number: prData.number,
64
+ title: prData.title,
65
+ state: prData.state,
66
+ user: prData.user?.login,
67
+ body: prData.body,
68
+ created_at: prData.created_at,
69
+ merged: prData.merged,
70
+ mergeable: prData.mergeable,
71
+ additions: prData.additions,
72
+ deletions: prData.deletions,
73
+ changed_files: prData.changed_files,
74
+ files: files_changed,
75
+ comments,
76
+ };
77
+ }
78
+ // List recent pull requests
79
+ const limit = args.limit && args.limit > 0 ? Math.min(args.limit, 100) : DEFAULT_PR_LIMIT;
80
+ const state = args.state || "all";
81
+ const listRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls?state=${state}&per_page=${limit}`, { headers: API_HEADERS });
82
+ const handledListRes = handleApiResponse(listRes);
83
+ if (isGithubError(handledListRes))
84
+ return handledListRes;
85
+ if (!listRes.ok) {
86
+ return { error: "FETCH_FAILED", status: listRes.status, message: "Failed to list PRs" };
87
+ }
88
+ const listData = await listRes.json();
89
+ return listData.map((pr) => ({
90
+ number: pr.number,
91
+ title: pr.title,
92
+ state: pr.state,
93
+ user: pr.user?.login,
94
+ created_at: pr.created_at,
95
+ updated_at: pr.updated_at,
96
+ }));
97
+ }
@@ -0,0 +1,8 @@
1
+ import type { GithubError } from "./types.js";
2
+ /**
3
+ * Guard that refuses new API calls once the remaining quota drops below a safety
4
+ * threshold, so we never trip GitHub's hard rate limit mid-flow.
5
+ */
6
+ export declare function canMakeApiCall(): true | GithubError;
7
+ export declare function recordApiCall(headers: Headers): void;
8
+ export declare function handleApiResponse(response: Response): Response | GithubError;
@@ -0,0 +1,47 @@
1
+ const SAFETY_THRESHOLD = 5;
2
+ const state = { remaining: 60, resetAt: null };
3
+ /**
4
+ * Guard that refuses new API calls once the remaining quota drops below a safety
5
+ * threshold, so we never trip GitHub's hard rate limit mid-flow.
6
+ */
7
+ export function canMakeApiCall() {
8
+ const nowSec = Math.floor(Date.now() / 1000);
9
+ if (state.resetAt && nowSec >= state.resetAt) {
10
+ // We've passed the reset window, assume quota is restored.
11
+ state.remaining = 60;
12
+ state.resetAt = null;
13
+ }
14
+ if (state.remaining < SAFETY_THRESHOLD) {
15
+ const resetTime = state.resetAt ? new Date(state.resetAt * 1000).toISOString() : "unknown";
16
+ return {
17
+ error: "RATE_LIMIT_LOW",
18
+ message: `GitHub API hourly rate limit is too low (remaining: ${state.remaining}). Please wait until ${resetTime} before making more API calls. Note: read actions (api.github.com / raw.githubusercontent.com GETs) do NOT consume the rate-limited Search API quota.`,
19
+ remaining: state.remaining,
20
+ reset_at: state.resetAt,
21
+ };
22
+ }
23
+ return true;
24
+ }
25
+ export function recordApiCall(headers) {
26
+ const remainingStr = headers.get("x-ratelimit-remaining");
27
+ const resetStr = headers.get("x-ratelimit-reset");
28
+ if (remainingStr) {
29
+ state.remaining = parseInt(remainingStr, 10) || state.remaining;
30
+ }
31
+ if (resetStr) {
32
+ state.resetAt = parseInt(resetStr, 10) || state.resetAt;
33
+ }
34
+ }
35
+ export function handleApiResponse(response) {
36
+ recordApiCall(response.headers);
37
+ if (response.status === 403 || response.status === 429) {
38
+ const resetTime = state.resetAt ? new Date(state.resetAt * 1000).toISOString() : "unknown";
39
+ return {
40
+ error: "RATE_LIMIT_EXCEEDED",
41
+ message: `GitHub API rate limit exceeded. Please wait until ${resetTime}.`,
42
+ remaining: state.remaining,
43
+ reset_at: state.resetAt,
44
+ };
45
+ }
46
+ return response;
47
+ }
package/dist/read.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { GithubRunContext } from "./types.js";
2
+ interface ReadArgs {
3
+ repo?: string;
4
+ paths: string[];
5
+ line_range?: number[];
6
+ }
7
+ /**
8
+ * `read` action — fetch raw file contents from the default branch.
9
+ *
10
+ * Ensures an analysis (and thus `default_branch` + `file_tree`) exists first,
11
+ * then fetches each requested path in parallel over raw.githubusercontent.com.
12
+ */
13
+ export declare function githubRead(ctx: GithubRunContext, args: ReadArgs): Promise<any>;
14
+ export {};
package/dist/read.js ADDED
@@ -0,0 +1,69 @@
1
+ import { getRepoContext, isGithubError } from "./repoContext.js";
2
+ import { loadRepoCache } from "./cache.js";
3
+ import { githubAnalyze } from "./analyze.js";
4
+ /**
5
+ * `read` action — fetch raw file contents from the default branch.
6
+ *
7
+ * Ensures an analysis (and thus `default_branch` + `file_tree`) exists first,
8
+ * then fetches each requested path in parallel over raw.githubusercontent.com.
9
+ */
10
+ export async function githubRead(ctx, args) {
11
+ const context = await getRepoContext(ctx.session, args.repo);
12
+ if (isGithubError(context))
13
+ return context;
14
+ const { owner, repo } = context;
15
+ let cached = await loadRepoCache(owner, repo, ctx.store, ctx.cacheTtlMs);
16
+ if (!cached) {
17
+ const analyzed = await githubAnalyze(ctx, args.repo);
18
+ if (isGithubError(analyzed))
19
+ return analyzed;
20
+ cached = analyzed;
21
+ }
22
+ const default_branch = cached.default_branch;
23
+ const file_tree = cached.file_tree;
24
+ const results = [];
25
+ const warnings = [];
26
+ const fetchPromises = args.paths.map(async (rawPath) => {
27
+ const path = rawPath.replace(/\\/g, "/").replace(/^\/+/, "");
28
+ if (path.includes("..") || path.includes("\0")) {
29
+ return { path: rawPath, error: "INVALID_PATH", message: "Path traversal or null bytes detected." };
30
+ }
31
+ if (!file_tree.some((entry) => entry.path === path)) {
32
+ warnings.push(`Path '${path}' was not found in the cached file tree. Branch contents may differ.`);
33
+ }
34
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${default_branch}/${path}`;
35
+ try {
36
+ const controller = new AbortController();
37
+ const timeout = setTimeout(() => controller.abort(), 10_000);
38
+ const res = await fetch(rawUrl, { signal: controller.signal });
39
+ clearTimeout(timeout);
40
+ if (!res.ok) {
41
+ return { path, error: "FETCH_FAILED", status: res.status, message: res.statusText };
42
+ }
43
+ const lines = (await res.text()).split("\n");
44
+ const total_lines = lines.length;
45
+ let returnedContent = lines.join("\n");
46
+ let truncated = false;
47
+ if (Array.isArray(args.line_range) && args.line_range.length === 2) {
48
+ const start = Math.max(0, args.line_range[0] - 1); // 1-indexed -> 0-indexed
49
+ const end = Math.min(total_lines, args.line_range[1]);
50
+ returnedContent = lines.slice(start, end).join("\n");
51
+ truncated = start > 0 || end < total_lines;
52
+ }
53
+ else if (total_lines > ctx.maxReadLines) {
54
+ returnedContent = lines.slice(0, ctx.maxReadLines).join("\n");
55
+ truncated = true;
56
+ }
57
+ return { path, total_lines, truncated, content: returnedContent };
58
+ }
59
+ catch (err) {
60
+ return { path, error: "FETCH_ERROR", message: err?.message };
61
+ }
62
+ });
63
+ const pathResults = await Promise.all(fetchPromises);
64
+ results.push(...pathResults);
65
+ return {
66
+ warnings: warnings.length > 0 ? warnings : undefined,
67
+ files: results,
68
+ };
69
+ }
@@ -0,0 +1,21 @@
1
+ import type { CdpSession, GithubError, RepoContext } from "./types.js";
2
+ /**
3
+ * Parse a repo slug from a string that can be:
4
+ * - "owner/repo"
5
+ * - "https://github.com/owner/repo[/anything]"
6
+ *
7
+ * Returns null (instead of throwing) for any non-string input so callers never
8
+ * encounter `TypeError: slug.trim is not a function`.
9
+ */
10
+ export declare function parseRepoSlug(slug: unknown): RepoContext | null;
11
+ /**
12
+ * Resolve the current GitHub repository target.
13
+ *
14
+ * @param session - CDP session used only to read the active tab URL.
15
+ * @param repoSlug - Optional "owner/repo" or full GitHub URL. When provided, it
16
+ * is used directly and the active tab is never read or mutated.
17
+ *
18
+ * GitHub data is always fetched over HTTP, so the active tab is never navigated.
19
+ */
20
+ export declare function getRepoContext(session: CdpSession | undefined, repoSlug?: unknown): Promise<RepoContext | GithubError>;
21
+ export declare function isGithubError(result: unknown): result is GithubError;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Parse a repo slug from a string that can be:
3
+ * - "owner/repo"
4
+ * - "https://github.com/owner/repo[/anything]"
5
+ *
6
+ * Returns null (instead of throwing) for any non-string input so callers never
7
+ * encounter `TypeError: slug.trim is not a function`.
8
+ */
9
+ export function parseRepoSlug(slug) {
10
+ // Runtime guard: MCP argument dispatch can forward an object / number / null
11
+ // when the model omits the field in some call patterns.
12
+ if (typeof slug !== "string")
13
+ return null;
14
+ const trimmed = slug.trim();
15
+ if (!trimmed)
16
+ return null;
17
+ // Full GitHub URL
18
+ try {
19
+ const u = new URL(trimmed);
20
+ if (u.hostname === "github.com" || u.hostname === "www.github.com") {
21
+ const parts = u.pathname.split("/").filter(Boolean);
22
+ if (parts.length >= 2)
23
+ return { owner: parts[0], repo: parts[1] };
24
+ }
25
+ }
26
+ catch {
27
+ /* not a URL — fall through to shorthand parse */
28
+ }
29
+ // "owner/repo" shorthand
30
+ const parts = trimmed.split("/").filter(Boolean);
31
+ if (parts.length >= 2)
32
+ return { owner: parts[0], repo: parts[1] };
33
+ return null;
34
+ }
35
+ /**
36
+ * Resolve the current GitHub repository target.
37
+ *
38
+ * @param session - CDP session used only to read the active tab URL.
39
+ * @param repoSlug - Optional "owner/repo" or full GitHub URL. When provided, it
40
+ * is used directly and the active tab is never read or mutated.
41
+ *
42
+ * GitHub data is always fetched over HTTP, so the active tab is never navigated.
43
+ */
44
+ export async function getRepoContext(session, repoSlug) {
45
+ const slugStr = typeof repoSlug === "string" && repoSlug.trim() !== "" ? repoSlug.trim() : undefined;
46
+ // Explicit slug wins over the current tab URL.
47
+ if (slugStr) {
48
+ const parsed = parseRepoSlug(slugStr);
49
+ if (!parsed) {
50
+ return {
51
+ error: "INVALID_REPO_SLUG",
52
+ message: `Cannot parse a GitHub owner/repo from "${slugStr}". Use "owner/repo" or a full github.com URL.`,
53
+ };
54
+ }
55
+ return parsed;
56
+ }
57
+ if (!session) {
58
+ return {
59
+ error: "NO_CONTEXT",
60
+ message: "No active browser session. Pass a 'repo' argument (e.g. \"owner/repo\") or open a github.com repository in the browser.",
61
+ };
62
+ }
63
+ let urlStr;
64
+ try {
65
+ const { result } = await session.Runtime.evaluate({
66
+ expression: "window.location.href",
67
+ returnByValue: true,
68
+ });
69
+ urlStr = result.value;
70
+ }
71
+ catch {
72
+ return {
73
+ error: "CONTEXT_ERROR",
74
+ message: "Failed to evaluate window.location.href on the active tab.",
75
+ };
76
+ }
77
+ if (typeof urlStr !== "string" || !urlStr) {
78
+ return { error: "CONTEXT_ERROR", message: "Active tab URL is empty." };
79
+ }
80
+ let url;
81
+ try {
82
+ url = new URL(urlStr);
83
+ }
84
+ catch {
85
+ return { error: "INVALID_URL", message: "Active tab URL is invalid." };
86
+ }
87
+ if (url.hostname !== "github.com") {
88
+ return {
89
+ error: "NOT_ON_GITHUB_REPO",
90
+ message: "Navigate to a GitHub repository first, or pass a 'repo' argument (e.g. \"owner/repo\").",
91
+ };
92
+ }
93
+ const parts = url.pathname.split("/").filter(Boolean);
94
+ if (parts.length < 2) {
95
+ return {
96
+ error: "NOT_ON_GITHUB_REPO",
97
+ message: "Navigate to a specific GitHub repository first (e.g. github.com/owner/repo).",
98
+ };
99
+ }
100
+ return { owner: parts[0], repo: parts[1] };
101
+ }
102
+ export function isGithubError(result) {
103
+ return (!!result &&
104
+ typeof result === "object" &&
105
+ "error" in result &&
106
+ "message" in result &&
107
+ typeof result.message === "string");
108
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Shared types for @weavetab/plugin-github.
3
+ *
4
+ * The plugin is intentionally decoupled from the Weavetab MCP internals: it only
5
+ * depends on the minimal CDP surface it actually uses (`session.Runtime.evaluate`)
6
+ * and on the isolated storage contract exposed by the plugin runtime.
7
+ */
8
+ import type { PluginStorage } from "@weavetab/sdk";
9
+ /** Minimal CDP client surface used to read the active tab URL. */
10
+ export interface CdpSession {
11
+ Runtime: {
12
+ evaluate(params: {
13
+ expression: string;
14
+ returnByValue?: boolean;
15
+ }): Promise<{
16
+ result: {
17
+ value: unknown;
18
+ };
19
+ }>;
20
+ };
21
+ }
22
+ /** A resolved GitHub repository target. */
23
+ export interface RepoContext {
24
+ owner: string;
25
+ repo: string;
26
+ }
27
+ /** Structured, non-throwing error returned to the MCP layer. */
28
+ export interface GithubError {
29
+ error: string;
30
+ message: string;
31
+ [key: string]: unknown;
32
+ }
33
+ export interface TreeEntry {
34
+ path: string;
35
+ mode: string;
36
+ type: string;
37
+ sha: string;
38
+ size?: number;
39
+ url: string;
40
+ }
41
+ /** Cached result of the `analyze` action. */
42
+ export interface GithubRepoCache {
43
+ owner: string;
44
+ repo: string;
45
+ default_branch: string;
46
+ description: string | null;
47
+ language: string | null;
48
+ file_tree: TreeEntry[];
49
+ readme_snippet: string;
50
+ analyzed_at: number;
51
+ }
52
+ /** Runtime dependencies injected into every GitHub action. */
53
+ export interface GithubRunContext {
54
+ session?: CdpSession;
55
+ store: PluginStorage;
56
+ cacheTtlMs: number;
57
+ maxReadLines: number;
58
+ defaultIssueLimit: number;
59
+ }
60
+ /** The single tool's argument shape. */
61
+ export interface GithubToolArgs {
62
+ action: "analyze" | "read" | "issues" | "pr";
63
+ repo?: string;
64
+ paths?: string[];
65
+ line_range?: number[];
66
+ number?: number;
67
+ search?: string;
68
+ labels?: string[];
69
+ state?: "open" | "closed" | "all";
70
+ limit?: number;
71
+ }
package/dist/types.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared types for @weavetab/plugin-github.
3
+ *
4
+ * The plugin is intentionally decoupled from the Weavetab MCP internals: it only
5
+ * depends on the minimal CDP surface it actually uses (`session.Runtime.evaluate`)
6
+ * and on the isolated storage contract exposed by the plugin runtime.
7
+ */
8
+ export {};
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "@weavetab/plugin-github",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Official Weavetab GitHub plugin - analyze repository structure, read raw source files, search and inspect issues, and examine pull requests over the GitHub API without burning browser turns.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "weavetab.json",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "dev": "tsc --watch",
23
+ "test": "node --test test/*.test.mjs",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "dependencies": {
27
+ "@weavetab/sdk": "^2.5.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^20.19.0",
31
+ "typescript": "^5.5.0"
32
+ },
33
+ "keywords": [
34
+ "weavetab",
35
+ "weavetab-plugin",
36
+ "weavetab-official",
37
+ "mcp",
38
+ "github",
39
+ "repository",
40
+ "issues",
41
+ "pull-requests",
42
+ "browser-automation"
43
+ ],
44
+ "author": "Weavetab (https://weavetab.dev)",
45
+ "license": "MIT",
46
+ "homepage": "https://github.com/Weavetab/plugin-github",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/Weavetab/plugin-github.git"
50
+ },
51
+ "bugs": {
52
+ "url": "https://github.com/Weavetab/plugin-github/issues"
53
+ },
54
+ "engines": {
55
+ "node": ">=18.0.0"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ }
60
+ }
package/weavetab.json ADDED
@@ -0,0 +1,114 @@
1
+ {
2
+ "$schema": "https://weavetab.dev/schema/plugin.json",
3
+ "name": "@weavetab/plugin-github",
4
+ "version": "1.0.0",
5
+ "description": "Official Weavetab GitHub plugin. Analyze repository structure, read raw source files, search and inspect issues, and examine pull requests over the public GitHub REST API — fetched directly over HTTP, so no browser turns are consumed.",
6
+ "entry": "dist/index.js",
7
+ "engines": {
8
+ "weavetab": "^2.5.1",
9
+ "node": ">=18.0.0"
10
+ },
11
+ "environments": [
12
+ "mcp"
13
+ ],
14
+ "authors": [
15
+ {
16
+ "name": "fy2ne",
17
+ "role": "Author & Maintainer",
18
+ "url": "https://github.com/fy2ne"
19
+ }
20
+ ],
21
+ "license": "MIT",
22
+ "tags": [
23
+ "GitHub",
24
+ "Developer Tools",
25
+ "Repositories",
26
+ "Issues",
27
+ "Pull Requests"
28
+ ],
29
+ "links": {
30
+ "github": "https://github.com/weavetab/plugin-github",
31
+ "npm": "https://www.npmjs.com/package/@weavetab/plugin-github"
32
+ },
33
+ "defaultConfig": {
34
+ "cacheTtlMs": 1800000,
35
+ "maxReadLines": 200,
36
+ "defaultIssueLimit": 5
37
+ },
38
+ "permissions": {
39
+ "config": {
40
+ "read": true,
41
+ "write": true,
42
+ "reason": "Reads and persists the isolated plugin configuration (cache TTL, read limits, default issue limit) at ~/.weavetab/plugins/@weavetab_plugin-github/config.json."
43
+ },
44
+ "cdp": {
45
+ "evaluate": true,
46
+ "reason": "Reads the active tab URL only, to infer the target repository when the optional 'repo' argument is omitted. Never navigates or mutates the page."
47
+ },
48
+ "network": {
49
+ "domains": [
50
+ "api.github.com",
51
+ "raw.githubusercontent.com",
52
+ "github.com"
53
+ ],
54
+ "reason": "Fetches repository metadata, file trees, raw source files, issues and pull requests from the public GitHub REST API and raw content CDN."
55
+ },
56
+ "tools": {
57
+ "add": true,
58
+ "reason": "Registers the single 'github' MCP tool."
59
+ }
60
+ },
61
+ "tools": [
62
+ {
63
+ "name": "github",
64
+ "description": "Interact with GitHub repositories without burning browser turns. Analyze a repo's file tree and README, read raw source files, list/search/inspect issues, and examine pull request details, changed files and comments — all fetched over the GitHub REST API.",
65
+ "inputSchema": {
66
+ "type": "object",
67
+ "properties": {
68
+ "action": {
69
+ "type": "string",
70
+ "enum": ["analyze", "read", "issues", "pr"],
71
+ "description": "The GitHub operation to perform. 'analyze' = repository metadata, recursive file tree and README snippet. 'read' = raw file contents (requires 'paths'). 'issues' = list, search or inspect issues. 'pr' = list or inspect pull requests."
72
+ },
73
+ "repo": {
74
+ "type": "string",
75
+ "description": "Target repository as an 'owner/repo' slug or a full https://github.com/owner/repo URL. Optional — when omitted, the repository is inferred from the active browser tab (which must be on a github.com repo page)."
76
+ },
77
+ "paths": {
78
+ "type": "array",
79
+ "items": { "type": "string" },
80
+ "description": "READ ONLY. File paths relative to the repository root to fetch (e.g. ['src/index.ts', 'README.md'])."
81
+ },
82
+ "line_range": {
83
+ "type": "array",
84
+ "items": { "type": "number" },
85
+ "description": "READ ONLY. Optional [start, end] 1-indexed line range to return from each file. When omitted, files longer than 'maxReadLines' are truncated from the top."
86
+ },
87
+ "number": {
88
+ "type": "number",
89
+ "description": "ISSUES / PR ONLY. Issue or pull request number to fetch the full detail and comments for. When omitted, a recent list is returned instead."
90
+ },
91
+ "search": {
92
+ "type": "string",
93
+ "description": "ISSUES ONLY. Free-text search query to find issues (uses the GitHub Search API, scoped to the target repo)."
94
+ },
95
+ "labels": {
96
+ "type": "array",
97
+ "items": { "type": "string" },
98
+ "description": "ISSUES ONLY. Restrict results to issues carrying all of the given labels."
99
+ },
100
+ "state": {
101
+ "type": "string",
102
+ "enum": ["open", "closed", "all"],
103
+ "description": "ISSUES ONLY. Filter by issue state."
104
+ },
105
+ "limit": {
106
+ "type": "number",
107
+ "description": "ISSUES ONLY. Maximum number of issues to return when listing or searching (default 5)."
108
+ }
109
+ },
110
+ "required": ["action"]
111
+ }
112
+ }
113
+ ]
114
+ }