@hemansubedi/aether-ai 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/.gitattributes +3 -0
- package/.github/workflows/live-stats.yml +42 -0
- package/.github/workflows/publish.yml +34 -0
- package/.github/workflows/update-preview.yml +41 -0
- package/INSTALL.md +59 -0
- package/LICENSE +21 -0
- package/README.md +397 -0
- package/assets/aether-arena.svg +72 -0
- package/assets/aether-banner.svg +62 -0
- package/assets/aether-router.svg +129 -0
- package/dist/agent.js +125 -0
- package/dist/arena.js +486 -0
- package/dist/checkpoint.js +105 -0
- package/dist/client.js +95 -0
- package/dist/combos.js +176 -0
- package/dist/commands.js +483 -0
- package/dist/config.js +104 -0
- package/dist/cost.js +176 -0
- package/dist/git.js +52 -0
- package/dist/health.js +81 -0
- package/dist/index.js +272 -0
- package/dist/keys.js +128 -0
- package/dist/memory.js +98 -0
- package/dist/modes.js +68 -0
- package/dist/providers/index.js +32 -0
- package/dist/providers/ollama.js +206 -0
- package/dist/providers/openai-compat.js +181 -0
- package/dist/providers/openrouter.js +189 -0
- package/dist/providers/registry.js +211 -0
- package/dist/router-engine.js +200 -0
- package/dist/router.js +171 -0
- package/dist/server.js +210 -0
- package/dist/session.js +97 -0
- package/dist/settings.js +97 -0
- package/dist/skills.js +100 -0
- package/dist/tokensaver.js +50 -0
- package/dist/tools/filesystem.js +243 -0
- package/dist/tools/git.js +53 -0
- package/dist/tools/glob.js +175 -0
- package/dist/tools/grep.js +193 -0
- package/dist/tools/registry.js +39 -0
- package/dist/tools/vision.js +140 -0
- package/dist/tools/websearch.js +118 -0
- package/dist/tui.js +562 -0
- package/dist/types.js +8 -0
- package/docs/preview.txt +51 -0
- package/docs/screenshots.md +110 -0
- package/docs/stats.md +5 -0
- package/install.ps1 +170 -0
- package/install.sh +196 -0
- package/package.json +34 -0
- package/scripts/generate-stats-card.ts +62 -0
- package/scripts/patch_index.ps1 +17 -0
- package/scripts/release.sh +7 -0
- package/src/agent.ts +146 -0
- package/src/arena.ts +584 -0
- package/src/checkpoint.ts +111 -0
- package/src/client.ts +172 -0
- package/src/combos.ts +199 -0
- package/src/commands.ts +973 -0
- package/src/config.ts +122 -0
- package/src/cost.ts +206 -0
- package/src/git.ts +68 -0
- package/src/health.ts +90 -0
- package/src/index.ts +281 -0
- package/src/keys.ts +135 -0
- package/src/memory.ts +101 -0
- package/src/modes.ts +84 -0
- package/src/providers/index.ts +59 -0
- package/src/providers/ollama.ts +222 -0
- package/src/providers/openai-compat.ts +188 -0
- package/src/providers/openrouter.ts +198 -0
- package/src/providers/registry.ts +223 -0
- package/src/router-engine.ts +214 -0
- package/src/router.ts +195 -0
- package/src/server.ts +242 -0
- package/src/session.ts +111 -0
- package/src/settings.ts +125 -0
- package/src/skills.ts +106 -0
- package/src/tokensaver.ts +57 -0
- package/src/tools/filesystem.ts +258 -0
- package/src/tools/git.ts +53 -0
- package/src/tools/glob.ts +180 -0
- package/src/tools/grep.ts +192 -0
- package/src/tools/registry.ts +54 -0
- package/src/tools/vision.ts +152 -0
- package/src/tools/websearch.ts +130 -0
- package/src/tui.ts +664 -0
- package/src/types.ts +77 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ToolDef, ToolCall } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export type ToolExecutor = (args: Record<string, any>) => Promise<string>;
|
|
4
|
+
|
|
5
|
+
interface RegisteredTool extends ToolDef {
|
|
6
|
+
execute: ToolExecutor;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class ToolRegistry {
|
|
10
|
+
private tools = new Map<string, RegisteredTool>();
|
|
11
|
+
|
|
12
|
+
register(def: ToolDef, execute: ToolExecutor): void {
|
|
13
|
+
this.tools.set(def.name, { ...def, execute });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get(name: string): RegisteredTool | undefined {
|
|
17
|
+
return this.tools.get(name);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
list(): ToolDef[] {
|
|
21
|
+
return Array.from(this.tools.values()).map((t) => ({
|
|
22
|
+
name: t.name,
|
|
23
|
+
description: t.description,
|
|
24
|
+
parameters: t.parameters,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
toJSON(): Array<{
|
|
29
|
+
type: "function";
|
|
30
|
+
function: { name: string; description: string; parameters: Record<string, any> };
|
|
31
|
+
}> {
|
|
32
|
+
return this.list().map((t) => ({
|
|
33
|
+
type: "function",
|
|
34
|
+
function: {
|
|
35
|
+
name: t.name,
|
|
36
|
+
description: t.description,
|
|
37
|
+
parameters: t.parameters,
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async executeTool(name: string, args: Record<string, any>): Promise<string> {
|
|
43
|
+
const registered = this.tools.get(name);
|
|
44
|
+
if (!registered) {
|
|
45
|
+
return `ERROR: Unknown tool "${name}". Available tools: ${Array.from(this.tools.keys()).join(", ")}`;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return await registered.execute(args ?? {});
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
51
|
+
return `ERROR executing tool "${name}": ${message}`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { ToolDef } from "../types.js";
|
|
4
|
+
import { getConfig } from "../config.js";
|
|
5
|
+
import { createProvider } from "../providers/index.js";
|
|
6
|
+
|
|
7
|
+
// Locally-installed vision-capable model (verified 2026-09-02).
|
|
8
|
+
const VISION_MODEL = "hf.co/OBLITERATUS/Qwen3.8-27B-OBLITERATED:Q4_K_M";
|
|
9
|
+
|
|
10
|
+
export interface VisionProvider {
|
|
11
|
+
name: string;
|
|
12
|
+
chatVision(
|
|
13
|
+
messages: Array<{ role: string; content: string; images?: string[] }>,
|
|
14
|
+
opts?: { signal?: AbortSignal; maxTokens?: number }
|
|
15
|
+
): AsyncIterable<{ text?: string; error?: string }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Find a vision-capable provider by inspecting the configured models for the
|
|
20
|
+
* known vision model. Falls back to any provider that lists the vision model.
|
|
21
|
+
*/
|
|
22
|
+
export async function findVisionProvider(): Promise<VisionProvider | null> {
|
|
23
|
+
const cfg = getConfig();
|
|
24
|
+
|
|
25
|
+
// 1. Any provider that has the vision model configured. Force the provider
|
|
26
|
+
// to use ONLY the vision model so resolveModel()/models[0] picks it.
|
|
27
|
+
for (const p of cfg.providers) {
|
|
28
|
+
if (!p.enabled) continue;
|
|
29
|
+
if (p.models.includes(VISION_MODEL)) {
|
|
30
|
+
try {
|
|
31
|
+
const visionConfig: any = { ...p, models: [VISION_MODEL] };
|
|
32
|
+
return wrapProvider(await createProvider(visionConfig), p.name);
|
|
33
|
+
} catch {
|
|
34
|
+
// try next
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. Any provider whose listModels() includes the vision model.
|
|
40
|
+
for (const p of cfg.providers) {
|
|
41
|
+
if (!p.enabled) continue;
|
|
42
|
+
try {
|
|
43
|
+
const probe = await createProvider(p);
|
|
44
|
+
const models = await probe.listModels();
|
|
45
|
+
if (models.includes(VISION_MODEL)) {
|
|
46
|
+
const visionConfig: any = { ...p, models: [VISION_MODEL] };
|
|
47
|
+
return wrapProvider(await createProvider(visionConfig), p.name);
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// ignore
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function wrapProvider(provider: any, name: string): VisionProvider {
|
|
58
|
+
return {
|
|
59
|
+
name,
|
|
60
|
+
async *chatVision(messages, opts?) {
|
|
61
|
+
try {
|
|
62
|
+
// Ollama's OpenAI-compatible endpoint expects `content` as a string
|
|
63
|
+
// plus a top-level `images` array of base64 strings on the message.
|
|
64
|
+
// Other OpenAI-compatible providers accept content arrays; send the
|
|
65
|
+
// Ollama form since that is the primary vision path here.
|
|
66
|
+
const serialised = messages.map((m) => ({
|
|
67
|
+
role: m.role,
|
|
68
|
+
content: m.content,
|
|
69
|
+
...(m.images ? { images: m.images } : {}),
|
|
70
|
+
}));
|
|
71
|
+
for await (const chunk of provider.chat(serialised as any, [], {
|
|
72
|
+
signal: opts?.signal,
|
|
73
|
+
maxTokens: opts?.maxTokens,
|
|
74
|
+
})) {
|
|
75
|
+
if (chunk.type === "text" && chunk.text) yield { text: chunk.text };
|
|
76
|
+
if (chunk.type === "error" && chunk.error) yield { error: chunk.error };
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
yield { error: (err as Error).message };
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function makeVisionTool(rootDir: string): { def: ToolDef; execute: (args: Record<string, any>) => Promise<string> } {
|
|
86
|
+
return {
|
|
87
|
+
def: {
|
|
88
|
+
name: "DescribeImage",
|
|
89
|
+
description:
|
|
90
|
+
"Describe an image file at a given path using a vision-capable model. Returns a textual description.",
|
|
91
|
+
parameters: {
|
|
92
|
+
type: "object",
|
|
93
|
+
properties: {
|
|
94
|
+
path: { type: "string", description: "path to the image file" },
|
|
95
|
+
question: { type: "string", default: "Describe this image in detail.", description: "question about the image" },
|
|
96
|
+
},
|
|
97
|
+
required: ["path"],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
execute: async (args) => {
|
|
101
|
+
const rel = String(args.path ?? "");
|
|
102
|
+
// Resolve relative to the project root, but allow absolute paths to
|
|
103
|
+
// image files anywhere on the filesystem (that is the tool's purpose).
|
|
104
|
+
const target = path.isAbsolute(rel)
|
|
105
|
+
? rel
|
|
106
|
+
: path.resolve(rootDir, rel);
|
|
107
|
+
const question = String(args.question ?? "Describe this image in detail.");
|
|
108
|
+
|
|
109
|
+
let buf: Buffer;
|
|
110
|
+
try {
|
|
111
|
+
if (!fs.existsSync(target)) {
|
|
112
|
+
return `ERROR: image not found: ${rel}`;
|
|
113
|
+
}
|
|
114
|
+
if (!fs.statSync(target).isFile()) {
|
|
115
|
+
return `ERROR: path "${rel}" is not a file`;
|
|
116
|
+
}
|
|
117
|
+
buf = fs.readFileSync(target);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
120
|
+
return `ERROR reading image "${rel}": ${message}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const b64 = buf.toString("base64");
|
|
124
|
+
|
|
125
|
+
const provider = await findVisionProvider();
|
|
126
|
+
if (!provider) {
|
|
127
|
+
return "No vision-capable provider available.";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const messages = [
|
|
131
|
+
{
|
|
132
|
+
role: "user",
|
|
133
|
+
content: question,
|
|
134
|
+
images: [b64],
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
let text = "";
|
|
139
|
+
try {
|
|
140
|
+
for await (const chunk of provider.chatVision(messages, { maxTokens: 1024 })) {
|
|
141
|
+
if (chunk.text) text += chunk.text;
|
|
142
|
+
if (chunk.error) return `ERROR: vision provider failed: ${chunk.error}`;
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
146
|
+
return `ERROR: vision provider failed: ${message}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return text.trim() || "(no description returned)";
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { ToolDef } from "../types.js";
|
|
2
|
+
|
|
3
|
+
const DDG_URL = "https://html.duckduckgo.com/html/";
|
|
4
|
+
const USER_AGENT =
|
|
5
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36";
|
|
6
|
+
|
|
7
|
+
export interface WebSearchResult {
|
|
8
|
+
title: string;
|
|
9
|
+
url: string;
|
|
10
|
+
snippet: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse DuckDuckGo HTML results into a list of {title, url, snippet}.
|
|
15
|
+
* DDG wraps real URLs in a redirect (`uddg` query param); we unwrap them.
|
|
16
|
+
*/
|
|
17
|
+
export function parseDdgResults(html: string): WebSearchResult[] {
|
|
18
|
+
const results: WebSearchResult[] = [];
|
|
19
|
+
// Each result is an <a class="result__a"> whose href points at the redirect.
|
|
20
|
+
const linkRe = /<a\b[^>]*class="[^"]*\bresult__a\b[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
21
|
+
// Snippets live in <a class="result__snippet"> ... </a>.
|
|
22
|
+
const snippetRe = /<a\b[^>]*class="[^"]*\bresult__snippet\b[^"]*"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
23
|
+
|
|
24
|
+
const rawSnippets: string[] = [];
|
|
25
|
+
let sm: RegExpExecArray | null;
|
|
26
|
+
while ((sm = snippetRe.exec(html)) !== null) {
|
|
27
|
+
rawSnippets.push(stripTags(sm[1] ?? ""));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let idx = 0;
|
|
31
|
+
let lm: RegExpExecArray | null;
|
|
32
|
+
while ((lm = linkRe.exec(html)) !== null) {
|
|
33
|
+
const href = lm[1] ?? "";
|
|
34
|
+
const title = stripTags(lm[2] ?? "").trim();
|
|
35
|
+
if (!title) continue;
|
|
36
|
+
const url = unwrapRedirect(href);
|
|
37
|
+
if (!url) continue;
|
|
38
|
+
const snippet = rawSnippets[idx] ?? "";
|
|
39
|
+
results.push({ title, url, snippet });
|
|
40
|
+
idx++;
|
|
41
|
+
}
|
|
42
|
+
return results;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function stripTags(s: string): string {
|
|
46
|
+
return s
|
|
47
|
+
.replace(/<[^>]+>/g, " ")
|
|
48
|
+
.replace(/&/g, "&")
|
|
49
|
+
.replace(/</g, "<")
|
|
50
|
+
.replace(/>/g, ">")
|
|
51
|
+
.replace(/"/g, '"')
|
|
52
|
+
.replace(/'|'/g, "'")
|
|
53
|
+
.replace(/\s+/g, " ")
|
|
54
|
+
.trim();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function unwrapRedirect(href: string): string {
|
|
58
|
+
// DDG wraps real URLs in a redirect like:
|
|
59
|
+
// //duckduckgo.com/l/?uddg=https%3A%2F%2Fexample.com%2Fpage&rut=...
|
|
60
|
+
// The real destination is the `uddg` query param. The hostname can vary
|
|
61
|
+
// (html.duckduckgo.com or duckduckgo.com) and the link may be protocol-
|
|
62
|
+
// relative, so we key off the /l/ path and the uddg param instead.
|
|
63
|
+
try {
|
|
64
|
+
const u = new URL(href, "https://html.duckduckgo.com/");
|
|
65
|
+
if (u.pathname === "/l/") {
|
|
66
|
+
const target = u.searchParams.get("uddg");
|
|
67
|
+
if (target) return decodeURIComponent(target);
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// fall through
|
|
71
|
+
}
|
|
72
|
+
// Some results are absolute URLs directly.
|
|
73
|
+
if (/^https?:\/\//i.test(href)) return href;
|
|
74
|
+
return "";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function makeWebSearchTool(): { def: ToolDef; execute: (args: Record<string, any>) => Promise<string> } {
|
|
78
|
+
return {
|
|
79
|
+
def: {
|
|
80
|
+
name: "WebSearch",
|
|
81
|
+
description:
|
|
82
|
+
"Search the web for current information. Returns titles, URLs, and snippets. Use for recent events or facts not in the project.",
|
|
83
|
+
parameters: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
query: { type: "string", description: "the search query" },
|
|
87
|
+
maxResults: { type: "number", default: 5, description: "max number of results to return" },
|
|
88
|
+
},
|
|
89
|
+
required: ["query"],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
execute: async (args) => {
|
|
93
|
+
const query = String(args.query ?? "").trim();
|
|
94
|
+
if (!query) return "ERROR: query is required";
|
|
95
|
+
const maxResults = Math.max(1, Math.min(20, Number(args.maxResults) || 5));
|
|
96
|
+
|
|
97
|
+
let res: Response;
|
|
98
|
+
try {
|
|
99
|
+
const url = `${DDG_URL}?${new URLSearchParams({ q: query }).toString()}`;
|
|
100
|
+
res = await fetch(url, {
|
|
101
|
+
method: "GET",
|
|
102
|
+
headers: { "User-Agent": USER_AGENT, "Accept": "text/html" },
|
|
103
|
+
});
|
|
104
|
+
} catch (err) {
|
|
105
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
106
|
+
return `ERROR: WebSearch network failure: ${message}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!res.ok) {
|
|
110
|
+
return `ERROR: WebSearch failed: HTTP ${res.status}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let html = "";
|
|
114
|
+
try {
|
|
115
|
+
html = await res.text();
|
|
116
|
+
} catch (err) {
|
|
117
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
118
|
+
return `ERROR: WebSearch failed to read response: ${message}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const parsed = parseDdgResults(html).slice(0, maxResults);
|
|
122
|
+
if (parsed.length === 0) {
|
|
123
|
+
return "No matches found";
|
|
124
|
+
}
|
|
125
|
+
return parsed
|
|
126
|
+
.map((r, i) => `${i + 1}. ${r.title} - ${r.url}\n ${r.snippet}`)
|
|
127
|
+
.join("\n\n");
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|