tarsk 0.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.
package/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # Tarsk
2
+
3
+ Tarsk is an AI tool editor.
4
+
5
+ Run `npx tarsk`
6
+
7
+ ## Details
8
+
9
+ This will serve at `http://localhost:4021`
@@ -0,0 +1,117 @@
1
+ import { logWrite, logEnd, logError, logStart } from "../log/log.js";
2
+ export async function completion(request, options) {
3
+ // Get the full list of tool definitions
4
+ const tools = request.tools.map(tool => JSON.parse(JSON.stringify(tool.definition)) ?? []);
5
+ // Create the object without the functions etc of the request
6
+ const req = {
7
+ model: request.model,
8
+ tools,
9
+ messages: request.messages,
10
+ };
11
+ logStart({ type: 'completion', args: '' });
12
+ let repeating;
13
+ let count = 0;
14
+ let response = {};
15
+ do {
16
+ repeating = false;
17
+ count++;
18
+ logStart({ type: `completion_request_${count}`, args: request });
19
+ response = await call(options, req);
20
+ logEnd(`completion_request_${count}`);
21
+ if (response.error) {
22
+ const message = JSON.stringify(response.error.metadata);
23
+ logError({ type: 'completion_error', message, args: { ...response.error } });
24
+ throw new Error(response.error.message);
25
+ }
26
+ for (const choice of response.choices) {
27
+ // Inject the response message back into the request so the LLM knows for next round
28
+ logWrite({ type: 'inject_choice', args: choice });
29
+ request.messages.push(cleanedMessage(choice.message));
30
+ if (choice.finish_reason == 'tool_calls') {
31
+ logStart({ type: 'tool_calls', args: choice.message.tool_calls });
32
+ const newMessages = await callTools(choice.message.tool_calls ?? [], request);
33
+ request.messages.push(...newMessages);
34
+ logEnd('tool_calls');
35
+ // Do another loop if requested
36
+ repeating = newMessages.length > 0;
37
+ }
38
+ }
39
+ } while (repeating);
40
+ logEnd('completion');
41
+ return response;
42
+ }
43
+ // This gets rid of the fields that the LLM throws errors on
44
+ function cleanedMessage(message) {
45
+ const cleaned = JSON.parse(JSON.stringify(message));
46
+ delete (cleaned.reasoning);
47
+ delete (cleaned.refusal);
48
+ for (const call of cleaned.tool_calls ?? []) {
49
+ delete (call.index);
50
+ }
51
+ return cleaned;
52
+ }
53
+ export async function callTools(toolCalls, request) {
54
+ const newMessages = [];
55
+ for (const toolCall of toolCalls) {
56
+ const toolName = toolCall.function.name;
57
+ const toolArgs = JSON.parse(toolCall.function.arguments);
58
+ // Find the tool by function name
59
+ let found;
60
+ let toolMap;
61
+ for (const tool of request.tools) {
62
+ for (const definition of tool.definition ?? []) {
63
+ if (definition.function.name == toolName) {
64
+ found = definition;
65
+ toolMap = tool.toolMap;
66
+ break;
67
+ }
68
+ }
69
+ }
70
+ if (!found) {
71
+ throw new Error(`Tool ${toolName} not found`);
72
+ }
73
+ if (!toolMap) {
74
+ throw new Error(`Tool map for ${toolName} not found`);
75
+ }
76
+ // TODO: Promise all the tool calls
77
+ if (!toolMap[toolName]) {
78
+ throw new Error(`Tool ${toolName} not found in tool map. Check the function name in the definition matches exactly to the name of the function`);
79
+ }
80
+ logStart({ type: `tool_call_${toolName}`, args: { toolName, toolArgs } });
81
+ try {
82
+ const toolResponse = await toolMap[toolName](...Object.values(toolArgs));
83
+ newMessages.push({
84
+ role: 'tool',
85
+ tool_call_id: toolCall.id,
86
+ //toolCallId: toolCall.id,
87
+ name: toolName,
88
+ content: JSON.stringify(toolResponse),
89
+ });
90
+ logWrite({ type: 'tool_call_response', args: { toolName, toolResponse }, message: JSON.stringify(toolResponse) });
91
+ }
92
+ catch (error) {
93
+ var err = new Error();
94
+ logWrite({ type: 'tool_call_error', args: { toolName, error }, message: err.stack?.toString() });
95
+ throw new Error(`Error calling tool ${toolName}: ${error}`);
96
+ }
97
+ finally {
98
+ logEnd(`tool_call_${toolName}`);
99
+ }
100
+ }
101
+ return newMessages;
102
+ }
103
+ async function call(options, request) {
104
+ const response = await fetch(`${options.baseUrl}/chat/completions`, {
105
+ method: 'POST',
106
+ headers: {
107
+ 'Authorization': 'Bearer ' + options.apiKey,
108
+ 'HTTP-Referer': 'https://webnative.dev', // Optional. Site URL for rankings on openrouter.ai.
109
+ 'X-Title': 'Tarsk', // Optional. Site title for rankings on openrouter.ai.
110
+ 'Content-Type': 'application/json',
111
+ },
112
+ body: JSON.stringify(request)
113
+ });
114
+ const text = await response.text();
115
+ const json = JSON.parse(text);
116
+ return json;
117
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,77 @@
1
+ import { logWrite, logEnd, logError, logStart } from "../log/log.js";
2
+ const weakModules = new WeakMap();
3
+ async function importWeak(modulePath, keyObject) {
4
+ if (weakModules.has(keyObject)) {
5
+ return weakModules.get(keyObject);
6
+ }
7
+ const module = await import(modulePath);
8
+ weakModules.set(keyObject, module);
9
+ return module;
10
+ }
11
+ export async function loadTools(modulePaths) {
12
+ const tools = [];
13
+ logWrite({
14
+ type: "load_tools",
15
+ args: modulePaths,
16
+ }, `modulePaths is "${modulePaths.join(", ")}"`);
17
+ for (const modulePath of modulePaths) {
18
+ logWrite({ type: "getTool", args: modulePath });
19
+ if (modulePath == "") {
20
+ logError({ type: "getTool", args: "Empty modulePath" });
21
+ continue;
22
+ }
23
+ else {
24
+ const tool = await getTool(modulePath);
25
+ tools.push(tool);
26
+ }
27
+ }
28
+ return tools;
29
+ }
30
+ // async function importFromMemory(moduleCode: string) {
31
+ // const module = new Function(
32
+ // "exports",
33
+ // "module",
34
+ // "require",
35
+ // "__filename",
36
+ // "__dirname",
37
+ // moduleCode
38
+ // );
39
+ // const exports = {};
40
+ // const moduleObj = { exports: exports };
41
+ // module(exports, moduleObj, require, null, null);
42
+ // return moduleObj.exports;
43
+ // }
44
+ // async function importModule(code: string) {
45
+ // const myModule = await importFromMemory(code);
46
+ // //console.log(myModule.hello()); // Output: Hello from memory!
47
+ // }
48
+ // eg modulePath = './tools/books.js'
49
+ async function getTool(modulePath) {
50
+ const result = {
51
+ key: {},
52
+ toolMap: undefined,
53
+ definition: undefined,
54
+ };
55
+ // When 'key' is no longer referenced and garbage collected,
56
+ // the module './myModule.js' associated with it in 'weakModules'
57
+ // will also be eligible for garbage collection.
58
+ logStart({ type: "load_tool", args: { modulePath } }, `The modulePath is "${modulePath}"`);
59
+ try {
60
+ const myModule = await importWeak(modulePath, result.key);
61
+ try {
62
+ result.toolMap = myModule.toolMap;
63
+ result.definition = myModule.default();
64
+ return result;
65
+ }
66
+ catch (e) {
67
+ throw new Error(`Error loading tool from ${modulePath}: ${e}`);
68
+ }
69
+ }
70
+ catch (e) {
71
+ logError({ type: "load_tool_error", args: e });
72
+ }
73
+ finally {
74
+ logEnd("load_tool");
75
+ }
76
+ throw new Error(`Error loading tool from ${modulePath}`);
77
+ }
@@ -0,0 +1,53 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "crypto";
2
+ import { hostname, machine } from "os";
3
+ export function encryptString(text) {
4
+ let id = `${machineIdSync()}11de20da-f6cf-4b85-b630-555fc6e8b881`;
5
+ return encrypt(text, id);
6
+ }
7
+ export function decryptString(text) {
8
+ try {
9
+ let id = `${machineIdSync()}11de20da-f6cf-4b85-b630-555fc6e8b881`;
10
+ return decrypt(text, id);
11
+ }
12
+ catch (e) {
13
+ return "";
14
+ }
15
+ }
16
+ function machineIdSync() {
17
+ return machine() + hostname();
18
+ }
19
+ const algorithm = "aes-256-cbc";
20
+ const ivLength = 16; // AES block size in bytes
21
+ /**
22
+ * Encrypts a string using AES-256-CBC and returns base64-encoded result.
23
+ * @param text - The plain text to encrypt.
24
+ * @param passPhrase - The secret passphrase used to generate the encryption key.
25
+ * @returns A base64 string containing IV and encrypted data, separated by a colon.
26
+ */
27
+ function encrypt(text, passPhrase) {
28
+ const iv = randomBytes(ivLength);
29
+ const key = scryptSync(passPhrase, "salt", 32); // 256-bit key
30
+ const cipher = createCipheriv(algorithm, key, iv);
31
+ let encrypted = cipher.update(text, "utf8", "base64");
32
+ encrypted += cipher.final("base64");
33
+ const encryptedData = `${iv.toString("base64")}:${encrypted}`;
34
+ return encryptedData;
35
+ }
36
+ /**
37
+ * Decrypts a base64-encoded string using AES-256-CBC.
38
+ * @param encryptedData - The base64-encoded encrypted data (with IV).
39
+ * @param passPhrase - The secret passphrase used to generate the decryption key.
40
+ * @returns The decrypted plain text.
41
+ */
42
+ function decrypt(encryptedData, passPhrase) {
43
+ const [ivBase64, encryptedBase64] = encryptedData.split(":");
44
+ if (!ivBase64 || !encryptedBase64) {
45
+ throw new Error("Invalid encrypted data format");
46
+ }
47
+ const iv = Buffer.from(ivBase64, "base64");
48
+ const key = scryptSync(passPhrase, "salt", 32);
49
+ const decipher = createDecipheriv(algorithm, key, iv);
50
+ let decrypted = decipher.update(encryptedBase64, "base64", "utf8");
51
+ decrypted += decipher.final("utf8");
52
+ return decrypted;
53
+ }
@@ -0,0 +1,162 @@
1
+ export async function api_get_models(c) {
2
+ const res = await fetch("https://openrouter.ai/api/v1/models");
3
+ const data = await res.json();
4
+ const models = [];
5
+ for (const model of data.data) {
6
+ if (hasTools(model.id)) {
7
+ models.push({
8
+ id: model.id,
9
+ description: model.description,
10
+ name: model.name,
11
+ type: model.architecture.tokenizer,
12
+ priceM: totalPrice(model.pricing),
13
+ price: model.pricing.prompt == "0" && model.pricing.completion == "0"
14
+ ? ""
15
+ : `($${totalPrice(model.pricing).toFixed(2)}m)`,
16
+ });
17
+ }
18
+ }
19
+ return c.json(models);
20
+ }
21
+ function totalPrice(price) {
22
+ const total = v(price.prompt) + v(price.completion);
23
+ return total * 1000000;
24
+ }
25
+ function promptPrice(price) {
26
+ const total = v(price.prompt);
27
+ return total * 1000000;
28
+ }
29
+ function completionPrice(price) {
30
+ const total = v(price.completion);
31
+ return total * 1000000;
32
+ }
33
+ function v(s) {
34
+ if (!s)
35
+ return 0;
36
+ return Number(s);
37
+ }
38
+ function hasTools(model) {
39
+ return [
40
+ "google/gemini-2.5-pro-preview-03-25",
41
+ "google/gemini-2.5-flash-preview",
42
+ "google/gemini-2.5-flash-preview:thinking",
43
+ "openai/o4-mini-high",
44
+ "openai/o3",
45
+ "openai/o4-mini",
46
+ "openai/gpt-4.1",
47
+ "openai/gpt-4.1-mini",
48
+ "openai/gpt-4.1-nano",
49
+ "x-ai/grok-3-beta",
50
+ "meta-llama/llama-4-maverick",
51
+ "meta-llama/llama-4-scout",
52
+ "all-hands/openhands-lm-32b-v0.1",
53
+ "mistral/ministral-8b",
54
+ "google/gemini-2.5-pro-exp-03-25:free",
55
+ "deepseek/deepseek-chat-v3-0324",
56
+ "mistralai/mistral-small-3.1-24b-instruct:free",
57
+ "mistralai/mistral-small-3.1-24b-instruct",
58
+ "ai21/jamba-1.6-large",
59
+ "ai21/jamba-1.6-mini",
60
+ "qwen/qwq-32b",
61
+ "openai/gpt-4.5-preview",
62
+ "google/gemini-2.0-flash-lite-001",
63
+ "anthropic/claude-3.7-sonnet",
64
+ "anthropic/claude-3.7-sonnet:thinking",
65
+ "anthropic/claude-3.7-sonnet:beta",
66
+ "mistralai/mistral-saba",
67
+ "openai/o3-mini-high",
68
+ "google/gemini-2.0-flash-001",
69
+ "qwen/qwen-turbo",
70
+ "qwen/qwen-plus",
71
+ "qwen/qwen-max",
72
+ "openai/o3-mini",
73
+ "mistralai/mistral-small-24b-instruct-2501",
74
+ "deepseek/deepseek-r1-distill-llama-70b",
75
+ "deepseek/deepseek-r1",
76
+ "mistralai/codestral-2501",
77
+ "deepseek/deepseek-chat",
78
+ "openai/o1",
79
+ "x-ai/grok-2-1212",
80
+ "google/gemini-2.0-flash-exp:free",
81
+ "meta-llama/llama-3.3-70b-instruct",
82
+ "amazon/nova-lite-v1",
83
+ "amazon/nova-micro-v1",
84
+ "amazon/nova-pro-v1",
85
+ "openai/gpt-4o-2024-11-20",
86
+ "mistralai/mistral-large-2411",
87
+ "mistralai/mistral-large-2407",
88
+ "mistralai/pixtral-large-2411",
89
+ "anthropic/claude-3.5-haiku:beta",
90
+ "anthropic/claude-3.5-haiku",
91
+ "anthropic/claude-3.5-haiku-20241022:beta",
92
+ "anthropic/claude-3.5-haiku-20241022",
93
+ "anthropic/claude-3.5-sonnet:beta",
94
+ "anthropic/claude-3.5-sonnet",
95
+ "x-ai/grok-beta",
96
+ "mistralai/ministral-8b",
97
+ "mistralai/ministral-3b",
98
+ "nvidia/llama-3.1-nemotron-70b-instruct",
99
+ "google/gemini-flash-1.5-8b",
100
+ "meta-llama/llama-3.2-3b-instruct",
101
+ "qwen/qwen-2.5-72b-instruct",
102
+ "mistralai/pixtral-12b",
103
+ "cohere/command-r-plus-08-2024",
104
+ "cohere/command-r-08-2024",
105
+ "google/gemini-flash-1.5-8b-exp",
106
+ "ai21/jamba-1-5-mini",
107
+ "ai21/jamba-1-5-large",
108
+ "microsoft/phi-3.5-mini-128k-instruct",
109
+ "nousresearch/hermes-3-llama-3.1-70b",
110
+ "openai/gpt-4o-2024-08-06",
111
+ "meta-llama/llama-3.1-8b-instruct",
112
+ "meta-llama/llama-3.1-405b-instruct",
113
+ "meta-llama/llama-3.1-70b-instruct",
114
+ "mistralai/codestral-mamba",
115
+ "mistralai/mistral-nemo",
116
+ "openai/gpt-4o-mini",
117
+ "openai/gpt-4o-mini-2024-07-18",
118
+ "anthropic/claude-3.5-sonnet-20240620:beta",
119
+ "anthropic/claude-3.5-sonnet-20240620",
120
+ "mistralai/mistral-7b-instruct:free",
121
+ "mistralai/mistral-7b-instruct",
122
+ "mistralai/mistral-7b-instruct-v0.3",
123
+ "microsoft/phi-3-mini-128k-instruct",
124
+ "microsoft/phi-3-medium-128k-instruct",
125
+ "google/gemini-flash-1.5",
126
+ "openai/gpt-4o",
127
+ "openai/gpt-4o:extended",
128
+ "openai/gpt-4o-2024-05-13",
129
+ "meta-llama/llama-3-8b-instruct",
130
+ "meta-llama/llama-3-70b-instruct",
131
+ "mistralai/mixtral-8x22b-instruct",
132
+ "google/gemini-pro-1.5",
133
+ "openai/gpt-4-turbo",
134
+ "cohere/command-r-plus",
135
+ "cohere/command-r-plus-04-2024",
136
+ "cohere/command-r",
137
+ "anthropic/claude-3-haiku:beta",
138
+ "anthropic/claude-3-haiku",
139
+ "anthropic/claude-3-opus:beta",
140
+ "anthropic/claude-3-opus",
141
+ "anthropic/claude-3-sonnet:beta",
142
+ "anthropic/claude-3-sonnet",
143
+ "cohere/command-r-03-2024",
144
+ "mistralai/mistral-large",
145
+ "openai/gpt-3.5-turbo-0613",
146
+ "openai/gpt-4-turbo-preview",
147
+ "mistralai/mistral-medium",
148
+ "mistralai/mistral-small",
149
+ "mistralai/mistral-tiny",
150
+ "mistralai/mixtral-8x7b-instruct",
151
+ "openai/gpt-3.5-turbo-1106",
152
+ "openai/gpt-4-1106-preview",
153
+ "mistralai/mistral-7b-instruct-v0.1",
154
+ "openai/gpt-3.5-turbo-16k",
155
+ "openai/gpt-4-32k",
156
+ "openai/gpt-4-32k-0314",
157
+ "openai/gpt-3.5-turbo",
158
+ "openai/gpt-3.5-turbo-0125",
159
+ "openai/gpt-4",
160
+ "openai/gpt-4-0314",
161
+ ].includes(model);
162
+ }
@@ -0,0 +1,12 @@
1
+ import { prompt } from "../prompt.js";
2
+ import { logWrite } from "../log/log.js";
3
+ export async function api_prompt(c) {
4
+ const body = await c.req.json();
5
+ logWrite({ type: 'prompt', args: body });
6
+ const question = body.prompt;
7
+ if (!question) {
8
+ return c.json({ error: 'Prompt is required' }, 400);
9
+ }
10
+ const answer = await prompt(question);
11
+ return c.json({ answer });
12
+ }
@@ -0,0 +1,39 @@
1
+ import {} from "../interfaces/settings.js";
2
+ import { getJSON, setJSON } from "../utils/json-file.js";
3
+ import { httpValidationErrror as httpValidationError, isEmpty } from "./utils.js";
4
+ const defaultOpenRouterURL = "https://openrouter.ai/api/v1";
5
+ export const defaultModel = "meta-llama/llama-3.3-70b-instruct";
6
+ export async function api_get_settings(c) {
7
+ return c.json(getSettings());
8
+ }
9
+ export async function api_save_settings(c) {
10
+ const settings = await c.req.json();
11
+ if (settings.openRouterApiKey == undefined) {
12
+ httpValidationError("openRouterApiKey is required");
13
+ }
14
+ if (isEmpty(settings.openRouterURL)) {
15
+ settings.openRouterURL = defaultOpenRouterURL;
16
+ }
17
+ if (isEmpty(settings.defaultModel)) {
18
+ settings.defaultModel = defaultModel;
19
+ }
20
+ if (settings.openRouterURL == undefined) {
21
+ httpValidationError("openRouterURL is required");
22
+ }
23
+ if (!settings.openRouterURL.startsWith("https://")) {
24
+ httpValidationError("openRouterURL must start with https://");
25
+ }
26
+ // Remove trailing slash if present
27
+ if (settings.openRouterURL.endsWith("/")) {
28
+ settings.openRouterURL = settings.openRouterURL.slice(0, -1);
29
+ }
30
+ setJSON('settings', settings);
31
+ return c.json(getSettings());
32
+ }
33
+ export function getSettings() {
34
+ const settings = getJSON("settings", {
35
+ openRouterApiKey: "",
36
+ openRouterURL: defaultOpenRouterURL,
37
+ });
38
+ return settings;
39
+ }
@@ -0,0 +1,106 @@
1
+ import { existsSync, mkdirSync, promises, readdirSync, writeFileSync } from "fs";
2
+ import { extname, join } from "path";
3
+ import { extensionLess, tarskFolder } from "./utils.js";
4
+ import { logError, logWrite } from "../log/log.js";
5
+ import tsBlankSpace from "ts-blank-space";
6
+ // POST /tools to save a tool's typescript definition, convert it to javascript and validate it
7
+ export async function api_save_tool(c) {
8
+ logWrite({ type: "api_save_tool", args: {} });
9
+ const tool = await c.req.json();
10
+ if (tool.name == "") {
11
+ tool.name = tool.title;
12
+ }
13
+ const title = tool.title;
14
+ const code = tool.code;
15
+ const name = tool.name.toLowerCase().replaceAll(" ", "_");
16
+ if (!title || !code || !name) {
17
+ return c.json({ error: "Title and code are required" }, 400);
18
+ }
19
+ // Convert typescript to javascript
20
+ // let answer = await prompt(
21
+ // `Given the following typescript return the equivalent javascript. Do not explain just return the javascript\n${tool.code}`
22
+ // );
23
+ const answer = tsBlankSpace(tool.code ?? '', (e) => {
24
+ logError({ type: 'failed_ts_to_js', args: e });
25
+ });
26
+ // Get rid of the markdown code block AI adds
27
+ // answer = stripMarkdown(answer);
28
+ // Choose a filename
29
+ const filename = name.replace(/\s+/g, "_").toLowerCase();
30
+ const filePath = join(toolsJSFolder(), `${filename}.js`);
31
+ const srcPath = join(toolsSrcFolder(), `${filename}.ts`);
32
+ const metaPath = join(toolsSrcFolder(), `${filename}.json`);
33
+ // Save the Typescript
34
+ writeFileSync(srcPath, code, "utf-8");
35
+ writeFileSync(metaPath, JSON.stringify({ title, name }, null, 2), "utf-8");
36
+ // Save the Javascript
37
+ writeFileSync(filePath, answer, "utf-8");
38
+ return c.json({ message: `Tool saved successfully (${filePath})` });
39
+ }
40
+ // Get the tool with code
41
+ export async function api_get_tool(c) {
42
+ const toolName = c.req.param("tool");
43
+ const tool = await getTool(toolName);
44
+ console.log(`api_get_tool ${toolName}`);
45
+ return c.json(tool);
46
+ }
47
+ // Delete the tool with code
48
+ export async function api_delete_tool(c) {
49
+ const toolName = c.req.param("tool");
50
+ await deleteTool(toolName);
51
+ return c.json({});
52
+ }
53
+ async function deleteTool(name) {
54
+ console.log(`Deleting tool ${name}`);
55
+ const toolsFolder = toolsSrcFolder();
56
+ await promises.rm(join(toolsFolder, `${name}.ts`));
57
+ await promises.rm(join(toolsFolder, `${name}.json`));
58
+ await promises.rm(join(toolsJSFolder(), `${name}.js`));
59
+ }
60
+ async function getTool(name) {
61
+ const toolsFolder = toolsSrcFolder();
62
+ const code = await promises.readFile(join(toolsFolder, `${name}.ts`), "utf-8");
63
+ const meta = await promises.readFile(join(toolsFolder, `${name}.json`), "utf-8");
64
+ const json = JSON.parse(meta);
65
+ return { code, title: json.title, name: json.name };
66
+ }
67
+ // Get a list of tools (code is omitted)
68
+ export async function api_get_tools(c) {
69
+ const fileList = await toolFilenames();
70
+ const result = [];
71
+ for (const file of fileList) {
72
+ const tool = await getTool(file);
73
+ tool.code = undefined;
74
+ result.push(tool);
75
+ }
76
+ return c.json(result);
77
+ }
78
+ // Typescript files for tools
79
+ async function toolFilenames() {
80
+ const toolsFolder = toolsSrcFolder();
81
+ const files = await promises.readdir(toolsFolder);
82
+ return files.filter((f) => extname(f) === ".ts").map((f) => extensionLess(f));
83
+ }
84
+ // Returns the compiled tools folder
85
+ function toolsJSFolder() {
86
+ const folderPath = join(tarskFolder(), ".tools");
87
+ if (!existsSync(folderPath)) {
88
+ mkdirSync(folderPath, { recursive: true });
89
+ }
90
+ return folderPath;
91
+ }
92
+ // Javascript files for tools
93
+ export function toolJavascriptFilenames() {
94
+ const files = readdirSync(toolsJSFolder());
95
+ return files
96
+ .filter((f) => extname(f) === ".js")
97
+ .map((f) => join(tarskFolder(), ".tools", f));
98
+ }
99
+ // Returns the source tools folder
100
+ function toolsSrcFolder() {
101
+ const folderPath = join(tarskFolder(), "tools");
102
+ if (!existsSync(folderPath)) {
103
+ mkdirSync(folderPath, { recursive: true });
104
+ }
105
+ return folderPath;
106
+ }
@@ -0,0 +1,17 @@
1
+ import { HTTPException } from "hono/http-exception";
2
+ import { extname, join } from "path";
3
+ export function extensionLess(filename) {
4
+ return filename.substring(0, filename.length - extname(filename).length);
5
+ }
6
+ export function tarskFolder() {
7
+ return join(process.cwd(), "dist", ".tarsk");
8
+ }
9
+ export function httpValidationErrror(message) {
10
+ throw new HTTPException(406, { message });
11
+ }
12
+ export function isEmpty(v) {
13
+ if (v == undefined || v == null || v == "" || v.trim() == "") {
14
+ return true;
15
+ }
16
+ return false;
17
+ }
package/dist/index.js ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ import { serve } from "@hono/node-server";
3
+ import { Hono } from "hono";
4
+ import { cors } from "hono/cors";
5
+ import { api_prompt } from "./api/prompt.js";
6
+ import { api_delete_tool, api_get_tool, api_get_tools, api_save_tool, } from "./api/tools.js";
7
+ import { api_get_settings, api_save_settings } from "./api/settings.js";
8
+ import { api_get_models } from "./api/models.js";
9
+ const app = new Hono();
10
+ app.use("/*", cors({
11
+ // `c` is a `Context` object
12
+ origin: (origin, c) => {
13
+ return (origin.startsWith("http://localhost") ||
14
+ origin.startsWith('https://tarsk.pages.dev')) ? origin : "x";
15
+ },
16
+ }));
17
+ // API endpoints
18
+ app.get("/", (c) => {
19
+ return c.text("Tarsk Started.");
20
+ });
21
+ app.post("/prompt", async (c) => await api_prompt(c));
22
+ app.post("/tools", async (c) => await api_save_tool(c));
23
+ app.get("/tools", async (c) => await api_get_tools(c));
24
+ app.get("/tools/:tool", async (c) => await api_get_tool(c));
25
+ app.delete("/tools/:tool", async (c) => await api_delete_tool(c));
26
+ app.get("/settings", async (c) => await api_get_settings(c));
27
+ app.post("/settings", async (c) => await api_save_settings(c));
28
+ app.get("/models", async (c) => await api_get_models(c));
29
+ serve({
30
+ fetch: app.fetch,
31
+ port: 4021,
32
+ }, (info) => {
33
+ console.log(`Tarsk is running on http://localhost:${info.port}`);
34
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ let logLevel = "debug";
2
+ // Structured logging
3
+ export function logWrite(l, additionalMessage) {
4
+ if (logLevel !== "none")
5
+ return;
6
+ l.id = id();
7
+ console.log(`${l.type}:${l.message} ${additionalMessage}`);
8
+ console.log(l, additionalMessage);
9
+ // TODO: send to server stringified
10
+ // console.log(JSON.stringify(l));
11
+ }
12
+ export function logStart(l, additionalMessage) {
13
+ if (logLevel === "none")
14
+ return;
15
+ console.time(l.type);
16
+ if (additionalMessage) {
17
+ console.log(`Started ${l.type} ${additionalMessage}`);
18
+ }
19
+ logWrite(l);
20
+ }
21
+ export function logEnd(label) {
22
+ if (logLevel === "none")
23
+ return;
24
+ console.timeEnd(label);
25
+ }
26
+ export function logError(l) {
27
+ if (logLevel === "none")
28
+ return;
29
+ console.error(l);
30
+ }
31
+ function id() {
32
+ return Math.random().toString().replace(".", "");
33
+ }
package/dist/prompt.js ADDED
@@ -0,0 +1,43 @@
1
+ import { completion } from "./agent/agent.js";
2
+ import { loadTools } from "./agent/tools.js";
3
+ import { defaultModel, getSettings } from "./api/settings.js";
4
+ import { toolJavascriptFilenames } from "./api/tools.js";
5
+ import { isEmpty } from "./api/utils.js";
6
+ import { logWrite, logEnd, logStart } from "./log/log.js";
7
+ export async function prompt(content) {
8
+ const settings = await getSettings();
9
+ if (settings.openRouterApiKey == "") {
10
+ return "You need to set the OpenRouter API key in the settings.";
11
+ }
12
+ const options = {
13
+ baseUrl: settings.openRouterURL,
14
+ apiKey: settings.openRouterApiKey,
15
+ };
16
+ const messages = [{ role: "user", content }];
17
+ const model = isEmpty(settings.defaultModel)
18
+ ? defaultModel
19
+ : settings.defaultModel;
20
+ // Load the tools needed for the request
21
+ logWrite({ type: "prompt", args: content }, `The model is ${model}`);
22
+ const filenames = toolJavascriptFilenames();
23
+ logStart({ type: "load_tools", args: filenames });
24
+ const tools = await loadTools(filenames // eg ["../tools/books.js"]
25
+ );
26
+ logEnd("load_tools");
27
+ const request = { model, tools, messages };
28
+ try {
29
+ const response = await completion(request, options);
30
+ logWrite({ type: "completion_response", args: response });
31
+ logWrite({ type: "message", args: response?.choices[0].message });
32
+ return response?.choices[0].message.content;
33
+ }
34
+ catch (e) {
35
+ return friendlyError(`${e}`, settings);
36
+ }
37
+ }
38
+ function friendlyError(e, settings) {
39
+ if (e.startsWith("Error: No endpoints found that support tool use")) {
40
+ return `The model "${settings.defaultModel}" does not support tool use. Please select a different model (A good default is ${defaultModel}).`;
41
+ }
42
+ return e;
43
+ }
@@ -0,0 +1,26 @@
1
+ import { join } from "path";
2
+ import { completion } from "../agent/agent.js";
3
+ import { loadTools } from "../agent/tools.js";
4
+ import { logWrite } from "../log/log.js";
5
+ export async function prompt(content) {
6
+ const options = {
7
+ baseUrl: 'https://openrouter.ai/api/v1',
8
+ apiKey: ''
9
+ };
10
+ const messages = [
11
+ { role: 'user', content }
12
+ ];
13
+ const model =
14
+ //'google/gemini-flash-1.5';
15
+ 'meta-llama/llama-3.3-70b-instruct';
16
+ logWrite({ type: 'run-test', args: content });
17
+ // Load the tools needed for the request
18
+ const tools = await loadTools([join('..', '.tarsk', '.tools', 'books.js')]
19
+ //['../tools/books.js']
20
+ );
21
+ const request = { model, tools, messages };
22
+ const response = await completion(request, options);
23
+ logWrite({ type: 'completion_response', args: response });
24
+ logWrite({ type: 'message', args: response?.choices[0].message });
25
+ return response?.choices[0].message.content;
26
+ }
@@ -0,0 +1,5 @@
1
+ import { prompt } from "./agent-spec.js";
2
+ const m = await prompt(
3
+ //'Are you capable of function/tool calls?',
4
+ 'what books were authored in 1981?');
5
+ console.log(m);
@@ -0,0 +1,42 @@
1
+ // Searches for books in the Project Gutenberg library based on provided search terms
2
+ // @param @array @required searchTerms List of search terms to find books in the Gutenberg library (e.g. ['dickens', 'great'] to search for books by Dickens with 'great' in the title)
3
+ export async function searchGutenbergBooks(searchTerms) {
4
+ const searchQuery = searchTerms.join(' ');
5
+ const url = 'https://gutendex.com/books';
6
+ const response = await fetch(`${url}?search=${searchQuery}`);
7
+ const data = await response.json();
8
+ return data.results.map((book) => ({
9
+ id: book.id,
10
+ title: book.title,
11
+ authors: book.authors,
12
+ }));
13
+ }
14
+ export const toolMap = {
15
+ searchGutenbergBooks
16
+ };
17
+ export default function tools() {
18
+ return booksTools;
19
+ }
20
+ // Automate via TS Morph?
21
+ const booksTools = [
22
+ {
23
+ type: 'function',
24
+ function: {
25
+ name: 'searchGutenbergBooks',
26
+ description: 'Search for books in the Project Gutenberg library based on specified search terms',
27
+ parameters: {
28
+ type: 'object',
29
+ properties: {
30
+ searchTerms: {
31
+ type: 'array',
32
+ items: {
33
+ type: 'string',
34
+ },
35
+ description: "List of search terms to find books in the Gutenberg library (e.g. ['dickens', 'great'] to search for books by Dickens with 'great' in the title)",
36
+ },
37
+ },
38
+ required: ['searchTerms'],
39
+ },
40
+ },
41
+ },
42
+ ];
@@ -0,0 +1,19 @@
1
+ import { join } from "path";
2
+ import { tarskFolder } from "../api/utils.js";
3
+ import { existsSync, readFileSync, writeFileSync } from "fs";
4
+ import { decryptString, encryptString } from "../api/encryption.js";
5
+ export function getJSON(name, defaultValue) {
6
+ const filename = join(tarskFolder(), name + '.json');
7
+ if (!existsSync(filename)) {
8
+ writeFileSync(filename, JSON.stringify(defaultValue));
9
+ return defaultValue;
10
+ }
11
+ else {
12
+ const data = JSON.parse(decryptString(readFileSync(filename, 'utf-8')));
13
+ return data;
14
+ }
15
+ }
16
+ export function setJSON(name, value) {
17
+ const filename = join(tarskFolder(), name + '.json');
18
+ writeFileSync(filename, encryptString(JSON.stringify(value)));
19
+ }
@@ -0,0 +1,7 @@
1
+ // Given some markdown for code remove the ``` code block lines
2
+ export function stripMarkdown(markdown) {
3
+ // Remove the code block lines
4
+ const lines = markdown.split("\n");
5
+ const strippedLines = lines.filter(line => !line.startsWith("```"));
6
+ return strippedLines.join("\n").trim();
7
+ }
@@ -0,0 +1,59 @@
1
+ // Searches for books in the Project Gutenberg library based on provided search terms
2
+ // @param @array @required searchTerms List of search terms to find books in the Gutenberg library (e.g. ['dickens', 'great'] to search for books by Dickens with 'great' in the title)
3
+ export async function searchGutenbergBooks(searchTerms ) {
4
+ const searchQuery = searchTerms.join(' ');
5
+ const url = 'https://gutendex.com/books';
6
+ const response = await fetch(`${url}?search=${searchQuery}`);
7
+ const data = await response.json();
8
+ return data.results.map((book ) => ({
9
+ id: book.id,
10
+ title: book.title,
11
+ authors: book.authors,
12
+ }));
13
+ }
14
+
15
+ export const toolMap = {
16
+ searchGutenbergBooks
17
+ }
18
+
19
+ export default function tools() {
20
+ return booksTools;
21
+ }
22
+
23
+ // Automate via TS Morph?
24
+ const booksTools = [
25
+ {
26
+ type: 'function',
27
+ function: {
28
+ name: 'searchGutenbergBooks',
29
+ description:
30
+ 'Search for books in the Project Gutenberg library based on specified search terms',
31
+ parameters: {
32
+ type: 'object',
33
+ properties: {
34
+ searchTerms: { // Must match argument name in function
35
+ type: 'array',
36
+ items: {
37
+ type: 'string',
38
+ },
39
+ description:
40
+ "List of search terms to find books in the Gutenberg library (e.g. ['dickens', 'great'] to search for books by Dickens with 'great' in the title)",
41
+ },
42
+ },
43
+ required: ['searchTerms'],
44
+ },
45
+ },
46
+ },
47
+ ];
48
+
49
+ ;
50
+
51
+
52
+
53
+
54
+
55
+ ;
56
+
57
+
58
+
59
+
@@ -0,0 +1,82 @@
1
+ export async function searchBurns(searchTerms ) {
2
+ const searchQuery = searchTerms.join(' ');
3
+ const url = 'https://api.dust.events/data/festivals.json';
4
+ const response = await fetch(`${url}`);
5
+ const data = await response.json();
6
+ data = data.filter((b) => b.active);
7
+ return data.map((burn ) => ({
8
+ name: burn.name,
9
+ title: burn.title,
10
+ year: burn.year,
11
+ startDate: burn.start,
12
+ endDate: burn.end,
13
+ lat: burn.lat,
14
+ long: burn.long,
15
+ timeZone: burn.timeZone,
16
+ region: burn.region,
17
+ website: burn.website
18
+ }));
19
+ }
20
+
21
+ export const toolMap = {
22
+ searchBurns
23
+ }
24
+
25
+ export default function tools() {
26
+ return burnTools;
27
+ }
28
+
29
+ const burnTools = [
30
+ {
31
+ type: 'function',
32
+ function: {
33
+ name: 'searchBurns',
34
+ description:
35
+ "Search for regional burning man events based on specified search terms",
36
+ parameters: {
37
+ type: 'object',
38
+ properties: {
39
+ searchTerms: { // Must match argument name in function
40
+ type: 'array',
41
+ items: {
42
+ type: 'string',
43
+ },
44
+ description:
45
+ "List of search terms (e.g. ['snrg', 'soak'] to search for regional burns with 'snrg' or 'soak' in the title",
46
+ },
47
+ },
48
+ required: ['searchTerms'],
49
+ },
50
+ },
51
+ },
52
+ ];
53
+
54
+ ;
55
+
56
+
57
+
58
+
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+
67
+
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+
82
+
@@ -0,0 +1 @@
1
+ zO3DHSWwKmpUWImXKWcplw==:3JB/NEXlkvGBxoHDcGPz7uWbAALPiq2+EiRLIC3ejJy0CxaKVV2Aquaw807PwN0z0LLBOvzD56a9atm9cA3yvVhRuwqqIC3Tx4Wy/8dQWixomj9rjKHbnDp7uJ2EZt853AYUuY9ZpgDgM61X+AwcVLw8Xs4meHzV7yZxM57oOER4Y/hLu7ywMV+wEucZd9PL/VBW4/6p/6tFmA5C7DD3CNgIEIyrqyJMzn8L/kQiSs+ZfGa3iXjdQtyPuNBY0fSA4o6lWJnbQ6IkKifL6DDx2A==
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "Books",
3
+ "name": "books"
4
+ }
@@ -0,0 +1,59 @@
1
+ // Searches for books in the Project Gutenberg library based on provided search terms
2
+ // @param @array @required searchTerms List of search terms to find books in the Gutenberg library (e.g. ['dickens', 'great'] to search for books by Dickens with 'great' in the title)
3
+ export async function searchGutenbergBooks(searchTerms: string[]): Promise<Book[]> {
4
+ const searchQuery = searchTerms.join(' ');
5
+ const url = 'https://gutendex.com/books';
6
+ const response = await fetch(`${url}?search=${searchQuery}`);
7
+ const data = await response.json();
8
+ return data.results.map((book: any) => ({
9
+ id: book.id,
10
+ title: book.title,
11
+ authors: book.authors,
12
+ }));
13
+ }
14
+
15
+ export const toolMap = {
16
+ searchGutenbergBooks
17
+ }
18
+
19
+ export default function tools() {
20
+ return booksTools;
21
+ }
22
+
23
+ // Automate via TS Morph?
24
+ const booksTools = [
25
+ {
26
+ type: 'function',
27
+ function: {
28
+ name: 'searchGutenbergBooks',
29
+ description:
30
+ 'Search for books in the Project Gutenberg library based on specified search terms',
31
+ parameters: {
32
+ type: 'object',
33
+ properties: {
34
+ searchTerms: { // Must match argument name in function
35
+ type: 'array',
36
+ items: {
37
+ type: 'string',
38
+ },
39
+ description:
40
+ "List of search terms to find books in the Gutenberg library (e.g. ['dickens', 'great'] to search for books by Dickens with 'great' in the title)",
41
+ },
42
+ },
43
+ required: ['searchTerms'],
44
+ },
45
+ },
46
+ },
47
+ ];
48
+
49
+ interface Book {
50
+ id: string;
51
+ title: string;
52
+ authors: Person[];
53
+ }
54
+
55
+ interface Person {
56
+ birth_year?: number;
57
+ death_year?: number;
58
+ name: string;
59
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "Dust",
3
+ "name": "dust"
4
+ }
@@ -0,0 +1,82 @@
1
+ export async function searchBurns(searchTerms: string[]): Promise<Burn[]> {
2
+ const searchQuery = searchTerms.join(' ');
3
+ const url = 'https://api.dust.events/data/festivals.json';
4
+ const response = await fetch(`${url}`);
5
+ const data = await response.json();
6
+ data = data.filter((b) => b.active);
7
+ return data.map((burn: any) => ({
8
+ name: burn.name,
9
+ title: burn.title,
10
+ year: burn.year,
11
+ startDate: burn.start,
12
+ endDate: burn.end,
13
+ lat: burn.lat,
14
+ long: burn.long,
15
+ timeZone: burn.timeZone,
16
+ region: burn.region,
17
+ website: burn.website
18
+ }));
19
+ }
20
+
21
+ export const toolMap = {
22
+ searchBurns
23
+ }
24
+
25
+ export default function tools() {
26
+ return burnTools;
27
+ }
28
+
29
+ const burnTools = [
30
+ {
31
+ type: 'function',
32
+ function: {
33
+ name: 'searchBurns',
34
+ description:
35
+ "Search for regional burning man events based on specified search terms",
36
+ parameters: {
37
+ type: 'object',
38
+ properties: {
39
+ searchTerms: { // Must match argument name in function
40
+ type: 'array',
41
+ items: {
42
+ type: 'string',
43
+ },
44
+ description:
45
+ "List of search terms (e.g. ['snrg', 'soak'] to search for regional burns with 'snrg' or 'soak' in the title",
46
+ },
47
+ },
48
+ required: ['searchTerms'],
49
+ },
50
+ },
51
+ },
52
+ ];
53
+
54
+ export interface Burn {
55
+ name: string
56
+ title: string
57
+ year: string
58
+ active: boolean
59
+ id: string
60
+ uid: number
61
+ start: string
62
+ end: string
63
+ lat: any
64
+ long: any
65
+ imageUrl?: string
66
+ timeZone: string
67
+ mapDirection: number
68
+ mastodonHandle: string
69
+ rssFeed: string
70
+ inboxEmail: string
71
+ region: string
72
+ website: string
73
+ unknownDates: boolean
74
+ volunteeripateSubdomain: string
75
+ volunteeripateIdentifier: string
76
+ pin_size_multiplier: number
77
+ camp_registration: boolean
78
+ event_registration: boolean
79
+ pin: string
80
+ directions?: string
81
+ }
82
+
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "tarsk",
3
+ "type": "module",
4
+ "scripts": {
5
+ "build": "tsc",
6
+ "start": "npm run build && npm link && tarsk",
7
+ "restart": "npm unlink tarsk && rm -rf dist && npm run build && npm link && tarsk",
8
+ "dev": "tsx watch src/index.ts"
9
+ },
10
+ "bin": {
11
+ "tarsk": "dist/index.js"
12
+ },
13
+ "dependencies": {
14
+ "@hono/node-server": "^1.14.1",
15
+ "hono": "^4.7.7",
16
+ "ts-blank-space": "^0.6.1"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^20.11.17",
20
+ "tsx": "^4.7.1"
21
+ },
22
+ "version": "0.0.1"
23
+ }