ashtar 1.0.10
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 +146 -0
- package/cli/guard.cjs +144 -0
- package/desktop/dist/daemon.cjs +20696 -0
- package/desktop/dist/overlay.html +283 -0
- package/desktop/dist/preload.cjs +7 -0
- package/desktop/resources/.gitkeep +1 -0
- package/desktop/resources/capture_testpad.ps1 +125 -0
- package/desktop/resources/find_testpad.ps1 +37 -0
- package/desktop/resources/inject_dll.ps1 +43 -0
- package/desktop/resources/unprotect.dll +0 -0
- package/dist/cli/index.js +657 -0
- package/dist/shared/index.cjs +553 -0
- package/dist/shared/index.d.cts +47 -0
- package/dist/shared/index.d.ts +47 -0
- package/dist/shared/index.js +518 -0
- package/docs/deployment.md +27 -0
- package/package.json +80 -0
- package/shared/keys.json +6 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import dotenv from "dotenv";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { dirname, resolve } from "path";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
|
|
6
|
+
function loadDotenvESM() {
|
|
7
|
+
try {
|
|
8
|
+
const seeds = [];
|
|
9
|
+
try {
|
|
10
|
+
seeds.push(dirname(fileURLToPath(import.meta.url)));
|
|
11
|
+
} catch {}
|
|
12
|
+
seeds.push(process.cwd());
|
|
13
|
+
const candidates = new Set();
|
|
14
|
+
for (const seed of seeds) {
|
|
15
|
+
candidates.add(resolve(seed, ".env"));
|
|
16
|
+
candidates.add(resolve(seed, "..", ".env"));
|
|
17
|
+
candidates.add(resolve(seed, "..", "..", ".env"));
|
|
18
|
+
candidates.add(resolve(seed, "..", "..", "..", ".env"));
|
|
19
|
+
}
|
|
20
|
+
for (const cand of candidates) {
|
|
21
|
+
if (existsSync(cand)) {
|
|
22
|
+
dotenv.config({ path: cand });
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} catch (err) {}
|
|
27
|
+
}
|
|
28
|
+
loadDotenvESM();
|
|
29
|
+
|
|
30
|
+
function isGeminiKey(key) {
|
|
31
|
+
if (!key) return false;
|
|
32
|
+
return key.startsWith("AIzaSy") || key.startsWith("AAQ.");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// shared/crypto.ts
|
|
36
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto";
|
|
37
|
+
var ALGO = "aes-256-gcm";
|
|
38
|
+
function machineScopedSecret() {
|
|
39
|
+
const basis = `${process.platform}|${process.arch}|${process.env.USERNAME ?? process.env.USER ?? "user"}`;
|
|
40
|
+
return createHash("sha256").update(basis).digest();
|
|
41
|
+
}
|
|
42
|
+
function poolSecret() {
|
|
43
|
+
const parts = [112, 97, 103, 101, 97, 105, 45, 112, 111, 111, 108, 45, 118, 50];
|
|
44
|
+
return createHash("sha256").update(Buffer.from(parts)).digest();
|
|
45
|
+
}
|
|
46
|
+
function encryptValue(raw) {
|
|
47
|
+
const iv = randomBytes(12);
|
|
48
|
+
const key = machineScopedSecret();
|
|
49
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
50
|
+
const encrypted = Buffer.concat([cipher.update(raw, "utf8"), cipher.final()]);
|
|
51
|
+
const tag = cipher.getAuthTag();
|
|
52
|
+
return Buffer.concat([iv, tag, encrypted]).toString("base64");
|
|
53
|
+
}
|
|
54
|
+
function decryptValue(payload) {
|
|
55
|
+
const data = Buffer.from(payload, "base64");
|
|
56
|
+
const iv = data.subarray(0, 12);
|
|
57
|
+
const tag = data.subarray(12, 28);
|
|
58
|
+
const encrypted = data.subarray(28);
|
|
59
|
+
const key = machineScopedSecret();
|
|
60
|
+
const decipher = createDecipheriv(ALGO, key, iv);
|
|
61
|
+
decipher.setAuthTag(tag);
|
|
62
|
+
const result = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
63
|
+
return result.toString("utf8");
|
|
64
|
+
}
|
|
65
|
+
function encryptPoolKey(raw) {
|
|
66
|
+
const iv = randomBytes(12);
|
|
67
|
+
const key = poolSecret();
|
|
68
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
69
|
+
const encrypted = Buffer.concat([cipher.update(raw, "utf8"), cipher.final()]);
|
|
70
|
+
const tag = cipher.getAuthTag();
|
|
71
|
+
return Buffer.concat([iv, tag, encrypted]).toString("base64");
|
|
72
|
+
}
|
|
73
|
+
function decryptPoolKey(payload) {
|
|
74
|
+
const data = Buffer.from(payload, "base64");
|
|
75
|
+
const iv = data.subarray(0, 12);
|
|
76
|
+
const tag = data.subarray(12, 28);
|
|
77
|
+
const encrypted = data.subarray(28);
|
|
78
|
+
const key = poolSecret();
|
|
79
|
+
const decipher = createDecipheriv(ALGO, key, iv);
|
|
80
|
+
decipher.setAuthTag(tag);
|
|
81
|
+
const result = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
82
|
+
return result.toString("utf8");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// shared/storage.ts
|
|
86
|
+
import Conf from "conf";
|
|
87
|
+
var conf;
|
|
88
|
+
function getConf() {
|
|
89
|
+
if (!conf) {
|
|
90
|
+
conf = new Conf({
|
|
91
|
+
projectName: "Idlidosa",
|
|
92
|
+
defaults: {
|
|
93
|
+
config: {
|
|
94
|
+
model: "llama-3.3-70b-versatile",
|
|
95
|
+
autoSummarizeOnLoad: false,
|
|
96
|
+
theme: "dark",
|
|
97
|
+
stealth: true
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return conf;
|
|
103
|
+
}
|
|
104
|
+
function saveUserConfig(config) {
|
|
105
|
+
const toSave = { ...config };
|
|
106
|
+
if (toSave.apiKey) {
|
|
107
|
+
toSave.apiKey = encryptValue(toSave.apiKey);
|
|
108
|
+
}
|
|
109
|
+
getConf().set("config", toSave);
|
|
110
|
+
}
|
|
111
|
+
function loadUserConfig() {
|
|
112
|
+
const config = getConf().get("config");
|
|
113
|
+
if (config?.apiKey) {
|
|
114
|
+
try {
|
|
115
|
+
return { ...config, apiKey: decryptValue(config.apiKey) };
|
|
116
|
+
} catch {
|
|
117
|
+
return { ...config, apiKey: void 0 };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return config;
|
|
121
|
+
}
|
|
122
|
+
function patchUserConfig(patch) {
|
|
123
|
+
const current = loadUserConfig();
|
|
124
|
+
const merged = { ...current, ...patch };
|
|
125
|
+
saveUserConfig(merged);
|
|
126
|
+
return merged;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// shared/groq.ts
|
|
130
|
+
import Groq from "groq-sdk";
|
|
131
|
+
|
|
132
|
+
// shared/keyPool.ts
|
|
133
|
+
import { createHash as createHash2 } from "crypto";
|
|
134
|
+
import { readFileSync } from "fs";
|
|
135
|
+
import nodeMachineId from "node-machine-id";
|
|
136
|
+
var machineIdSync = nodeMachineId.machineIdSync ?? nodeMachineId.default?.machineIdSync ?? (() => {
|
|
137
|
+
throw new Error("node-machine-id is unavailable");
|
|
138
|
+
});
|
|
139
|
+
var cachedPool = null;
|
|
140
|
+
function findKeysFile() {
|
|
141
|
+
const seeds = [];
|
|
142
|
+
if (typeof __dirname !== "undefined") {
|
|
143
|
+
seeds.push(__dirname);
|
|
144
|
+
}
|
|
145
|
+
seeds.push(process.cwd());
|
|
146
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
147
|
+
for (const seed of seeds) {
|
|
148
|
+
candidates.add(resolve(seed, "shared", "keys.json"));
|
|
149
|
+
candidates.add(resolve(seed, "..", "shared", "keys.json"));
|
|
150
|
+
candidates.add(resolve(seed, "..", "..", "shared", "keys.json"));
|
|
151
|
+
candidates.add(resolve(seed, "..", "..", "..", "shared", "keys.json"));
|
|
152
|
+
candidates.add(resolve(seed, "keys.json"));
|
|
153
|
+
}
|
|
154
|
+
for (const candidate of candidates) {
|
|
155
|
+
if (existsSync(candidate)) return candidate;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
function loadPool() {
|
|
160
|
+
if (cachedPool) return cachedPool;
|
|
161
|
+
const path = findKeysFile();
|
|
162
|
+
if (!path) {
|
|
163
|
+
cachedPool = [];
|
|
164
|
+
return cachedPool;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const raw = readFileSync(path, "utf8");
|
|
168
|
+
const parsed = JSON.parse(raw);
|
|
169
|
+
const isEncrypted = parsed.v === 2;
|
|
170
|
+
cachedPool = (parsed.keys ?? []).filter((k) => typeof k === "string" && k.length > 0).map((k) => {
|
|
171
|
+
if (isEncrypted) {
|
|
172
|
+
try {
|
|
173
|
+
return decryptPoolKey(k);
|
|
174
|
+
} catch {
|
|
175
|
+
return "";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return k;
|
|
179
|
+
}).filter((k) => k.length > 0 && !k.startsWith("REPLACE_WITH_"));
|
|
180
|
+
} catch {
|
|
181
|
+
cachedPool = [];
|
|
182
|
+
}
|
|
183
|
+
return cachedPool;
|
|
184
|
+
}
|
|
185
|
+
function machineSlot(poolSize) {
|
|
186
|
+
if (poolSize <= 0) return 0;
|
|
187
|
+
let id = "fallback";
|
|
188
|
+
try {
|
|
189
|
+
id = machineIdSync(true);
|
|
190
|
+
} catch {
|
|
191
|
+
id = `${process.platform}|${process.arch}|${process.env.USER ?? process.env.USERNAME ?? "user"}`;
|
|
192
|
+
}
|
|
193
|
+
const digest = createHash2("sha256").update(id).digest();
|
|
194
|
+
return digest.readUInt32BE(0) % poolSize;
|
|
195
|
+
}
|
|
196
|
+
function resolveCurrentKey() {
|
|
197
|
+
const envKey = process.env.GEMINI_API_KEY || process.env.GROQ_API_KEY || process.env.API_KEY;
|
|
198
|
+
if (envKey && envKey.trim().length > 0) {
|
|
199
|
+
return {
|
|
200
|
+
apiKey: envKey.trim(),
|
|
201
|
+
source: "env-variable",
|
|
202
|
+
poolSize: 0
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const config = loadUserConfig();
|
|
206
|
+
const pool = loadPool();
|
|
207
|
+
if (config.apiKey && config.apiKey.trim().length > 0) {
|
|
208
|
+
return {
|
|
209
|
+
apiKey: config.apiKey,
|
|
210
|
+
source: "user-override",
|
|
211
|
+
poolSize: pool.length
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (pool.length === 0) return null;
|
|
215
|
+
let slot = config.keyPoolSlot;
|
|
216
|
+
if (typeof slot !== "number" || slot < 0 || slot >= pool.length) {
|
|
217
|
+
slot = machineSlot(pool.length);
|
|
218
|
+
patchUserConfig({ keyPoolSlot: slot });
|
|
219
|
+
}
|
|
220
|
+
return { apiKey: pool[slot], source: "pool", slot, poolSize: pool.length };
|
|
221
|
+
}
|
|
222
|
+
function rotateKey() {
|
|
223
|
+
const pool = loadPool();
|
|
224
|
+
if (pool.length === 0) return null;
|
|
225
|
+
const config = loadUserConfig();
|
|
226
|
+
const current = typeof config.keyPoolSlot === "number" ? config.keyPoolSlot : machineSlot(pool.length);
|
|
227
|
+
const next = (current + 1) % pool.length;
|
|
228
|
+
patchUserConfig({ keyPoolSlot: next });
|
|
229
|
+
return { apiKey: pool[next], source: "pool", slot: next, poolSize: pool.length };
|
|
230
|
+
}
|
|
231
|
+
function maskKey(key) {
|
|
232
|
+
if (!key) return "(none)";
|
|
233
|
+
if (key.length <= 8) return "********";
|
|
234
|
+
return `${key.slice(0, 4)}\u2026${key.slice(-4)}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// shared/groq.ts
|
|
238
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
239
|
+
var DEFAULT_MODEL = "llama-3.3-70b-versatile";
|
|
240
|
+
var ANSWER_MODEL = "llama-3.3-70b-versatile";
|
|
241
|
+
var CODING_MODEL = "deepseek-r1-distill-llama-70b";
|
|
242
|
+
var VISION_PROMPT = [
|
|
243
|
+
"You are an OCR and screen reader. Extract EVERYTHING visible in this screenshot.",
|
|
244
|
+
"IMPORTANT RULES:",
|
|
245
|
+
"- Extract ALL text, questions, code, options, tables, constraints, and input/output examples.",
|
|
246
|
+
"- Return the EXACT text as it appears. Do not summarize. Do not skip any part.",
|
|
247
|
+
"- Include question numbers, option letters (A, B, C, D), code snippets, EVERYTHING.",
|
|
248
|
+
"- If there is code, reproduce it EXACTLY including variable names, operators (*, +, -, /, %, etc.), and formatting.",
|
|
249
|
+
"- If you see a coding problem, extract the full problem statement, constraints, examples, and any starter code.",
|
|
250
|
+
"- NEVER say 'no question found' or 'question not provided'. There IS content on screen \u2014 extract it.",
|
|
251
|
+
"- If the text is partially visible or blurry, do your best to read it. Guess if needed.",
|
|
252
|
+
"- Output raw extracted text only. No commentary."
|
|
253
|
+
].join("\n");
|
|
254
|
+
var ANSWER_PROMPT = [
|
|
255
|
+
"You are an expert exam solver. You must provide the CORRECT answer.",
|
|
256
|
+
"",
|
|
257
|
+
"OUTPUT FORMAT: Plain text only. No markdown formatting whatsoever.",
|
|
258
|
+
"- Do NOT wrap text in ** or * for emphasis",
|
|
259
|
+
"- Do NOT use # for headings or ``` for code blocks",
|
|
260
|
+
"- Do NOT use bullet markers (-, *, +) for lists",
|
|
261
|
+
"- You MUST keep the * symbol when it appears in code as multiplication, pointer, or any operator",
|
|
262
|
+
"- Write code directly without any code fence markers",
|
|
263
|
+
"",
|
|
264
|
+
"ANSWERING RULES:",
|
|
265
|
+
"- For MCQ: State the correct option letter and the answer text. Be precise.",
|
|
266
|
+
"- For coding problems: Write the COMPLETE working solution. Include ALL operators (*, /, %, +, -, etc.) exactly as needed. Test edge cases mentally.",
|
|
267
|
+
"- For short answer: Give the precise answer only.",
|
|
268
|
+
"- For math: Show the final answer with key steps. Use * for multiplication.",
|
|
269
|
+
"- For fill-in-the-blank: Give the exact word or phrase.",
|
|
270
|
+
"- Do NOT say 'Here is', 'Sure', 'Let me explain', or any filler.",
|
|
271
|
+
"- If the question involves code, write COMPLETE runnable code with correct syntax.",
|
|
272
|
+
"- VERIFY your answer is correct before responding. Accuracy is critical.",
|
|
273
|
+
"- If you see a question, ALWAYS provide an answer. Never say 'question not provided'."
|
|
274
|
+
].join("\n");
|
|
275
|
+
var CODING_PROMPT = [
|
|
276
|
+
"You are an expert competitive programmer. You solve LeetCode, HackerRank, and CodeForces problems.",
|
|
277
|
+
"",
|
|
278
|
+
"CRITICAL RULES:",
|
|
279
|
+
"1. Write the OPTIMAL solution with the best possible time and space complexity.",
|
|
280
|
+
"2. DO NOT write brute force. Use the most efficient algorithm (sliding window, two pointers, hash maps, DP, binary search, etc.).",
|
|
281
|
+
"3. Your code MUST compile and run without errors.",
|
|
282
|
+
"4. Your code MUST pass ALL test cases including edge cases.",
|
|
283
|
+
"5. Your code MUST NOT get TLE (Time Limit Exceeded). Optimize for speed.",
|
|
284
|
+
"",
|
|
285
|
+
"CODE COMPLETENESS (VERY IMPORTANT):",
|
|
286
|
+
"- Include ALL closing brackets } and closing braces",
|
|
287
|
+
"- In C++: ALWAYS end class with }; (semicolon after closing brace)",
|
|
288
|
+
"- In Java: ALWAYS close all brackets }",
|
|
289
|
+
"- In Python: ensure proper indentation",
|
|
290
|
+
"- Include ALL necessary imports/headers",
|
|
291
|
+
"- The code must be copy-paste ready \u2014 compilable as-is",
|
|
292
|
+
"",
|
|
293
|
+
"OUTPUT FORMAT:",
|
|
294
|
+
"- Output PLAIN TEXT only. No markdown. No ``` code fences.",
|
|
295
|
+
"- Keep ALL operators: * (multiply/pointer/dereference), /, %, +, -, &, |, ^, ~, <<, >>",
|
|
296
|
+
"- Write ONLY the solution code. No explanation unless very brief.",
|
|
297
|
+
"- If the problem asks for a class Solution, write the complete class with method.",
|
|
298
|
+
"",
|
|
299
|
+
"VERIFY BEFORE RESPONDING:",
|
|
300
|
+
"- Mentally trace through the examples given in the problem",
|
|
301
|
+
"- Check edge cases: empty input, single element, large input, duplicates",
|
|
302
|
+
"- Ensure time complexity is optimal (usually O(n) or O(n log n), NOT O(n\xB2) or O(n\xB3))",
|
|
303
|
+
"- Ensure the code handles all constraints mentioned in the problem"
|
|
304
|
+
].join("\n");
|
|
305
|
+
function sleep(ms) {
|
|
306
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
307
|
+
}
|
|
308
|
+
function isRotatableError(msg) {
|
|
309
|
+
return /429|rate.?limit|too many requests|quota|resource.?exhausted|organization.?restricted|invalid.?api.?key|authentication|unauthorized|forbidden|deactivated|suspended|revoked|disabled|401|403/i.test(msg);
|
|
310
|
+
}
|
|
311
|
+
var keyCooldowns = /* @__PURE__ */ new Map();
|
|
312
|
+
function markCooling(slot) {
|
|
313
|
+
keyCooldowns.set(slot, Date.now() + 6e4);
|
|
314
|
+
}
|
|
315
|
+
function isCooling(slot) {
|
|
316
|
+
const until = keyCooldowns.get(slot);
|
|
317
|
+
if (!until) return false;
|
|
318
|
+
if (Date.now() >= until) {
|
|
319
|
+
keyCooldowns.delete(slot);
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
function isCodingProblem(text) {
|
|
325
|
+
const lower = text.toLowerCase();
|
|
326
|
+
return /leetcode|hackerrank|codeforces|codechef|geeksforgeeks|atcoder/i.test(lower) || /class\s+solution/i.test(lower) || /\bfunction\s+\w+\s*\(/i.test(lower) || /\b(input|output)\s*:/i.test(lower) && /\b(example|constraint|return)\b/i.test(lower) || /\btime\s*complexity\b/i.test(lower) || /\bgiven\s+(an?\s+)?array\b/i.test(lower) || /\b(vector|listnode|treenode|linked\s*list)\b/i.test(lower) || /\breturn\s+(the|an?)\s+/i.test(lower) && /\bexample\b/i.test(lower);
|
|
327
|
+
}
|
|
328
|
+
function stripThinkingBlocks(text) {
|
|
329
|
+
return text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
|
|
330
|
+
}
|
|
331
|
+
async function callGroq(apiKey, model, systemPrompt, userContent, temperature = 0.2) {
|
|
332
|
+
const isGem = isGeminiKey(apiKey);
|
|
333
|
+
const baseURL = isGem ? (process.env.GEMINI_BASE_URL || "https://generativelanguage.googleapis.com/v1beta") : void 0;
|
|
334
|
+
|
|
335
|
+
let finalContent = userContent;
|
|
336
|
+
let finalModel = model;
|
|
337
|
+
if (Array.isArray(userContent)) {
|
|
338
|
+
const hasImage = userContent.some(item => item && item.type === "image_url");
|
|
339
|
+
if (hasImage) {
|
|
340
|
+
if (isGem) {
|
|
341
|
+
finalModel = process.env.GEMINI_MODEL || "gemini-2.0-flash";
|
|
342
|
+
} else {
|
|
343
|
+
finalModel = "llama-3.3-70b-versatile";
|
|
344
|
+
finalContent = userContent
|
|
345
|
+
.filter(item => item && item.type === "text")
|
|
346
|
+
.map(item => item.text)
|
|
347
|
+
.join("\n\n");
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
finalContent = userContent.map(item => typeof item === "string" ? item : (item?.text || "")).join("\n\n");
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const client = new Groq({
|
|
355
|
+
apiKey,
|
|
356
|
+
baseURL,
|
|
357
|
+
timeout: 9e4,
|
|
358
|
+
maxRetries: 0
|
|
359
|
+
});
|
|
360
|
+
const response = await client.chat.completions.create({
|
|
361
|
+
model: finalModel,
|
|
362
|
+
messages: [
|
|
363
|
+
{ role: "system", content: systemPrompt },
|
|
364
|
+
{ role: "user", content: finalContent }
|
|
365
|
+
],
|
|
366
|
+
temperature,
|
|
367
|
+
max_tokens: 8192
|
|
368
|
+
});
|
|
369
|
+
return response.choices?.[0]?.message?.content ?? "";
|
|
370
|
+
}
|
|
371
|
+
async function askGroq(apiKey, payload, model = DEFAULT_MODEL) {
|
|
372
|
+
const isGem = isGeminiKey(apiKey);
|
|
373
|
+
const gemModel = process.env.GEMINI_MODEL || "gemini-2.5-flash";
|
|
374
|
+
if (isGem) {
|
|
375
|
+
model = gemModel;
|
|
376
|
+
}
|
|
377
|
+
let extractedText = "";
|
|
378
|
+
let rawImageB64 = "";
|
|
379
|
+
const imagesToOcr = [];
|
|
380
|
+
if (payload.screenshotPages && payload.screenshotPages.length > 0) {
|
|
381
|
+
for (const page of payload.screenshotPages) {
|
|
382
|
+
const b64 = page.replace(/^data:image\/\w+;base64,/, "");
|
|
383
|
+
imagesToOcr.push(b64);
|
|
384
|
+
}
|
|
385
|
+
} else if (payload.screenshotBase64) {
|
|
386
|
+
const b64 = payload.screenshotBase64.replace(/^data:image\/\w+;base64,/, "");
|
|
387
|
+
imagesToOcr.push(b64);
|
|
388
|
+
}
|
|
389
|
+
if (imagesToOcr.length > 0) {
|
|
390
|
+
rawImageB64 = imagesToOcr[0];
|
|
391
|
+
try {
|
|
392
|
+
if (isGem) {
|
|
393
|
+
const visionContent = [
|
|
394
|
+
{
|
|
395
|
+
type: "text",
|
|
396
|
+
text: imagesToOcr.length > 1 ? VISION_PROMPT + "\n\nIMPORTANT: Multiple screenshots are provided showing different parts of the same page (scrolled). Extract text from ALL images and combine into one complete output." : VISION_PROMPT
|
|
397
|
+
}
|
|
398
|
+
];
|
|
399
|
+
for (const b64 of imagesToOcr) {
|
|
400
|
+
visionContent.push({
|
|
401
|
+
type: "image_url",
|
|
402
|
+
image_url: { url: `data:image/png;base64,${b64}` }
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
extractedText = await callGroq(apiKey, model, "You are an OCR assistant. Extract all text from the image(s) accurately.", visionContent);
|
|
406
|
+
} else {
|
|
407
|
+
const tessModule = await import("tesseract.js");
|
|
408
|
+
const recognize = tessModule.recognize || tessModule.default?.recognize;
|
|
409
|
+
const ocrTexts = [];
|
|
410
|
+
for (const b64 of imagesToOcr) {
|
|
411
|
+
const buf = Buffer.from(b64, "base64");
|
|
412
|
+
const ret = await recognize(buf, "eng");
|
|
413
|
+
if (ret?.data?.text) {
|
|
414
|
+
ocrTexts.push(ret.data.text.trim());
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
extractedText = ocrTexts.join("\n\n");
|
|
418
|
+
}
|
|
419
|
+
} catch {
|
|
420
|
+
extractedText = "";
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const contextParts = [];
|
|
424
|
+
if (payload.title) contextParts.push(`Window: ${payload.title}`);
|
|
425
|
+
if (payload.visibleText && payload.visibleText.length > 30) {
|
|
426
|
+
contextParts.push(`App context:
|
|
427
|
+
${payload.visibleText.slice(0, 6e3)}`);
|
|
428
|
+
}
|
|
429
|
+
if (extractedText && extractedText.length > 10) {
|
|
430
|
+
contextParts.push(`Screen content:
|
|
431
|
+
${extractedText}`);
|
|
432
|
+
}
|
|
433
|
+
if (payload.question) {
|
|
434
|
+
contextParts.push(`User question: ${payload.question}`);
|
|
435
|
+
} else {
|
|
436
|
+
contextParts.push("Solve the problem shown above. Give the correct answer.");
|
|
437
|
+
}
|
|
438
|
+
const fullContext = contextParts.join("\n\n");
|
|
439
|
+
const coding = isCodingProblem(fullContext + " " + (payload.title ?? ""));
|
|
440
|
+
const chosenModel = isGem ? gemModel : (coding ? CODING_MODEL : ANSWER_MODEL);
|
|
441
|
+
const chosenPrompt = coding ? CODING_PROMPT : ANSWER_PROMPT;
|
|
442
|
+
const temp = coding ? 0 : 0.1;
|
|
443
|
+
let answerText;
|
|
444
|
+
async function tryAnswer(mdl, prompt, content, t) {
|
|
445
|
+
return callGroq(apiKey, mdl, prompt, content, t);
|
|
446
|
+
}
|
|
447
|
+
if (extractedText && extractedText.length > 10) {
|
|
448
|
+
const answerContent = [{ type: "text", text: fullContext }];
|
|
449
|
+
try {
|
|
450
|
+
answerText = await tryAnswer(chosenModel, chosenPrompt, answerContent, temp);
|
|
451
|
+
} catch {
|
|
452
|
+
answerText = await tryAnswer(ANSWER_MODEL, chosenPrompt, answerContent, temp);
|
|
453
|
+
}
|
|
454
|
+
} else if (rawImageB64) {
|
|
455
|
+
const answerContent = [
|
|
456
|
+
{ type: "text", text: fullContext + "\n\nThe screenshot is attached. Read it and solve the problem." },
|
|
457
|
+
{ type: "image_url", image_url: { url: `data:image/png;base64,${rawImageB64}` } }
|
|
458
|
+
];
|
|
459
|
+
answerText = await callGroq(apiKey, model, chosenPrompt, answerContent, temp);
|
|
460
|
+
} else {
|
|
461
|
+
const answerContent = [{ type: "text", text: fullContext }];
|
|
462
|
+
try {
|
|
463
|
+
answerText = await tryAnswer(chosenModel, chosenPrompt, answerContent, temp);
|
|
464
|
+
} catch {
|
|
465
|
+
answerText = await tryAnswer(ANSWER_MODEL, chosenPrompt, answerContent, temp);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
answerText = stripThinkingBlocks(answerText);
|
|
469
|
+
return {
|
|
470
|
+
text: answerText || "No response generated.",
|
|
471
|
+
model: chosenModel
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
async function askWithAutoRotate(payload, model = DEFAULT_MODEL) {
|
|
475
|
+
const pool = loadPool();
|
|
476
|
+
const totalKeys = Math.max(pool.length, 1);
|
|
477
|
+
let lastError;
|
|
478
|
+
for (let attempt = 0; attempt < totalKeys; attempt++) {
|
|
479
|
+
const resolved2 = resolveCurrentKey();
|
|
480
|
+
if (!resolved2) throw new Error("No API keys available.");
|
|
481
|
+
if (resolved2.slot !== void 0 && isCooling(resolved2.slot)) {
|
|
482
|
+
rotateKey();
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
return await askGroq(resolved2.apiKey, payload, model);
|
|
487
|
+
} catch (err) {
|
|
488
|
+
lastError = err;
|
|
489
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
490
|
+
if (!isRotatableError(msg)) throw err;
|
|
491
|
+
if (resolved2.slot !== void 0) markCooling(resolved2.slot);
|
|
492
|
+
rotateKey();
|
|
493
|
+
await sleep(500);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const resolved = resolveCurrentKey();
|
|
497
|
+
if (resolved) {
|
|
498
|
+
keyCooldowns.clear();
|
|
499
|
+
return await askGroq(resolved.apiKey, payload, model);
|
|
500
|
+
}
|
|
501
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
502
|
+
}
|
|
503
|
+
export {
|
|
504
|
+
DEFAULT_MODEL,
|
|
505
|
+
askGroq,
|
|
506
|
+
askWithAutoRotate,
|
|
507
|
+
decryptPoolKey,
|
|
508
|
+
decryptValue,
|
|
509
|
+
encryptPoolKey,
|
|
510
|
+
encryptValue,
|
|
511
|
+
loadPool,
|
|
512
|
+
loadUserConfig,
|
|
513
|
+
maskKey,
|
|
514
|
+
patchUserConfig,
|
|
515
|
+
resolveCurrentKey,
|
|
516
|
+
rotateKey,
|
|
517
|
+
saveUserConfig
|
|
518
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Deployment Guide
|
|
2
|
+
|
|
3
|
+
## npm package
|
|
4
|
+
|
|
5
|
+
1. `npm login`
|
|
6
|
+
2. `npm run build`
|
|
7
|
+
3. `npm publish --access public`
|
|
8
|
+
|
|
9
|
+
## Chrome / Edge extension package
|
|
10
|
+
|
|
11
|
+
1. `npm run build:extension`
|
|
12
|
+
2. Zip `extension/dist` including `manifest.json`
|
|
13
|
+
3. Upload to Chrome Web Store Developer Dashboard
|
|
14
|
+
4. Upload same zip to Edge Add-ons Dashboard
|
|
15
|
+
|
|
16
|
+
## Electron desktop builds
|
|
17
|
+
|
|
18
|
+
Use electron-builder from the project root:
|
|
19
|
+
|
|
20
|
+
- Windows: `npx electron-builder --win nsis`
|
|
21
|
+
- macOS: `npx electron-builder --mac dmg`
|
|
22
|
+
- Linux: `npx electron-builder --linux AppImage`
|
|
23
|
+
|
|
24
|
+
## GitHub CI/CD
|
|
25
|
+
|
|
26
|
+
CI workflow is in `.github/workflows/ci.yml`.
|
|
27
|
+
You can add release automation by creating a second workflow that runs on tags and uploads build artifacts.
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ashtar",
|
|
3
|
+
"version": "1.0.10",
|
|
4
|
+
"description": "System-wide AI overlay. Hidden from screen sharing.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"npxsks": "dist/cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"cli/guard.cjs",
|
|
12
|
+
"desktop/dist",
|
|
13
|
+
"desktop/resources",
|
|
14
|
+
"shared/keys.json",
|
|
15
|
+
"docs",
|
|
16
|
+
"README.md",
|
|
17
|
+
".env.example"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18.0.0"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "tsx watch desktop/src/daemon.ts",
|
|
24
|
+
"build": "echo \"Using pre-compiled build files.\"",
|
|
25
|
+
"build:shared": "tsup shared/index.ts --format esm,cjs --dts --out-dir dist/shared",
|
|
26
|
+
"build:cli": "tsup cli/index.ts --format esm --out-dir dist/cli && node -e \"require('node:fs').chmodSync('dist/cli/index.js', 0o755)\"",
|
|
27
|
+
"build:daemon": "tsup --config desktop/tsup.config.ts && node -e \"require('node:fs').copyFileSync('desktop/src/overlay.html','desktop/dist/overlay.html');\"",
|
|
28
|
+
"build:extension": "vite build --config extension/vite.config.ts && node -e \"require('node:fs').copyFileSync('extension/manifest.json','extension/dist/manifest.json')\"",
|
|
29
|
+
"encrypt-keys": "node scripts/encrypt-keys.js",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"lint": "echo \"Lint skipped: no TypeScript source files present.\"",
|
|
32
|
+
"clean": "echo \"Clean skipped to prevent erasing pre-compiled build outputs.\"",
|
|
33
|
+
"publish:prep": "npm run build && npm pack"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"groq",
|
|
37
|
+
"llama",
|
|
38
|
+
"ai",
|
|
39
|
+
"overlay",
|
|
40
|
+
"screenshot",
|
|
41
|
+
"system-wide",
|
|
42
|
+
"hotkey",
|
|
43
|
+
"stealth",
|
|
44
|
+
"screen-share-hidden",
|
|
45
|
+
"electron",
|
|
46
|
+
"automation"
|
|
47
|
+
],
|
|
48
|
+
"author": "idlidosa contributors",
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"active-win": "^7.7.2",
|
|
52
|
+
"chalk": "^5.4.1",
|
|
53
|
+
"commander": "^14.0.0",
|
|
54
|
+
"conf": "^13.0.1",
|
|
55
|
+
"dotenv": "^17.2.1",
|
|
56
|
+
"electron": "^41.5.0",
|
|
57
|
+
"groq-sdk": "^0.18.0",
|
|
58
|
+
"node-machine-id": "^1.1.12",
|
|
59
|
+
"ora": "^8.2.0",
|
|
60
|
+
"prompts": "^2.4.2",
|
|
61
|
+
"rcedit": "^5.0.2",
|
|
62
|
+
"screenshot-desktop": "^1.15.1",
|
|
63
|
+
"uiohook-napi": "^1.5.4",
|
|
64
|
+
"zod": "^4.1.5"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@electron/rebuild": "^4.0.4",
|
|
68
|
+
"@types/chrome": "^0.0.328",
|
|
69
|
+
"@types/node": "^24.3.0",
|
|
70
|
+
"@types/prompts": "^2.4.9",
|
|
71
|
+
"@types/screenshot-desktop": "^1.12.3",
|
|
72
|
+
"concurrently": "^9.2.0",
|
|
73
|
+
"rimraf": "^6.0.1",
|
|
74
|
+
"tsup": "^8.5.0",
|
|
75
|
+
"tsx": "^4.20.4",
|
|
76
|
+
"typescript": "^5.9.2",
|
|
77
|
+
"vite": "^7.1.5",
|
|
78
|
+
"vitest": "^3.2.4"
|
|
79
|
+
}
|
|
80
|
+
}
|