@makeshkumar/blueorch-studio 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/bin/cli.js +69 -0
- package/cache-manager.js +191 -0
- package/chat.routes.js +691 -0
- package/llm.routes.js +285 -0
- package/log.routes.js +42 -0
- package/logger.js +86 -0
- package/mcp.routes.js +320 -0
- package/package.json +37 -0
- package/public/3rdpartylicenses.txt +416 -0
- package/public/browser/assets/monaco/_commonjsHelpers-CT9FvmAN.js +1 -0
- package/public/browser/assets/monaco/abap-D-t0cyap.js +1 -0
- package/public/browser/assets/monaco/apex-CcIm7xu6.js +1 -0
- package/public/browser/assets/monaco/assets/css.worker-HnVq6Ewq.js +93 -0
- package/public/browser/assets/monaco/assets/editor.worker-Be8ye1pW.js +26 -0
- package/public/browser/assets/monaco/assets/html.worker-B51mlPHg.js +470 -0
- package/public/browser/assets/monaco/assets/json.worker-DKiEKt88.js +58 -0
- package/public/browser/assets/monaco/assets/ts.worker-CMbG-7ft.js +67731 -0
- package/public/browser/assets/monaco/azcli-BA0tQDCg.js +1 -0
- package/public/browser/assets/monaco/basic-languages/monaco.contribution.js +1 -0
- package/public/browser/assets/monaco/bat-C397hTD6.js +1 -0
- package/public/browser/assets/monaco/bicep-DF5aW17k.js +2 -0
- package/public/browser/assets/monaco/cameligo-plsz8qhj.js +1 -0
- package/public/browser/assets/monaco/clojure-Y2auQMzK.js +1 -0
- package/public/browser/assets/monaco/coffee-Bu45yuWE.js +1 -0
- package/public/browser/assets/monaco/cpp-CkKPQIni.js +1 -0
- package/public/browser/assets/monaco/csharp-CX28MZyh.js +1 -0
- package/public/browser/assets/monaco/csp-D8uWnyxW.js +1 -0
- package/public/browser/assets/monaco/css-CaeNmE3S.js +3 -0
- package/public/browser/assets/monaco/cssMode-CjiAH6dQ.js +1 -0
- package/public/browser/assets/monaco/cypher-DVThT8BS.js +1 -0
- package/public/browser/assets/monaco/dart-CmGfCvrO.js +1 -0
- package/public/browser/assets/monaco/dockerfile-CZqqYdch.js +1 -0
- package/public/browser/assets/monaco/ecl-30fUercY.js +1 -0
- package/public/browser/assets/monaco/editor/editor.main.css +1 -0
- package/public/browser/assets/monaco/editor/editor.main.js +5 -0
- package/public/browser/assets/monaco/editor.api-CalNCsUg.js +903 -0
- package/public/browser/assets/monaco/elixir-xjPaIfzF.js +1 -0
- package/public/browser/assets/monaco/flow9-DqtmStfK.js +1 -0
- package/public/browser/assets/monaco/freemarker2-Cz_sV6Md.js +3 -0
- package/public/browser/assets/monaco/fsharp-BOMdg4U1.js +1 -0
- package/public/browser/assets/monaco/go-D_hbi-Jt.js +1 -0
- package/public/browser/assets/monaco/graphql-CKUU4kLG.js +1 -0
- package/public/browser/assets/monaco/handlebars-OwglfO-1.js +1 -0
- package/public/browser/assets/monaco/hcl-DTaboeZW.js +1 -0
- package/public/browser/assets/monaco/html-Pa1xEWsY.js +1 -0
- package/public/browser/assets/monaco/htmlMode-Bz67EXwp.js +1 -0
- package/public/browser/assets/monaco/ini-CsNwO04R.js +1 -0
- package/public/browser/assets/monaco/java-CI4ZMsH9.js +1 -0
- package/public/browser/assets/monaco/javascript-PczUCGdz.js +1 -0
- package/public/browser/assets/monaco/jsonMode-DULH5oaX.js +7 -0
- package/public/browser/assets/monaco/julia-BwzEvaQw.js +1 -0
- package/public/browser/assets/monaco/kotlin-IUYPiTV8.js +1 -0
- package/public/browser/assets/monaco/language/css/monaco.contribution.js +1 -0
- package/public/browser/assets/monaco/language/html/monaco.contribution.js +1 -0
- package/public/browser/assets/monaco/language/json/monaco.contribution.js +1 -0
- package/public/browser/assets/monaco/language/typescript/monaco.contribution.js +1 -0
- package/public/browser/assets/monaco/less-C0eDYdqa.js +2 -0
- package/public/browser/assets/monaco/lexon-iON-Kj97.js +1 -0
- package/public/browser/assets/monaco/liquid-DqKjdPGy.js +1 -0
- package/public/browser/assets/monaco/loader.js +1368 -0
- package/public/browser/assets/monaco/lspLanguageFeatures-kM9O9rjY.js +4 -0
- package/public/browser/assets/monaco/lua-DtygF91M.js +1 -0
- package/public/browser/assets/monaco/m3-CsR4AuFi.js +1 -0
- package/public/browser/assets/monaco/markdown-C_rD0bIw.js +1 -0
- package/public/browser/assets/monaco/mdx-DEWtB1K5.js +1 -0
- package/public/browser/assets/monaco/mips-CiYP61RB.js +1 -0
- package/public/browser/assets/monaco/monaco.contribution-D2OdxNBt.js +1 -0
- package/public/browser/assets/monaco/monaco.contribution-DO3azKX8.js +1 -0
- package/public/browser/assets/monaco/monaco.contribution-EcChJV6a.js +1 -0
- package/public/browser/assets/monaco/monaco.contribution-qLAYrEOP.js +1 -0
- package/public/browser/assets/monaco/msdax-C38-sJlp.js +1 -0
- package/public/browser/assets/monaco/mysql-CdtbpvbG.js +1 -0
- package/public/browser/assets/monaco/nls.messages-loader.js +1 -0
- package/public/browser/assets/monaco/nls.messages.cs.js.js +17 -0
- package/public/browser/assets/monaco/nls.messages.de.js.js +17 -0
- package/public/browser/assets/monaco/nls.messages.es.js.js +17 -0
- package/public/browser/assets/monaco/nls.messages.fr.js.js +15 -0
- package/public/browser/assets/monaco/nls.messages.it.js.js +15 -0
- package/public/browser/assets/monaco/nls.messages.ja.js.js +17 -0
- package/public/browser/assets/monaco/nls.messages.js.js +10 -0
- package/public/browser/assets/monaco/nls.messages.ko.js.js +25 -0
- package/public/browser/assets/monaco/nls.messages.pl.js.js +17 -0
- package/public/browser/assets/monaco/nls.messages.pt-br.js.js +6 -0
- package/public/browser/assets/monaco/nls.messages.ru.js.js +17 -0
- package/public/browser/assets/monaco/nls.messages.tr.js.js +15 -0
- package/public/browser/assets/monaco/nls.messages.zh-cn.js.js +17 -0
- package/public/browser/assets/monaco/nls.messages.zh-tw.js.js +15 -0
- package/public/browser/assets/monaco/objective-c-CntZFaHX.js +1 -0
- package/public/browser/assets/monaco/pascal-r6kuqfl_.js +1 -0
- package/public/browser/assets/monaco/pascaligo-BiXoTmXh.js +1 -0
- package/public/browser/assets/monaco/perl-DABw_TcH.js +1 -0
- package/public/browser/assets/monaco/pgsql-me_jFXeX.js +1 -0
- package/public/browser/assets/monaco/php-D_kh-9LK.js +1 -0
- package/public/browser/assets/monaco/pla-VfZjczW0.js +1 -0
- package/public/browser/assets/monaco/postiats-BBSzz8Pk.js +1 -0
- package/public/browser/assets/monaco/powerquery-Dt-g_2cc.js +1 -0
- package/public/browser/assets/monaco/powershell-B-7ap1zc.js +1 -0
- package/public/browser/assets/monaco/protobuf-BmtuEB1A.js +2 -0
- package/public/browser/assets/monaco/pug-BRpRNeEb.js +1 -0
- package/public/browser/assets/monaco/python-Cr0UkIbn.js +1 -0
- package/public/browser/assets/monaco/qsharp-BzsFaUU9.js +1 -0
- package/public/browser/assets/monaco/r-f8dDdrp4.js +1 -0
- package/public/browser/assets/monaco/razor-BYAHOTkz.js +1 -0
- package/public/browser/assets/monaco/redis-fvZQY4PI.js +1 -0
- package/public/browser/assets/monaco/redshift-45Et0LQi.js +1 -0
- package/public/browser/assets/monaco/restructuredtext-C7UUFKFD.js +1 -0
- package/public/browser/assets/monaco/ruby-CZO8zYTz.js +1 -0
- package/public/browser/assets/monaco/rust-Bfetafyc.js +1 -0
- package/public/browser/assets/monaco/sb-3GYllVck.js +1 -0
- package/public/browser/assets/monaco/scala-foMgrKo1.js +1 -0
- package/public/browser/assets/monaco/scheme-CHdMtr7p.js +1 -0
- package/public/browser/assets/monaco/scss-C1cmLt9V.js +3 -0
- package/public/browser/assets/monaco/shell-ClXCKCEW.js +1 -0
- package/public/browser/assets/monaco/solidity-MZ6ExpPy.js +1 -0
- package/public/browser/assets/monaco/sophia-DWkuSsPQ.js +1 -0
- package/public/browser/assets/monaco/sparql-AUGFYSyk.js +1 -0
- package/public/browser/assets/monaco/sql-32GpJSV2.js +1 -0
- package/public/browser/assets/monaco/st-CuDFIVZ_.js +1 -0
- package/public/browser/assets/monaco/swift-n-2HociN.js +3 -0
- package/public/browser/assets/monaco/systemverilog-Ch4vA8Yt.js +1 -0
- package/public/browser/assets/monaco/tcl-D74tq1nH.js +1 -0
- package/public/browser/assets/monaco/tsMode-CZz1Umrk.js +11 -0
- package/public/browser/assets/monaco/twig-C6taOxMV.js +1 -0
- package/public/browser/assets/monaco/typescript-DfOrAzoV.js +1 -0
- package/public/browser/assets/monaco/typespec-D-PIh9Xw.js +1 -0
- package/public/browser/assets/monaco/vb-Dyb2648j.js +1 -0
- package/public/browser/assets/monaco/wgsl-BhLXMOR0.js +298 -0
- package/public/browser/assets/monaco/workers-DcJshg-q.js +1 -0
- package/public/browser/assets/monaco/xml-CdsdnY8S.js +1 -0
- package/public/browser/assets/monaco/yaml-DYGvmE88.js +1 -0
- package/public/browser/favicon.ico +0 -0
- package/public/browser/favicon.svg +4 -0
- package/public/browser/index.html +20 -0
- package/public/browser/main-BRU65EMU.js +80 -0
- package/public/browser/polyfills-FFHMD2TL.js +2 -0
- package/public/browser/styles-R37AZPY2.css +1 -0
- package/server.js +60 -0
- package/system.routes.js +150 -0
- package/usage-normalizer.js +117 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* BlueOrch Studio — CLI entry point
|
|
4
|
+
* Usage: npx blueorch [--port 3000]
|
|
5
|
+
* blueorch --port 5000
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { program } from 'commander';
|
|
9
|
+
import { createRequire } from 'module';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { dirname, join } from 'path';
|
|
12
|
+
import open from 'open';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const pkg = require(join(__dirname, '../package.json'));
|
|
18
|
+
|
|
19
|
+
const ts = () =>
|
|
20
|
+
new Date(Date.now() + (5 * 60 + 30) * 60000).toISOString().replace('Z', '+05:30');
|
|
21
|
+
|
|
22
|
+
// ─── CLI definition ───────────────────────────────────────────────────────────
|
|
23
|
+
program
|
|
24
|
+
.name('blueorch')
|
|
25
|
+
.description(pkg.description)
|
|
26
|
+
.version(pkg.version)
|
|
27
|
+
.option('-p, --port <number>', 'Port to run the server on', '3000')
|
|
28
|
+
.parse();
|
|
29
|
+
|
|
30
|
+
const { port: portStr } = program.opts();
|
|
31
|
+
const port = Number(portStr);
|
|
32
|
+
|
|
33
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
34
|
+
console.error(`[ERROR] ${ts()} Invalid port: "${portStr}"`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ─── Set env vars BEFORE importing server.js ─────────────────────────────────
|
|
39
|
+
// dotenv (loaded inside server.js) does not override existing env vars, so
|
|
40
|
+
// these CLI values win over any .env file values.
|
|
41
|
+
process.env.PORT = String(port);
|
|
42
|
+
process.env.NODE_ENV = process.env.NODE_ENV || 'production';
|
|
43
|
+
|
|
44
|
+
console.log(`[INIT] ${ts()} BlueOrch Studio | port=${port} | NODE_ENV=${process.env.NODE_ENV}`);
|
|
45
|
+
|
|
46
|
+
// ─── Start the Express server ─────────────────────────────────────────────────
|
|
47
|
+
await import(join(__dirname, '../server.js'));
|
|
48
|
+
|
|
49
|
+
// ─── Open browser once server is healthy ─────────────────────────────────────
|
|
50
|
+
const appUrl = `http://localhost:${port}`;
|
|
51
|
+
|
|
52
|
+
const pollAndOpen = async (retries = 25) => {
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch(`${appUrl}/api/system/health`);
|
|
55
|
+
if (res.ok) {
|
|
56
|
+
console.log(`[SUCCESS] ${ts()} Server is up — opening ${appUrl}`);
|
|
57
|
+
await open(appUrl);
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
if (retries > 0) {
|
|
61
|
+
setTimeout(() => pollAndOpen(retries - 1), 400);
|
|
62
|
+
} else {
|
|
63
|
+
console.warn(`[WARN] ${ts()} Could not confirm server health — please open ${appUrl} manually`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Give the event-loop a tick for the server to bind its port before polling
|
|
69
|
+
setTimeout(() => pollAndOpen(), 600);
|
package/cache-manager.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cache-manager.js
|
|
3
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
* Gemini Context Cache Manager using @google/genai SDK.
|
|
5
|
+
*
|
|
6
|
+
* Responsibilities:
|
|
7
|
+
* • Build a compact "signature-only" workspace snapshot (file tree, no content)
|
|
8
|
+
* • Create / reuse Gemini cached contexts keyed by workspace path
|
|
9
|
+
* • Auto-delete expired caches every 10 minutes
|
|
10
|
+
* • Provide token-safety truncation for oversized tool outputs
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readdirSync } from 'fs';
|
|
14
|
+
import { join, extname } from 'path';
|
|
15
|
+
|
|
16
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const CACHE_TTL_SECONDS = 3600; // 1 hour
|
|
19
|
+
const MIN_CACHE_CHARS = 4096 * 4; // ~4 096 tokens × 4 chars/token
|
|
20
|
+
const TOKEN_TRUNCATION_LIMIT = 10_000; // chars before truncation kicks in
|
|
21
|
+
const CLEANUP_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
|
|
22
|
+
|
|
23
|
+
const CODE_EXTENSIONS = new Set([
|
|
24
|
+
'.ts', '.js', '.mjs', '.jsx', '.tsx',
|
|
25
|
+
'.py', '.java', '.go', '.rs', '.vue',
|
|
26
|
+
'.css', '.scss', '.less',
|
|
27
|
+
'.html', '.json', '.md', '.yaml', '.yml', '.toml',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const IGNORE_DIRS = new Set([
|
|
31
|
+
'node_modules', '.git', 'dist', 'build', '.angular',
|
|
32
|
+
'__pycache__', '.next', 'coverage', '.cache', 'vendor', 'out',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const ts = () => new Date(Date.now() + (5 * 60 + 30) * 60000).toISOString().replace('Z', '+05:30');
|
|
36
|
+
|
|
37
|
+
// ─── Cache Store ──────────────────────────────────────────────────────────────
|
|
38
|
+
// Map: workspacePath → { cacheName: string, expiresAt: number, apiKey: string }
|
|
39
|
+
|
|
40
|
+
const cacheStore = new Map();
|
|
41
|
+
|
|
42
|
+
// ─── Workspace Snapshot ───────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build a compact signature-only directory tree showing file names only.
|
|
46
|
+
* No file contents are read — keeps the cached context token-efficient.
|
|
47
|
+
* Max depth prevents runaway recursion on deep mono-repos.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} rootPath Absolute workspace path
|
|
50
|
+
* @param {number} [maxDepth=5]
|
|
51
|
+
* @returns {string} Formatted directory listing
|
|
52
|
+
*/
|
|
53
|
+
export function buildWorkspaceSnapshot(rootPath, maxDepth = 5) {
|
|
54
|
+
console.log(`[INIT] ${ts()} buildWorkspaceSnapshot() | root: "${rootPath}"`);
|
|
55
|
+
|
|
56
|
+
const lines = [`# Workspace: ${rootPath}`, ''];
|
|
57
|
+
|
|
58
|
+
function walk(dir, depth, indent) {
|
|
59
|
+
if (depth > maxDepth) { lines.push(`${indent}...`); return; }
|
|
60
|
+
|
|
61
|
+
let entries;
|
|
62
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); }
|
|
63
|
+
catch { return; }
|
|
64
|
+
|
|
65
|
+
// Directories first, then alphabetical
|
|
66
|
+
entries.sort((a, b) => {
|
|
67
|
+
if (a.isDirectory() === b.isDirectory()) return a.name.localeCompare(b.name);
|
|
68
|
+
return a.isDirectory() ? -1 : 1;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
for (const e of entries) {
|
|
72
|
+
if (e.isDirectory()) {
|
|
73
|
+
if (IGNORE_DIRS.has(e.name)) continue;
|
|
74
|
+
lines.push(`${indent}${e.name}/`);
|
|
75
|
+
walk(join(dir, e.name), depth + 1, indent + ' ');
|
|
76
|
+
} else if (CODE_EXTENSIONS.has(extname(e.name))) {
|
|
77
|
+
lines.push(`${indent}${e.name}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
walk(rootPath, 0, '');
|
|
83
|
+
|
|
84
|
+
const snapshot = lines.join('\n');
|
|
85
|
+
console.log(`[SUCCESS] ${ts()} buildWorkspaceSnapshot() | ${lines.length} lines (~${Math.ceil(snapshot.length / 4)} tokens)`);
|
|
86
|
+
return snapshot;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Token Safety Valve ───────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Truncate text if it exceeds TOKEN_TRUNCATION_LIMIT characters.
|
|
93
|
+
* Keeps the first 3 000 and last 3 000 chars; inserts a placeholder in the middle.
|
|
94
|
+
* Prevents "token spikes" when an MCP tool reads a huge file.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} text
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
export function truncateIfLarge(text) {
|
|
100
|
+
if (!text || text.length <= TOKEN_TRUNCATION_LIMIT) return text;
|
|
101
|
+
|
|
102
|
+
const keep = 3000;
|
|
103
|
+
const head = text.slice(0, keep);
|
|
104
|
+
const tail = text.slice(-keep);
|
|
105
|
+
const removed = text.length - keep * 2;
|
|
106
|
+
|
|
107
|
+
console.log(`[INIT] ${ts()} truncateIfLarge: ${text.length} chars → truncated (${removed} chars removed)`);
|
|
108
|
+
return `${head}\n\n[... TRUNCATED FOR OPTIMIZATION (${removed} chars removed) ...]\n\n${tail}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Auto-Cleanup ─────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
async function runCleanup() {
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
for (const [path, entry] of cacheStore.entries()) {
|
|
116
|
+
if (entry.expiresAt > now) continue;
|
|
117
|
+
|
|
118
|
+
console.log(`[INIT] ${ts()} CacheManager cleanup: deleting expired cache | path: "${path}"`);
|
|
119
|
+
try {
|
|
120
|
+
const { GoogleGenAI } = await import('@google/genai');
|
|
121
|
+
const ai = new GoogleGenAI({ apiKey: entry.apiKey });
|
|
122
|
+
await ai.caches.delete({ name: entry.cacheName });
|
|
123
|
+
console.log(`[SUCCESS] ${ts()} CacheManager: deleted remote cache "${entry.cacheName}"`);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.log(`[ERROR] ${ts()} CacheManager cleanup: remote delete failed | ${err.message}`);
|
|
126
|
+
}
|
|
127
|
+
cacheStore.delete(path);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
setInterval(runCleanup, CLEANUP_INTERVAL_MS);
|
|
132
|
+
console.log(`[INIT] ${ts()} CacheManager: auto-cleanup interval registered (every 10 min)`);
|
|
133
|
+
|
|
134
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Return a valid Gemini cacheName for the given workspace path.
|
|
138
|
+
* Creates a new cache if none exists or the existing one has expired.
|
|
139
|
+
* Returns null if the workspace snapshot is too small to be worth caching.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} apiKey
|
|
142
|
+
* @param {string} model Gemini model ID (must support context caching)
|
|
143
|
+
* @param {string} workspacePath Absolute path of the active workspace
|
|
144
|
+
* @param {string} systemContext System instruction to bake into the cache
|
|
145
|
+
* @returns {Promise<string|null>} cacheName or null
|
|
146
|
+
*/
|
|
147
|
+
export async function getOrCreateCache(apiKey, model, workspacePath, systemContext) {
|
|
148
|
+
console.log(`[INIT] ${ts()} CacheManager.getOrCreateCache() | path: "${workspacePath}"`);
|
|
149
|
+
|
|
150
|
+
// 1 ─ Return existing valid cache
|
|
151
|
+
const existing = cacheStore.get(workspacePath);
|
|
152
|
+
if (existing && existing.expiresAt > Date.now()) {
|
|
153
|
+
console.log(`[SUCCESS] ${ts()} CacheManager: CACHE HIT | name: ${existing.cacheName}`);
|
|
154
|
+
return existing.cacheName;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 2 ─ Build compact workspace snapshot (signature-only: names, no content)
|
|
158
|
+
const snapshot = buildWorkspaceSnapshot(workspacePath);
|
|
159
|
+
const contextText = `${systemContext}\n\n${snapshot}`;
|
|
160
|
+
|
|
161
|
+
if (contextText.length < MIN_CACHE_CHARS) {
|
|
162
|
+
console.log(`[SUCCESS] ${ts()} CacheManager: context too small (${contextText.length} chars) — skipping cache`);
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 3 ─ Create remote cache via @google/genai SDK
|
|
167
|
+
try {
|
|
168
|
+
const { GoogleGenAI } = await import('@google/genai');
|
|
169
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
170
|
+
|
|
171
|
+
const cache = await ai.caches.create({
|
|
172
|
+
model,
|
|
173
|
+
config: {
|
|
174
|
+
systemInstruction: systemContext,
|
|
175
|
+
contents: [{ role: 'user', parts: [{ text: snapshot }] }],
|
|
176
|
+
ttl: `${CACHE_TTL_SECONDS}s`,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const cacheName = cache.name;
|
|
181
|
+
const expiresAt = Date.now() + CACHE_TTL_SECONDS * 1000;
|
|
182
|
+
cacheStore.set(workspacePath, { cacheName, expiresAt, apiKey });
|
|
183
|
+
|
|
184
|
+
console.log(`[SUCCESS] ${ts()} CacheManager: cache CREATED | name: ${cacheName}`);
|
|
185
|
+
return cacheName;
|
|
186
|
+
|
|
187
|
+
} catch (err) {
|
|
188
|
+
console.log(`[ERROR] ${ts()} CacheManager.getOrCreateCache() failed: ${err.message}`);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|