Skip to content

Commit

Permalink
Add anthropic plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
ktx-mega committed Jan 9, 2025
1 parent 0315f38 commit 73c47b7
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 2 deletions.
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,10 @@ expect the tool to be installed globally, but they should work by replacing

### Usage

Next you'll need to decide whether to use `openai`, `gemini` or `local` mode. In a
Next you'll need to decide whether to use `openai`, `gemini`, `anthropic` or `local` mode. In a
nutshell:

* `openai` or `gemini` mode
* `openai`, `gemini` or `anthropic` mode
* Runs on someone else's computer that's specifically optimized for this kind
of things
* Costs money depending on the length of your code
Expand Down Expand Up @@ -135,6 +135,20 @@ humanify gemini --apiKey="your-token" obfuscated-file.js
Alternatively you can also use an environment variable `GEMINI_API_KEY`. Use
`humanify --help` to see all available options.

### Anthropic mode

You'll need an Anthropic key. You can get one by signing up at
https://console.anthropic.com.

You need to provice the API key to the tool:

```shell
humanify anthropic --apiKey="your-token" obfuscated-file.js
```

Alternatively you can also use an environment variable `ANTHROPIC_API_KEY`. Use
`humanify --help` to see all available options.

### Local mode

The local mode uses a pre-trained language model to deobfuscate the code. The
Expand Down
28 changes: 28 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"author": "Jesse Luoto",
"license": "MIT",
"dependencies": {
"@anthropic-ai/sdk": "^0.33.1",
"@babel/core": "^7.25.2",
"@babel/types": "^7.25.2",
"@google/generative-ai": "^0.20.0",
Expand Down
50 changes: 50 additions & 0 deletions src/commands/anthropic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { cli } from "../cli.js";
import prettier from "../plugins/prettier.js";
import { unminify } from "../unminify.js";
import babel from "../plugins/babel/babel.js";
import { verbose } from "../verbose.js";
import { anthropicRename } from "../plugins/anthropic-rename.js";
import { env } from "../env.js";
import { parseNumber } from "../number-utils.js";
import { DEFAULT_CONTEXT_WINDOW_SIZE } from "./default-args.js";

export const anthropic = cli()
.name("anthropic")
.description("Use Anthropic's Claude API to unminify code")
.option("-m, --model <model>", "The model to use", "claude-3-sonnet-20240229")
.option("-o, --outputDir <output>", "The output directory", "output")
.option(
"-k, --apiKey <apiKey>",
"The Anthropic API key. Alternatively use ANTHROPIC_API_KEY environment variable"
)
.option(
"--baseURL <baseURL>",
"The Anthropic base server URL.",
env("ANTHROPIC_BASE_URL") ?? "https://api.anthropic.com"
)
.option("--verbose", "Show verbose output")
.option(
"--contextSize <contextSize>",
"The context size to use for the LLM",
`${DEFAULT_CONTEXT_WINDOW_SIZE}`
)
.argument("input", "The input minified Javascript file")
.action(async (filename, opts) => {
if (opts.verbose) {
verbose.enabled = true;
}
const apiKey = opts.apiKey ?? env("ANTHROPIC_API_KEY");
const baseURL = opts.baseURL;
const contextWindowSize = parseNumber(opts.contextSize);

await unminify(filename, opts.outputDir, [
babel,
anthropicRename({
apiKey,
baseURL,
model: opts.model,
contextWindowSize
}),
prettier
]);
});
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { local } from "./commands/local.js";
import { openai } from "./commands/openai.js";
import { cli } from "./cli.js";
import { azure } from "./commands/gemini.js";
import { anthropic } from "./commands/anthropic.js";

cli()
.name("humanify")
Expand All @@ -13,5 +14,6 @@ cli()
.addCommand(local)
.addCommand(openai)
.addCommand(azure)
.addCommand(anthropic)
.addCommand(download())
.parse(process.argv);
85 changes: 85 additions & 0 deletions src/plugins/anthropic-rename.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import Anthropic from "@anthropic-ai/sdk";
import { visitAllIdentifiers } from "./local-llm-rename/visit-all-identifiers.js";
import { showPercentage } from "../progress.js";
import { verbose } from "../verbose.js";

export function anthropicRename({
apiKey,
baseURL,
model,
contextWindowSize
}: {
apiKey: string;
baseURL?: string;
model: string;
contextWindowSize: number;
}) {
const client = new Anthropic({
apiKey,
baseURL
});

return async (code: string): Promise<string> => {
return await visitAllIdentifiers(
code,
async (name, surroundingCode) => {
verbose.log(`Renaming ${name}`);
verbose.log("Context: ", surroundingCode);

const response = await client.messages.create(
toRenamePrompt(name, surroundingCode, model, contextWindowSize)
);

const result = response.content[0];
if (!result) {
throw new Error('Failed to rename', { cause: response });
}
const renamed = result.input.newName
verbose.log(`${name} renamed to ${renamed}`);
return renamed;
},
contextWindowSize,
showPercentage
);
};
}

function toRenamePrompt(
name: string,
surroundingCode: string,
model: string,
contextWindowSize: number,
): Anthropic.Messages.MessageCreateParams {
return {
model,
messages: [
{
role: "user",
content: `Analyze this code and suggest a descriptive name for the variable/function \`${name}\`:
${surroundingCode}`
}
],
max_tokens: contextWindowSize,
tools: [
{
name: "suggest_name",
description: "Suggest a descriptive name for the code element",
input_schema: {
type: "object",
properties: {
newName: {
type: "string",
description: `The new descriptive name for the variable/function called \`${name}\``
}
},
required: ["newName"],
additionalProperties: false
}
}
],
tool_choice: {
type: "tool",
name: "suggest_name"
}
};
}
47 changes: 47 additions & 0 deletions src/test/e2e.anthropictest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import test from "node:test";
import { readFile, rm } from "node:fs/promises";
import { testPrompt } from "./test-prompt.js";
import { gbnf } from "../plugins/local-llm-rename/gbnf.js";
import assert from "node:assert";
import { humanify } from "../test-utils.js";

const TEST_OUTPUT_DIR = "test-output";

test.afterEach(async () => {
await rm(TEST_OUTPUT_DIR, { recursive: true, force: true });
});

test("Unminifies an example file successfully", async () => {
const fileIsMinified = async (filename: string) => {
const prompt = await testPrompt();
return await prompt(
`Your job is to read the following code and rate it's readabillity and variable names. Answer "EXCELLENT", "GOOD" or "UNREADABLE".`,
await readFile(filename, "utf-8"),
gbnf`${/("EXCELLENT" | "GOOD" | "UNREADABLE") [^.]+/}.`
);
};
const expectStartsWith = (expected: string[], actual: string) => {
assert(
expected.some((e) => actual.startsWith(e)),
`Expected "${expected}" but got ${actual}`
);
};

await expectStartsWith(
["UNREADABLE"],
await fileIsMinified(`fixtures/example.min.js`)
);

await humanify(
"anthropic",
"fixtures/example.min.js",
"--verbose",
"--outputDir",
TEST_OUTPUT_DIR
);

await expectStartsWith(
["EXCELLENT", "GOOD"],
await fileIsMinified(`${TEST_OUTPUT_DIR}/deobfuscated.js`)
);
});

0 comments on commit 73c47b7

Please sign in to comment.