@krmxd/onegpt 0.0.0-beta

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/src/config.js ADDED
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+ const { ogptNamesMap, tierRouting } = require("./catalog");
7
+
8
+ const CONFIG_DIR = path.join(os.homedir(), ".config", "ogpt");
9
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
10
+
11
+ const DEFAULTS = {
12
+ active_provider: "ollama",
13
+ active_model: "qwen2.5-coder:1.5b",
14
+ ollama: {
15
+ host: "http://127.0.0.1:11434",
16
+ timeout: 300,
17
+ keep_alive: "30m",
18
+ num_ctx: 8192,
19
+ warmup: true,
20
+ },
21
+ model_routing: tierRouting(),
22
+ ogpt_names: ogptNamesMap(),
23
+ aliases: {},
24
+ agent: {
25
+ max_rounds: 25,
26
+ max_tokens: 4096,
27
+ temperature: 0.7,
28
+ system_prompt:
29
+ "You are OGPT, the AI coding assistant built by KareemXD, running as {ogpt_model}. You are an expert programmer. You have tools to read/write files, run commands, search code, and use git - always use tools when needed. When the user asks you to create code or build something, write the files using your write_file tool and provide a clear explanation of what each file does. Be concise, direct, and helpful.",
30
+ },
31
+ ui: {
32
+ stream: true,
33
+ color: true,
34
+ preview: true,
35
+ timing: true,
36
+ },
37
+ session: {
38
+ auto_save: true,
39
+ restore_last: true,
40
+ },
41
+ };
42
+
43
+ function deepMerge(base, over) {
44
+ const r = { ...base };
45
+ for (const [k, v] of Object.entries(over)) {
46
+ if (r[k] && typeof r[k] === "object" && !Array.isArray(r[k]) && typeof v === "object" && !Array.isArray(v)) {
47
+ r[k] = deepMerge(r[k], v);
48
+ } else {
49
+ r[k] = v;
50
+ }
51
+ }
52
+ return r;
53
+ }
54
+
55
+ class Config {
56
+ constructor(filePath) {
57
+ this.path = filePath || CONFIG_FILE;
58
+ fs.mkdirSync(path.dirname(this.path), { recursive: true });
59
+ this._data = {};
60
+ this.load();
61
+ }
62
+
63
+ load() {
64
+ try {
65
+ if (fs.existsSync(this.path)) {
66
+ const saved = JSON.parse(fs.readFileSync(this.path, "utf-8"));
67
+ this._data = deepMerge(DEFAULTS, saved);
68
+ } else {
69
+ this._data = JSON.parse(JSON.stringify(DEFAULTS));
70
+ }
71
+ } catch {
72
+ this._data = JSON.parse(JSON.stringify(DEFAULTS));
73
+ }
74
+ this._envOverride();
75
+ return this._data;
76
+ }
77
+
78
+ save() {
79
+ fs.writeFileSync(this.path, JSON.stringify(this._data, null, 2));
80
+ }
81
+
82
+ get(key, defaultValue) {
83
+ const keys = key.split(".");
84
+ let val = this._data;
85
+ for (const k of keys) {
86
+ if (val && typeof val === "object") val = val[k];
87
+ else return defaultValue;
88
+ if (val === undefined || val === null) return defaultValue;
89
+ }
90
+ return val;
91
+ }
92
+
93
+ set(key, value) {
94
+ const keys = key.split(".");
95
+ let d = this._data;
96
+ for (let i = 0; i < keys.length - 1; i++) {
97
+ if (!d[keys[i]] || typeof d[keys[i]] !== "object") d[keys[i]] = {};
98
+ d = d[keys[i]];
99
+ }
100
+ d[keys[keys.length - 1]] = value;
101
+ this.save();
102
+ }
103
+
104
+ activeProvider() {
105
+ return this._data.active_provider || "ollama";
106
+ }
107
+
108
+ activeModel() {
109
+ return this._data.active_model || "qwen2.5-coder:1.5b";
110
+ }
111
+
112
+ displayName(model) {
113
+ const m = model || this.activeModel();
114
+ const { displayName: catDisplay, MODEL_CATALOG } = require("./catalog");
115
+ const custom = this._data.ogpt_names && this._data.ogpt_names[m];
116
+ if (custom && !MODEL_CATALOG[m]) return custom;
117
+ return catDisplay(m);
118
+ }
119
+
120
+ resolveModel(name) {
121
+ if (!name) return "";
122
+ const { resolveModel: catResolve } = require("./catalog");
123
+ const resolved = catResolve(name);
124
+ if (resolved) return resolved;
125
+ const aliases = this._data.aliases || {};
126
+ const target = aliases[name.toLowerCase()] || aliases[name];
127
+ if (target) {
128
+ // alias may be a full command ("/model oGPT-2a") or a raw model id
129
+ const parts = String(target).trim().split(/\s+/);
130
+ const last = parts[parts.length - 1];
131
+ return catResolve(last) || target;
132
+ }
133
+ for (const [mid, disp] of Object.entries(this._data.ogpt_names || {})) {
134
+ if (disp.toLowerCase() === name.toLowerCase()) return mid;
135
+ }
136
+ return name;
137
+ }
138
+
139
+ data() {
140
+ return this._data;
141
+ }
142
+
143
+ _envOverride() {
144
+ const ollamaHost = process.env.OLLAMA_HOST;
145
+ if (ollamaHost && this._data.ollama) {
146
+ this._data.ollama.host = ollamaHost;
147
+ }
148
+ }
149
+ }
150
+
151
+ let _cfg = null;
152
+ function getConfig(filePath) {
153
+ if (!_cfg) _cfg = new Config(filePath);
154
+ return _cfg;
155
+ }
156
+
157
+ module.exports = { Config, getConfig, DEFAULTS, CONFIG_DIR };
package/src/glob.js ADDED
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ function globSync(pattern, root) {
7
+ root = root || ".";
8
+ const results = [];
9
+ const parts = pattern.split("**");
10
+ if (parts.length === 2) {
11
+ const before = parts[0];
12
+ const after = parts[1].replace(/^\//, "");
13
+ const walk = (dir) => {
14
+ try {
15
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
16
+ const full = path.join(dir, entry.name);
17
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
18
+ walk(full);
19
+ } else if (entry.isFile()) {
20
+ const rel = path.relative(root, full);
21
+ if (matchGlob(after, entry.name)) {
22
+ results.push(rel);
23
+ }
24
+ }
25
+ }
26
+ } catch {}
27
+ };
28
+ walk(path.resolve(root));
29
+ } else {
30
+ const dir = path.dirname(pattern);
31
+ const filePattern = path.basename(pattern);
32
+ try {
33
+ const resolvedDir = path.resolve(root, dir === "." ? "" : dir);
34
+ for (const entry of fs.readdirSync(resolvedDir, { withFileTypes: true })) {
35
+ if (entry.isFile() && matchGlob(filePattern, entry.name)) {
36
+ results.push(path.join(dir, entry.name));
37
+ }
38
+ }
39
+ } catch {}
40
+ }
41
+ return results;
42
+ }
43
+
44
+ function matchGlob(pattern, name) {
45
+ if (pattern === "*") return true;
46
+ if (pattern.startsWith("*.")) {
47
+ return name.endsWith(pattern.slice(1));
48
+ }
49
+ if (pattern.includes("*")) {
50
+ const regex = new RegExp(
51
+ "^" + pattern.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"
52
+ );
53
+ return regex.test(name);
54
+ }
55
+ return name === pattern;
56
+ }
57
+
58
+ module.exports = { globSync };
package/src/index.js ADDED
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+
3
+ const { CLI } = require("./cli");
4
+ const { Agent } = require("./agent");
5
+ const { ToolRegistry } = require("./tools");
6
+ const { getConfig, Config } = require("./config");
7
+ const { OllamaProvider, getSharedProvider } = require("./ollama");
8
+ const catalog = require("./catalog");
9
+ const Platform = require("./platform");
10
+
11
+ module.exports = { CLI, Agent, ToolRegistry, getConfig, Config, OllamaProvider, getSharedProvider, catalog, Platform };
package/src/ollama.js ADDED
@@ -0,0 +1,427 @@
1
+ "use strict";
2
+
3
+ const http = require("http");
4
+ const https = require("https");
5
+ const { URL } = require("url");
6
+
7
+ function normalizeHost(host) {
8
+ if (!host) return "http://127.0.0.1:11434";
9
+ return host.replace(/localhost/g, "127.0.0.1");
10
+ }
11
+
12
+ class OllamaProvider {
13
+ constructor(host, options = {}) {
14
+ this.host = normalizeHost(host || "http://127.0.0.1:11434");
15
+ this.timeout = (options.timeout || 600) * 1000;
16
+ this.maxRetries = options.maxRetries || 3;
17
+ this.retryDelay = options.retryDelay || 1000;
18
+ // Max silence between streamed bytes before giving up.
19
+ // Generous: cold model loads may send nothing for minutes.
20
+ this.idleTimeout = options.idleTimeout || 300 * 1000;
21
+ this.availabilityTtl = options.availabilityTtl || 5000;
22
+ this._modelCache = null;
23
+ this._modelCacheTs = 0;
24
+ this._availCache = null;
25
+ this._availTs = 0;
26
+ this._agent = new http.Agent({
27
+ keepAlive: true,
28
+ keepAliveMsecs: 120000,
29
+ maxSockets: 8,
30
+ maxFreeSockets: 4,
31
+ });
32
+ }
33
+
34
+ _cfg(key, fallback) {
35
+ try {
36
+ const { getConfig } = require("./config");
37
+ return getConfig().get(key, fallback);
38
+ } catch {
39
+ return fallback;
40
+ }
41
+ }
42
+
43
+ _keepAlive() {
44
+ return this._cfg("ollama.keep_alive", "30m");
45
+ }
46
+
47
+ _options(temperature = 0.7, maxTokens = 4096) {
48
+ const opts = {
49
+ temperature,
50
+ num_predict: maxTokens,
51
+ num_ctx: parseInt(this._cfg("ollama.num_ctx", 8192), 10) || 8192,
52
+ };
53
+ const threads = this._cfg("ollama.num_thread", 0);
54
+ if (threads) opts.num_thread = parseInt(threads, 10);
55
+ return opts;
56
+ }
57
+
58
+ async isAvailable() {
59
+ const now = Date.now();
60
+ if (this._availCache !== null && now - this._availTs < this.availabilityTtl) {
61
+ return this._availCache;
62
+ }
63
+ let ok = false;
64
+ try {
65
+ const r = await this._get("/api/tags", 3000);
66
+ ok = r.status === 200;
67
+ } catch {
68
+ ok = false;
69
+ }
70
+ this._availCache = ok;
71
+ this._availTs = Date.now();
72
+ return ok;
73
+ }
74
+
75
+ _markUnavailable() {
76
+ this._availCache = false;
77
+ this._availTs = Date.now();
78
+ }
79
+
80
+ // Preload a model into memory so the first real message is instant.
81
+ // Must pass the same options as chat() (num_ctx etc.), otherwise Ollama
82
+ // reloads the model on the next chat request, causing lag.
83
+ async warmup(model) {
84
+ try {
85
+ const r = await this._post("/api/generate", {
86
+ model,
87
+ keep_alive: this._keepAlive(),
88
+ options: this._options(0.7, 1),
89
+ });
90
+ return r.status === 200;
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ // Models currently loaded in memory (/api/ps).
97
+ async runningModels() {
98
+ try {
99
+ const r = await this._get("/api/ps", 5000);
100
+ if (r.status !== 200) return [];
101
+ return JSON.parse(r.body).models || [];
102
+ } catch {
103
+ return [];
104
+ }
105
+ }
106
+
107
+ // Stream-pull a model, yielding progress objects.
108
+ async *pull(model) {
109
+ try {
110
+ const response = await this._postStream("/api/pull", { model, stream: true });
111
+ if (response.status !== 200) {
112
+ yield { error: `Engine HTTP ${response.status}` };
113
+ return;
114
+ }
115
+ let buffer = "";
116
+ for await (const chunk of response.body) {
117
+ buffer += chunk.toString();
118
+ const lines = buffer.split("\n");
119
+ buffer = lines.pop();
120
+ for (const line of lines) {
121
+ if (!line.trim()) continue;
122
+ try {
123
+ yield JSON.parse(line);
124
+ } catch {}
125
+ }
126
+ }
127
+ } catch (e) {
128
+ yield { error: e.message };
129
+ }
130
+ }
131
+
132
+ async chat(messages, model, options = {}) {
133
+ const { tools, system, temperature = 0.7, maxTokens = 4096 } = options;
134
+ const msgs = this._fmt(messages, system);
135
+ const payload = {
136
+ model,
137
+ messages: msgs,
138
+ stream: false,
139
+ keep_alive: this._keepAlive(),
140
+ options: this._options(temperature, maxTokens),
141
+ };
142
+ if (tools && tools.length) {
143
+ payload.tools = tools.map((t) => this._toOllamaTool(t));
144
+ }
145
+ let lastErr;
146
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
147
+ try {
148
+ const r = await this._post("/api/chat", payload);
149
+ if (r.status !== 200) {
150
+ throw new Error(`Engine HTTP ${r.status}: ${r.body}`);
151
+ }
152
+ const data = JSON.parse(r.body);
153
+ const msg = data.message || {};
154
+ const toolCalls = (msg.tool_calls || []).map((tc) => ({
155
+ id: tc.id || `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
156
+ name: tc.function.name,
157
+ arguments: tc.function.arguments || {},
158
+ }));
159
+ return {
160
+ content: msg.content || "",
161
+ toolCalls,
162
+ usage: {
163
+ prompt: data.prompt_eval_count || 0,
164
+ completion: data.eval_count || 0,
165
+ total: (data.prompt_eval_count || 0) + (data.eval_count || 0),
166
+ },
167
+ model,
168
+ };
169
+ } catch (e) {
170
+ lastErr = e;
171
+ this._markUnavailable();
172
+ if (attempt < this.maxRetries - 1) {
173
+ await sleep(this.retryDelay * (attempt + 1));
174
+ }
175
+ }
176
+ }
177
+ throw lastErr || new Error("Local engine connection failed");
178
+ }
179
+
180
+ async *stream(messages, model, options = {}) {
181
+ const { tools, system, temperature = 0.7, maxTokens = 4096 } = options;
182
+ const msgs = this._fmt(messages, system);
183
+ const payload = {
184
+ model,
185
+ messages: msgs,
186
+ stream: true,
187
+ keep_alive: this._keepAlive(),
188
+ options: this._options(temperature, maxTokens),
189
+ };
190
+ if (tools && tools.length) {
191
+ payload.tools = tools.map((t) => this._toOllamaTool(t));
192
+ }
193
+ let lastErr;
194
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
195
+ try {
196
+ const response = await this._postStream("/api/chat", payload);
197
+ if (response.status !== 200) {
198
+ yield { type: "error", error: `Engine HTTP ${response.status}` };
199
+ return;
200
+ }
201
+ let buffer = "";
202
+ for await (const chunk of response.body) {
203
+ buffer += chunk.toString();
204
+ const lines = buffer.split("\n");
205
+ buffer = lines.pop();
206
+ for (const line of lines) {
207
+ if (!line.trim()) continue;
208
+ try {
209
+ const data = JSON.parse(line);
210
+ if (data.message) {
211
+ const tok = data.message.content || "";
212
+ if (tok) yield { type: "text", text: tok };
213
+ for (const tc of data.message.tool_calls || []) {
214
+ const fn = tc.function || {};
215
+ let args = fn.arguments || {};
216
+ if (typeof args === "string") {
217
+ try { args = JSON.parse(args); } catch { args = {}; }
218
+ }
219
+ yield { type: "tool", id: tc.id || "", name: fn.name, arguments: args };
220
+ }
221
+ }
222
+ if (data.done) {
223
+ yield {
224
+ type: "usage",
225
+ usage: {
226
+ prompt: data.prompt_eval_count || 0,
227
+ completion: data.eval_count || 0,
228
+ total: (data.prompt_eval_count || 0) + (data.eval_count || 0),
229
+ },
230
+ };
231
+ return;
232
+ }
233
+ } catch {}
234
+ }
235
+ }
236
+ return;
237
+ } catch (e) {
238
+ lastErr = e;
239
+ this._markUnavailable();
240
+ if (attempt < this.maxRetries - 1) {
241
+ yield { type: "retry", attempt: attempt + 2, max: this.maxRetries };
242
+ await sleep(this.retryDelay * (attempt + 1));
243
+ }
244
+ }
245
+ }
246
+ yield { type: "error", error: lastErr?.message || "Local engine connection failed" };
247
+ }
248
+
249
+ async listModels(forceRefresh = false) {
250
+ const now = Date.now();
251
+ if (this._modelCache && !forceRefresh && now - this._modelCacheTs < 30000) {
252
+ return this._modelCache;
253
+ }
254
+ try {
255
+ const r = await this._get("/api/tags", 5000);
256
+ if (r.status !== 200) return this._modelCache || [];
257
+ const data = JSON.parse(r.body);
258
+ this._modelCache = (data.models || []).map((m) => ({
259
+ id: m.name,
260
+ name: m.name,
261
+ size: m.size || 0,
262
+ }));
263
+ this._modelCacheTs = now;
264
+ return this._modelCache;
265
+ } catch {
266
+ return this._modelCache || [];
267
+ }
268
+ }
269
+
270
+ async isModelAvailable(model) {
271
+ const models = await this.listModels();
272
+ return models.some((m) => m.id === model);
273
+ }
274
+
275
+ async bestAvailableModel(tier, configuredModel) {
276
+ if (configuredModel && (await this.isModelAvailable(configuredModel))) {
277
+ return configuredModel;
278
+ }
279
+ const { getConfig } = require("./config");
280
+ const routing = getConfig().get("model_routing", {});
281
+ const tierModel = routing[tier] || "";
282
+ if (tierModel && (await this.isModelAvailable(tierModel))) {
283
+ return tierModel;
284
+ }
285
+ const models = await this.listModels();
286
+ if (!models.length) return configuredModel || tierModel || "qwen2.5-coder:1.5b";
287
+ // Prefer the strongest catalog model that fits this machine's RAM
288
+ try {
289
+ const Platform = require("./platform");
290
+ const { recommendForRam } = require("./catalog");
291
+ const fit = recommendForRam(Platform.ramGB());
292
+ if (models.some((m) => m.id === fit)) return fit;
293
+ } catch {}
294
+ const qwen = models.find((m) => m.id.includes("qwen"));
295
+ return qwen ? qwen.id : models[0].id;
296
+ }
297
+
298
+ _fmt(messages, system) {
299
+ const msgs = [];
300
+ if (system) msgs.push({ role: "system", content: system });
301
+ for (const m of messages) {
302
+ const d = { role: m.role, content: m.content };
303
+ if (m.toolCalls && m.toolCalls.length) {
304
+ d.tool_calls = m.toolCalls.map((tc) => ({
305
+ function: { name: tc.name, arguments: tc.arguments },
306
+ }));
307
+ }
308
+ if (m.toolCallId) {
309
+ d.role = "tool";
310
+ d.tool_call_id = m.toolCallId;
311
+ }
312
+ msgs.push(d);
313
+ }
314
+ return msgs;
315
+ }
316
+
317
+ _toOllamaTool(toolDef) {
318
+ return {
319
+ type: "function",
320
+ function: {
321
+ name: toolDef.name,
322
+ description: toolDef.description,
323
+ parameters: toolDef.parameters,
324
+ },
325
+ };
326
+ }
327
+
328
+ _request(method, urlPath, body, timeout) {
329
+ return new Promise((resolve, reject) => {
330
+ const parsed = new URL(urlPath, this.host);
331
+ const isHttps = parsed.protocol === "https:";
332
+ const lib = isHttps ? https : http;
333
+ const req = lib.request(
334
+ {
335
+ hostname: parsed.hostname,
336
+ port: parsed.port,
337
+ path: parsed.pathname + parsed.search,
338
+ method,
339
+ agent: this._agent,
340
+ timeout: timeout || this.timeout,
341
+ headers: body ? { "Content-Type": "application/json" } : {},
342
+ },
343
+ (res) => {
344
+ const chunks = [];
345
+ res.on("data", (c) => chunks.push(c));
346
+ res.on("end", () => {
347
+ resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString() });
348
+ });
349
+ }
350
+ );
351
+ req.on("error", reject);
352
+ req.on("timeout", () => { req.destroy(); reject(new Error("Timeout")); });
353
+ if (body) req.write(JSON.stringify(body));
354
+ req.end();
355
+ });
356
+ }
357
+
358
+ _get(urlPath, timeout) {
359
+ return this._request("GET", urlPath, null, timeout);
360
+ }
361
+
362
+ _post(urlPath, body) {
363
+ return this._request("POST", urlPath, body);
364
+ }
365
+
366
+ _postStream(urlPath, body) {
367
+ return new Promise((resolve, reject) => {
368
+ const parsed = new URL(urlPath, this.host);
369
+ const isHttps = parsed.protocol === "https:";
370
+ const lib = isHttps ? https : http;
371
+ let idleTimer = null;
372
+ const req = lib.request(
373
+ {
374
+ hostname: parsed.hostname,
375
+ port: parsed.port,
376
+ path: parsed.pathname + parsed.search,
377
+ method: "POST",
378
+ agent: this._agent,
379
+ // No fixed socket timeout: an inactivity watchdog below handles stalls,
380
+ // while cold model loads (minutes of silence) are tolerated.
381
+ timeout: 0,
382
+ headers: { "Content-Type": "application/json" },
383
+ },
384
+ (res) => {
385
+ // Reset the idle watchdog on every received chunk
386
+ res.on("data", () => resetIdle());
387
+ resolve({ status: res.statusCode, body: res });
388
+ }
389
+ );
390
+ const clearIdle = () => { if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; } };
391
+ const resetIdle = () => {
392
+ clearIdle();
393
+ idleTimer = setTimeout(() => {
394
+ req.destroy(new Error(`No data from the local engine for ${Math.round(this.idleTimeout / 1000)}s`));
395
+ }, this.idleTimeout);
396
+ };
397
+ req.on("error", (e) => { clearIdle(); reject(e); });
398
+ req.on("close", clearIdle);
399
+ req.write(JSON.stringify(body));
400
+ req.end();
401
+ resetIdle(); // also covers slow cold starts before the first byte
402
+ });
403
+ }
404
+
405
+ close() {
406
+ this._agent.destroy();
407
+ }
408
+ }
409
+
410
+ function sleep(ms) {
411
+ return new Promise((r) => setTimeout(r, ms));
412
+ }
413
+
414
+ // Shared connection pool
415
+ let _shared = null;
416
+ let _sharedHost = "";
417
+
418
+ function getSharedProvider(host) {
419
+ const normalized = normalizeHost(host);
420
+ if (_shared && _sharedHost === normalized) return _shared;
421
+ if (_shared) _shared.close();
422
+ _shared = new OllamaProvider(normalized);
423
+ _sharedHost = normalized;
424
+ return _shared;
425
+ }
426
+
427
+ module.exports = { OllamaProvider, getSharedProvider };
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+
3
+ const os = require("os");
4
+
5
+ class Platform {
6
+ static ramBytes() {
7
+ return os.totalmem();
8
+ }
9
+
10
+ static ramGB() {
11
+ return Math.round((os.totalmem() / (1024 ** 3)) * 10) / 10;
12
+ }
13
+
14
+ static cpuCount() {
15
+ return os.cpus().length || 4;
16
+ }
17
+
18
+ static safeThreads() {
19
+ return Math.max(1, Math.min(Math.floor(this.cpuCount() * 0.6), 8));
20
+ }
21
+
22
+ static recommendTier() {
23
+ const gb = this.ramGB();
24
+ if (gb >= 24) return "pro";
25
+ if (gb >= 8) return "balanced";
26
+ return "fast";
27
+ }
28
+
29
+ static recommendThreads() {
30
+ const cpus = this.cpuCount();
31
+ const gb = this.ramGB();
32
+ if (gb > 0 && gb < 6) return Math.max(1, Math.floor(cpus / 2));
33
+ return this.safeThreads();
34
+ }
35
+ }
36
+
37
+ module.exports = Platform;