Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules/
dist/
old/
2 changes: 1 addition & 1 deletion packages/github/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"build": "tsup",
"dev": "tsup --watch",
"prepublishOnly": "pnpm run build",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "vitest"
},
"keywords": [],
"author": "amk-dev",
Expand Down
Binary file added packages/github/tools/.DS_Store
Binary file not shown.
119 changes: 119 additions & 0 deletions packages/github/tools/gists-create.test.ts

Large diffs are not rendered by default.

66 changes: 66 additions & 0 deletions packages/github/tools/gists-create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { z } from "zod";
import { ofetch } from "ofetch";
import { Result, ok, err } from "neverthrow";
import type { ToolsetteTool } from "@toolsette/utils";

// Schema for creating a gist based on the OpenAPI specification
const createGistSchema = z.object({
description: z
.string()
.optional()
.describe("Description of the gist (e.g. 'Example Ruby script')"),
files: z
.record(
z.object({
content: z.string().describe("Content of the file"),
})
)
.describe(
"Names and content for the files that make up the gist. Example: { 'hello.rb': { content: 'puts \"Hello, World!\"' } }"
),
public: z
.union([z.boolean(), z.enum(["true", "false"])])
.optional()
.default(false)
.describe(
"Flag indicating whether the gist is public. Can be boolean or a string ('true' or 'false')."
),
});

type CreateGistInput = z.infer<typeof createGistSchema>;

// Function to create a gist using GitHub's API
async function createGistFunction(
input: CreateGistInput,
metadata: { auth: { type: "Bearer"; apiKey: string } }
): Promise<Result<string, Error>> {
try {
const headers: Record<string, string> = {
Accept: "application/vnd.github.v3+json",
Authorization: `Bearer ${metadata.auth.apiKey}`,
};

const response = await ofetch("https://api.github.com/gists", {
method: "POST",
body: input,
headers,
// Return the raw response text
parseResponse: (text) => text,
});

return ok(response);
} catch (error) {
return err(
error instanceof Error ? error : new Error("Failed to create gist")
);
}
}

// Exporting the tool object according to the Toolsette framework guidelines
export const createGist: ToolsetteTool = {
name: "create_gist",
description:
'Allows you to add a new gist with one or more files.\n\n> [!NOTE]\n> Don\'t name your files "gistfile" with a numerical suffix. This is the format of the automatic naming scheme that Gist uses internally.',
parameters: createGistSchema,
function: createGistFunction,
};
95 changes: 95 additions & 0 deletions packages/github/tools/gists-delete.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { describe, it, expect } from "vitest";
import { ofetch } from "ofetch";
import { deleteGist } from "./gists-delete";

const deleteGistFunction = deleteGist.function;
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
if (!GITHUB_TOKEN) {
throw new Error("GITHUB_TOKEN environment variable is required to run tests");
}

const auth = { type: "Bearer" as const, apiKey: GITHUB_TOKEN };

describe("deleteGistFunction Integration Tests", () => {
it("should successfully delete an existing gist", async () => {
const createUrl = "https://api.github.com/gists";
const createHeaders = {
Accept: "application/vnd.github.v3+json",
Authorization: `Bearer ${GITHUB_TOKEN}`,
};
const createBody = {
description: "Test gist for successful deletion",
public: false,
files: { "test.txt": { content: "Integration test content" } },
};
const createdGist = await ofetch(createUrl, {
method: "POST",
headers: createHeaders,
body: createBody,
});
const gistId = createdGist.id;
const result = await deleteGistFunction({ gist_id: gistId }, { auth });
expect(result).toMatchInlineSnapshot();
});

it("should handle deletion of an already deleted gist", async () => {
const createUrl = "https://api.github.com/gists";
const createHeaders = {
Accept: "application/vnd.github.v3+json",
Authorization: `Bearer ${GITHUB_TOKEN}`,
};
const createBody = {
description: "Test gist for double deletion",
public: false,
files: { "test.txt": { content: "Delete twice" } },
};
const createdGist = await ofetch(createUrl, {
method: "POST",
headers: createHeaders,
body: createBody,
});
const gistId = createdGist.id;
await deleteGistFunction({ gist_id: gistId }, { auth });
const result = await deleteGistFunction({ gist_id: gistId }, { auth });
expect(result).toMatchInlineSnapshot();
});

it("should handle deletion of a non-existent gist", async () => {
const result = await deleteGistFunction({ gist_id: "non-existent-gist-id" }, { auth });
expect(result).toMatchInlineSnapshot();
});

it("should handle invalid auth token", async () => {
const createUrl = "https://api.github.com/gists";
const createHeaders = {
Accept: "application/vnd.github.v3+json",
Authorization: `Bearer ${GITHUB_TOKEN}`,
};
const createBody = {
description: "Test gist for deletion with invalid auth",
public: false,
files: { "test.txt": { content: "Invalid auth test" } },
};
const createdGist = await ofetch(createUrl, {
method: "POST",
headers: createHeaders,
body: createBody,
});
const gistId = createdGist.id;
const result = await deleteGistFunction(
{ gist_id: gistId },
{ auth: { type: "Bearer", apiKey: "invalid-token" } }
);
expect(result).toMatchInlineSnapshot();
});

it("should handle empty gist_id", async () => {
const result = await deleteGistFunction({ gist_id: "" }, { auth });
expect(result).toMatchInlineSnapshot();
});

it("should handle gist_id with only whitespace", async () => {
const result = await deleteGistFunction({ gist_id: " " }, { auth });
expect(result).toMatchInlineSnapshot();
});
});
42 changes: 42 additions & 0 deletions packages/github/tools/gists-delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { z } from "zod";
import { ofetch } from "ofetch";
import { Result, ok, err } from "neverthrow";
import { ToolsetteTool } from "@toolsette/utils";

const deleteGistSchema = z.object({
gist_id: z.string().describe("The unique identifier of the gist.")
});

type DeleteGistInput = z.infer<typeof deleteGistSchema>;

async function deleteGistFunction(
input: DeleteGistInput,
metadata: { auth: { type: "Bearer"; apiKey: string } }
): Promise<Result<string, Error>> {
try {
const url = `https://api.github.com/gists/${input.gist_id}`;

const headers = {
Accept: "application/vnd.github.v3+json",
Authorization: `Bearer ${metadata.auth.apiKey}`,
};

await ofetch(url, {
method: "DELETE",
headers,
});

return ok("Gist deleted successfully");
} catch (error) {
return err(
error instanceof Error ? error : new Error("Failed to delete gist")
);
}
}

export const deleteGist: ToolsetteTool = {
name: "delete_gist",
description: "Delete a gist",
parameters: deleteGistSchema,
function: deleteGistFunction,
};
61 changes: 61 additions & 0 deletions packages/github/tools/gists-get.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { describe, it, expect } from "vitest";
import { getGist } from "./gists-get";

const getGistFunction = getGist.function;

const GITHUB_TOKEN = process.env.GITHUB_TOKEN;

if (!GITHUB_TOKEN) {
throw new Error("GITHUB_TOKEN environment variable is required to run tests");
}

describe("getGistFunction Integration Tests", () => {
it("should fetch gist successfully with valid token and valid gist id", async () => {
const result = await getGistFunction(
{ gist_id: "aa5a315d61ae9438b18d" },
{ auth: { type: "Bearer", apiKey: GITHUB_TOKEN } }
);
expect(result).toMatchInlineSnapshot();
});

it("should handle invalid authentication token", async () => {
const result = await getGistFunction(
{ gist_id: "aa5a315d61ae9438b18d" },
{ auth: { type: "Bearer", apiKey: "invalid-token" } }
);
expect(result).toMatchInlineSnapshot();
});

it("should handle non-existent gist id", async () => {
const result = await getGistFunction(
{ gist_id: "nonexistentgistid" },
{ auth: { type: "Bearer", apiKey: GITHUB_TOKEN } }
);
expect(result).toMatchInlineSnapshot();
});

it("should handle empty gist id", async () => {
const result = await getGistFunction(
{ gist_id: "" },
{ auth: { type: "Bearer", apiKey: GITHUB_TOKEN } }
);
expect(result).toMatchInlineSnapshot();
});

it("should handle extremely long gist id", async () => {
const longGistId = "a".repeat(1000);
const result = await getGistFunction(
{ gist_id: longGistId },
{ auth: { type: "Bearer", apiKey: GITHUB_TOKEN } }
);
expect(result).toMatchInlineSnapshot();
});

it("should fetch gist successfully without auth token", async () => {
const result = await getGistFunction(
{ gist_id: "aa5a315d61ae9438b18d" },
{ auth: { type: "Bearer", apiKey: "" } }
);
expect(result).toMatchInlineSnapshot();
});
});
62 changes: 62 additions & 0 deletions packages/github/tools/gists-get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { z } from "zod";
import { ofetch } from "ofetch";
import { Result, ok, err } from "neverthrow";
import { ToolsetteTool } from "@toolsette/utils";

/**
* Schema for the input parameters required to get a gist.
*/
const getGistSchema = z.object({
gist_id: z.string().describe("The unique identifier of the gist."),
});

type GetGistInput = z.infer<typeof getGistSchema>;

/**
* Gets a specified gist.
*
* This endpoint supports the following custom media types:
* - application/vnd.github.raw+json: Returns the raw markdown.
* (This is the default if you do not pass any specific media type.)
* - application/vnd.github.base64+json: Returns the base64-encoded contents.
* This can be useful if your gist contains any invalid UTF-8 sequences.
*
* @param input - The input containing the gist ID.
* @param metadata - Metadata including authentication information.
* @returns A Result which contains the gist response as a string, or an Error.
*/
async function getGistFunction(
input: GetGistInput,
metadata: { auth: { type: "Bearer"; apiKey: string } }
): Promise<Result<string, Error>> {
try {
const url = `https://api.github.com/gists/${input.gist_id}`;

// Setup headers including the Accept header for custom media types.
const headers: Record<string, string> = {
"Accept": "application/vnd.github.raw+json",
};

if (metadata.auth.apiKey) {
headers["Authorization"] = `Bearer ${metadata.auth.apiKey}`;
}

const response = await ofetch(url, {
method: "GET",
headers,
parseResponse: (txt) => txt,
});

return ok(response);
} catch (error) {
return err(error instanceof Error ? error : new Error("Failed to fetch gist"));
}
}

export const getGist: ToolsetteTool = {
name: "get_gist",
description:
"Gets a specified gist. This endpoint supports the following custom media types: application/vnd.github.raw+json returns the raw markdown (default), and application/vnd.github.base64+json returns the base64-encoded contents.",
parameters: getGistSchema,
function: getGistFunction,
};
48 changes: 48 additions & 0 deletions packages/github/tools/gists-list.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, it, expect } from "vitest";
import { listGists } from "./gists-list";

const listGistsFunction = listGists.function;
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
if (!GITHUB_TOKEN) {
throw new Error("GITHUB_TOKEN environment variable is required to run tests");
}

describe("listGistsFunction Integration Tests", () => {
const validAuth = { type: "Bearer", apiKey: GITHUB_TOKEN };

it("should fetch gists with default parameters", async () => {
const result = await listGistsFunction({ per_page: 30, page: 1 }, { auth: validAuth });
expect(result).toMatchInlineSnapshot();
});

it("should respect per_page parameter with per_page = 5", async () => {
const result = await listGistsFunction({ per_page: 5, page: 1 }, { auth: validAuth });
expect(result).toMatchInlineSnapshot();
});

it("should fetch gists with a valid 'since' parameter", async () => {
const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
const result = await listGistsFunction({ since, per_page: 30, page: 1 }, { auth: validAuth });
expect(result).toMatchInlineSnapshot();
});

it("should handle invalid auth token", async () => {
const result = await listGistsFunction({ per_page: 30, page: 1 }, { auth: { type: "Bearer", apiKey: "invalid-token" } });
expect(result).toMatchInlineSnapshot();
});

it("should fetch gists anonymously", async () => {
const result = await listGistsFunction({ per_page: 30, page: 1 }, { auth: { type: "Bearer", apiKey: "" } });
expect(result).toMatchInlineSnapshot();
});

it("should handle edge case: per_page = 1 and page = 1", async () => {
const result = await listGistsFunction({ per_page: 1, page: 1 }, { auth: validAuth });
expect(result).toMatchInlineSnapshot();
});

it("should handle edge case: per_page = 100 and page = 1", async () => {
const result = await listGistsFunction({ per_page: 100, page: 1 }, { auth: validAuth });
expect(result).toMatchInlineSnapshot();
});
});
Loading