@antodevs/groundtruth 0.1.4 → 0.1.5

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 CHANGED
@@ -1,4 +1,4 @@
1
- ![GroundTruth Banner](assets/banner.png)
1
+ ![GroundTruth Banner](assets/banner.svg)
2
2
 
3
3
  # GroundTruth
4
4
 
@@ -0,0 +1,106 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 300" width="100%">
2
+ <defs>
3
+ <!-- Background Gradient -->
4
+ <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
5
+ <stop offset="0%" stop-color="#090a0f" />
6
+ <stop offset="50%" stop-color="#121521" />
7
+ <stop offset="100%" stop-color="#040508" />
8
+ </linearGradient>
9
+
10
+ <!-- Glowing Text Gradient -->
11
+ <linearGradient id="textGlow" x1="0%" y1="0%" x2="100%" y2="0%">
12
+ <stop offset="0%" stop-color="#4F46E5" />
13
+ <stop offset="50%" stop-color="#0ea5e9" />
14
+ <stop offset="100%" stop-color="#8b5cf6" />
15
+ </linearGradient>
16
+
17
+ <!-- Node Gradient -->
18
+ <linearGradient id="nodeGrad" x1="0%" y1="0%" x2="100%" y2="100%">
19
+ <stop offset="0%" stop-color="#38bdf8" />
20
+ <stop offset="100%" stop-color="#818cf8" />
21
+ </linearGradient>
22
+
23
+ <!-- Grid Pattern -->
24
+ <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
25
+ <path d="M 40 0 L 0 0 0 40" fill="none" stroke="rgba(255, 255, 255, 0.03)" stroke-width="1"/>
26
+ </pattern>
27
+
28
+ <filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
29
+ <feGaussianBlur stdDeviation="6" result="blur" />
30
+ <feMerge>
31
+ <feMergeNode in="blur" />
32
+ <feMergeNode in="SourceGraphic" />
33
+ </feMerge>
34
+ </filter>
35
+ </defs>
36
+
37
+ <!-- Deep Dark Solid Background -->
38
+ <rect width="100%" height="100%" fill="url(#bg)" />
39
+
40
+ <!-- Overlay Subtle Grid -->
41
+ <rect width="100%" height="100%" fill="url(#grid)" />
42
+
43
+ <!-- Connecting Lines representing Context Injection (Left) -->
44
+ <g opacity="0.6" stroke-width="2" fill="none" filter="url(#glow)">
45
+ <!-- Primary flow -->
46
+ <path d="M0 150 C 200 150, 300 100, 450 150" stroke="#0ea5e9" opacity="0.8"/>
47
+ <path d="M0 80 C 150 80, 250 150, 400 130" stroke="#4F46E5" opacity="0.5"/>
48
+ <path d="M0 220 C 180 220, 280 150, 420 170" stroke="#8b5cf6" opacity="0.5"/>
49
+
50
+ <!-- Minor branches -->
51
+ <path d="M100 80 L 150 110" stroke="#0ea5e9"/>
52
+ <path d="M200 220 L 250 185" stroke="#8b5cf6"/>
53
+ <path d="M300 100 L 320 130" stroke="#4F46E5"/>
54
+ </g>
55
+
56
+ <!-- Connecting Lines representing Context Injection (Right) -->
57
+ <g opacity="0.6" stroke-width="2" fill="none" filter="url(#glow)">
58
+ <!-- Primary flow -->
59
+ <path d="M1200 150 C 1000 150, 900 200, 750 150" stroke="#0ea5e9" opacity="0.8"/>
60
+ <path d="M1200 80 C 1050 80, 950 150, 800 130" stroke="#8b5cf6" opacity="0.5"/>
61
+ <path d="M1200 220 C 1020 220, 920 150, 780 170" stroke="#4F46E5" opacity="0.5"/>
62
+
63
+ <!-- Minor branches -->
64
+ <path d="M1100 80 L 1050 110" stroke="#0ea5e9"/>
65
+ <path d="M1000 220 L 950 185" stroke="#4F46E5"/>
66
+ <path d="M900 200 L 880 170" stroke="#8b5cf6"/>
67
+ </g>
68
+
69
+ <!-- Data Nodes (Left) -->
70
+ <g fill="url(#nodeGrad)" filter="url(#glow)">
71
+ <circle cx="100" cy="80" r="5" />
72
+ <circle cx="150" cy="110" r="4" />
73
+ <circle cx="200" cy="220" r="6" />
74
+ <circle cx="250" cy="185" r="4" />
75
+ <circle cx="300" cy="100" r="7" />
76
+ <circle cx="320" cy="130" r="3" />
77
+ <circle cx="450" cy="150" r="8" fill="#ffffff"/>
78
+ </g>
79
+
80
+ <!-- Data Nodes (Right) -->
81
+ <g fill="url(#nodeGrad)" filter="url(#glow)">
82
+ <circle cx="1100" cy="80" r="5" />
83
+ <circle cx="1050" cy="110" r="4" />
84
+ <circle cx="1000" cy="220" r="6" />
85
+ <circle cx="950" cy="185" r="4" />
86
+ <circle cx="900" cy="200" r="7" />
87
+ <circle cx="880" cy="170" r="3" />
88
+ <circle cx="750" cy="150" r="8" fill="#ffffff"/>
89
+ </g>
90
+
91
+ <!-- Typography -->
92
+ <text x="50%" y="48%" text-anchor="middle" font-family="-apple-system, system-ui, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif" font-size="94" font-weight="900" fill="url(#textGlow)" filter="url(#glow)" letter-spacing="2">
93
+ GROUNDTRUTH
94
+ </text>
95
+
96
+ <text x="50%" y="48%" text-anchor="middle" font-family="-apple-system, system-ui, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif" font-size="94" font-weight="900" fill="#ffffff" letter-spacing="2">
97
+ GROUNDTRUTH
98
+ </text>
99
+
100
+ <text x="50%" y="68%" text-anchor="middle" font-family="-apple-system, system-ui, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif" font-size="28" font-weight="500" fill="#94a3b8" letter-spacing="4">
101
+ ZERO ZERO-CONFIGURATION CONTEXT INJECTION
102
+ </text>
103
+
104
+ <rect x="350" y="72%" width="500" height="2" fill="rgba(255,255,255,0.1)" />
105
+ <rect x="450" y="72%" width="300" height="2" fill="#0ea5e9" filter="url(#glow)" />
106
+ </svg>
package/index.js CHANGED
@@ -4,14 +4,18 @@
4
4
  * @description Entry point runtime groundtruth delegazione CLI o proxy flow logic.
5
5
  */
6
6
  import { chalk, label } from './src/logger.js';
7
- import { usePackageJson, antigravityMode, claudeCodeMode, port, intervalMinutes, batchSize, version } from './src/cli.js';
7
+ import { usePackageJson, antigravityMode, claudeCodeMode, uninstallMode, port, intervalMinutes, batchSize, version } from './src/cli.js';
8
8
  import { createServer } from './src/proxy.js';
9
- import { autoSetEnv } from './src/env.js';
9
+ import { autoSetEnv, removeEnv } from './src/env.js';
10
10
  import { startWatcher } from './src/watcher.js';
11
11
 
12
12
  // ─── Dispatcher start app logic ──────────────────────
13
13
 
14
- if (antigravityMode) {
14
+ if (uninstallMode) {
15
+ console.log(`\n ${chalk.white.bold('GroundTruth')} ${chalk.gray(`v${version}`)} ${chalk.gray('[uninstall]')}\n`);
16
+ await removeEnv();
17
+ process.exit(0);
18
+ } else if (antigravityMode) {
15
19
  startWatcher({ intervalMinutes, usePackageJson, batchSize });
16
20
  } else if (claudeCodeMode) {
17
21
  const server = await createServer(usePackageJson);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antodevs/groundtruth",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Lightweight Node.js proxy to intercept API requests from coding agents and inject fresh web context",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,5 +1,5 @@
1
1
  name: GroundTruth
2
- version: 0.1.0
2
+ version: 0.1.4
3
3
  description: |
4
4
  GroundTruth is a zero-configuration, transparent middleware context injection layer.
5
5
  It is designed to bridge the deterministic knowledge cutoff gap of LLM-based coding agents
@@ -35,7 +35,7 @@ export class CircuitBreaker {
35
35
  this.onSuccess();
36
36
  return result;
37
37
  } catch (err) {
38
- this.onFailure();
38
+ this.onFailure(err);
39
39
  throw err;
40
40
  }
41
41
  }
@@ -53,8 +53,13 @@ export class CircuitBreaker {
53
53
  }
54
54
  }
55
55
 
56
- onFailure() {
57
- this.failures++;
56
+ onFailure(err) {
57
+ // 429 rate limit apre il circuito immediatamente
58
+ if (err?.message?.includes('429')) {
59
+ this.failures = this.failureThreshold;
60
+ } else {
61
+ this.failures++;
62
+ }
58
63
  this.lastFailureTime = Date.now();
59
64
  if (this.failures >= this.failureThreshold) {
60
65
  this.state = 'OPEN';
package/src/cli.js CHANGED
@@ -13,15 +13,17 @@ const args = process.argv.slice(2);
13
13
  const usePackageJson = args.includes('--use-package-json');
14
14
  const antigravityMode = args.includes('--antigravity');
15
15
  const claudeCodeMode = args.includes('--claude-code');
16
+ const uninstallMode = args.includes('--uninstall');
16
17
 
17
18
  // Stop immediato se nessun mode definito
18
- if (!antigravityMode && !claudeCodeMode) {
19
+ if (!antigravityMode && !claudeCodeMode && !uninstallMode) {
19
20
  console.log();
20
21
  console.log(` ${chalk.white.bold('GroundTruth')} ${chalk.gray(`v${version}`)}`);
21
22
  console.log();
22
23
  console.log(` Usage:`);
23
24
  console.log(` groundtruth --claude-code proxy mode (Claude Code)`);
24
25
  console.log(` groundtruth --antigravity rules mode (Antigravity/Gemini)`);
26
+ console.log(` groundtruth --uninstall remove shell env config`);
25
27
  console.log();
26
28
  console.log(` Options:`);
27
29
  console.log(` --use-package-json use package.json as search query`);
@@ -55,4 +57,4 @@ const batchSize = batchSizeIndex !== -1
55
57
  ? Math.max(2, Math.min(parseInt(args[batchSizeIndex + 1]) || 3, 5))
56
58
  : 3;
57
59
 
58
- export { args, usePackageJson, antigravityMode, claudeCodeMode, port, intervalMinutes, batchSize, version };
60
+ export { args, usePackageJson, antigravityMode, claudeCodeMode, uninstallMode, port, intervalMinutes, batchSize, version };
package/src/env.js CHANGED
@@ -118,3 +118,43 @@ export async function autoSetEnv(p) {
118
118
  log(LOG_WARN, chalk.yellow, chalk.white('env setup error') + ` → ${chalk.yellow(err.message)}`);
119
119
  }
120
120
  }
121
+
122
+ /**
123
+ * @description Rimuove ANTHROPIC_BASE_URL da tutti i file di configurazione shell.
124
+ * @returns {Promise<void>}
125
+ */
126
+ export async function removeEnv() {
127
+ const homeDir = os.homedir();
128
+ const targets = [
129
+ { file: path.join(homeDir, '.zshrc'), pattern: /^export ANTHROPIC_BASE_URL=.*\n?/gm },
130
+ { file: path.join(homeDir, '.bashrc'), pattern: /^export ANTHROPIC_BASE_URL=.*\n?/gm },
131
+ { file: path.join(homeDir, '.bash_profile'), pattern: /^export ANTHROPIC_BASE_URL=.*\n?/gm },
132
+ { file: path.join(homeDir, '.profile'), pattern: /^export ANTHROPIC_BASE_URL=.*\n?/gm },
133
+ { file: path.join(homeDir, '.config', 'fish', 'config.fish'), pattern: /^set -gx ANTHROPIC_BASE_URL .*\n?/gm },
134
+ ];
135
+
136
+ let cleaned = 0;
137
+ for (const t of targets) {
138
+ if (!existsSync(t.file)) continue;
139
+ try {
140
+ const content = await fs.readFile(t.file, 'utf8');
141
+ const result = content.replace(t.pattern, '').replace(/\n{3,}/g, '\n\n');
142
+ if (result !== content) {
143
+ await atomicWrite(t.file, result);
144
+ const rel = t.file.replace(homeDir, '~');
145
+ log(LOG_OK, chalk.green, chalk.white('removed ANTHROPIC_BASE_URL from') + ' ' + chalk.white(rel));
146
+ cleaned++;
147
+ }
148
+ } catch (e) {
149
+ log(LOG_WARN, chalk.yellow, chalk.white(`cannot clean ${path.basename(t.file)}`) + ` → ${chalk.yellow(e.message)}`);
150
+ }
151
+ }
152
+
153
+ if (cleaned === 0) {
154
+ log(LOG_WARN, chalk.yellow, chalk.white('nothing to clean') + ` → ${chalk.yellow('no ANTHROPIC_BASE_URL found in shell configs')}`);
155
+ } else {
156
+ log(LOG_OK, chalk.green, chalk.white(`cleaned ${cleaned} file(s)`));
157
+ }
158
+
159
+ delete process.env.ANTHROPIC_BASE_URL;
160
+ }
package/src/inject.js CHANGED
@@ -33,6 +33,7 @@ export async function injectBlock(filePath, content, blockId) {
33
33
  if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
34
34
  fileContent = fileContent.slice(0, startIndex) + block + fileContent.slice(endIndex + endTag.length);
35
35
  } else {
36
+ fileContent = fileContent.trimEnd() + '\n\n' + block + '\n';
36
37
  }
37
38
 
38
39
  await atomicWrite(filePath, fileContent);
package/src/packages.js CHANGED
@@ -5,6 +5,7 @@
5
5
  import fs from 'fs/promises';
6
6
  import path from 'path';
7
7
  import { createHash } from 'crypto';
8
+ import { chalk, log, LOG_WARN } from './logger.js';
8
9
 
9
10
  // ─── Logica Dipendenze ───────────────────────────────
10
11
 
@@ -41,7 +42,8 @@ export async function readPackageDeps() {
41
42
  selected = selected.concat(filterAndFormat(pkg.devDependencies));
42
43
 
43
44
  return selected.length > 0 ? selected : null;
44
- } catch (_) {
45
+ } catch (err) {
46
+ log(LOG_WARN, chalk.yellow, chalk.white('package.json parse error') + ` → ${chalk.yellow(err.message)}`);
45
47
  return null;
46
48
  }
47
49
  }
package/src/proxy.js CHANGED
@@ -8,6 +8,7 @@ import { webSearch } from './search.js';
8
8
  import { readPackageDeps, buildQuery } from './packages.js';
9
9
  import { chalk, log, LOG_WARN, LOG_BOLT } from './logger.js';
10
10
  import { httpsAgent } from './http-agent.js';
11
+ import { sanitizeWebContent } from './sanitize.js';
11
12
 
12
13
  // ─── HTTP Node server daemon ─────────────────────────
13
14
 
@@ -98,9 +99,9 @@ export async function createServer(usePackageJson) {
98
99
 
99
100
  contextBlock = `\n\n--- WEB CONTEXT (live, ${new Date().toISOString()}) ---\n`;
100
101
  results.forEach((r, i) => {
101
- contextBlock += `${i + 1}. ${r.title}: ${r.snippet} (${r.url})\n`;
102
+ contextBlock += `${i + 1}. ${r.title}: ${sanitizeWebContent(r.snippet, 500)} (${r.url})\n`;
102
103
  });
103
- if (pageText) contextBlock += `\nFULL TEXT:\n${pageText}\n`;
104
+ if (pageText) contextBlock += `\nFULL TEXT:\n${sanitizeWebContent(pageText)}\n`;
104
105
  contextBlock += `--- END WEB CONTEXT ---\n`;
105
106
  didInject = true;
106
107
  } catch (_) {
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @module sanitize
3
+ * @description Sanitizzazione contenuto web contro prompt injection attacks.
4
+ */
5
+
6
+ // Pattern noti di prompt injection che devono essere filtrati
7
+ const DANGEROUS_PATTERNS = [
8
+ /ignore\s+(all\s+)?previous\s+instructions?/gi,
9
+ /disregard\s+(all\s+)?previous/gi,
10
+ /you\s+are\s+now\s+/gi,
11
+ /forget\s+(all\s+)?(your\s+)?instructions?/gi,
12
+ /new\s+instructions?\s*:/gi,
13
+ /system\s*prompt\s*:/gi,
14
+ /\[INST\]/gi,
15
+ /<\|im_start\|>/gi,
16
+ /<\|im_end\|>/gi,
17
+ /```system/gi,
18
+ /ASSISTANT:\s/gi,
19
+ /HUMAN:\s/gi,
20
+ ];
21
+
22
+ /**
23
+ * @description Filtra pattern pericolosi di prompt injection dal testo web scrappato.
24
+ * @param {string} text - Testo raw proveniente da web scraping
25
+ * @param {number} maxLen - Lunghezza massima output (default 8000)
26
+ * @returns {string} Testo sanitizzato
27
+ */
28
+ export function sanitizeWebContent(text, maxLen = 8000) {
29
+ if (!text || typeof text !== 'string') return '';
30
+ let cleaned = text;
31
+ for (const p of DANGEROUS_PATTERNS) {
32
+ cleaned = cleaned.replace(p, '[FILTERED]');
33
+ }
34
+ return cleaned.slice(0, maxLen);
35
+ }
package/src/search.js CHANGED
@@ -9,6 +9,7 @@ import { DOMParser } from 'linkedom';
9
9
  import { searchCache } from './cache.js';
10
10
  import { CircuitBreaker } from './circuit-breaker.js';
11
11
  import { httpAgent, httpsAgent } from './http-agent.js';
12
+ import { sanitizeWebContent } from './sanitize.js';
12
13
 
13
14
  // ─── Config & Cache ──────────────────────────────────
14
15
 
@@ -118,7 +119,7 @@ export async function webSearch(query, parallel = false) {
118
119
  } catch (_) {
119
120
  text = document.body?.textContent || '';
120
121
  }
121
- if (text) return text.replace(/\s+/g, ' ').slice(0, 4000);
122
+ if (text) return sanitizeWebContent(text.replace(/\s+/g, ' '), 4000);
122
123
  }
123
124
  } catch (_) { // fail silenzioso parallelo tollerato per timeout link third-party
124
125
  }
@@ -143,7 +144,7 @@ export async function webSearch(query, parallel = false) {
143
144
  text = document.body?.textContent || '';
144
145
  }
145
146
  if (text) {
146
- pageText = text.replace(/\s+/g, ' ').slice(0, 4000);
147
+ pageText = sanitizeWebContent(text.replace(/\s+/g, ' '), 4000);
147
148
  }
148
149
  }
149
150
  }
package/src/state.js CHANGED
@@ -2,7 +2,8 @@
2
2
  * @module state
3
3
  * @description Persiste la memoria di antigravity prev-hash per fault tolleranza riavvii.
4
4
  */
5
- import { readFile, writeFile, mkdir } from 'fs/promises';
5
+ import { readFile, mkdir } from 'fs/promises';
6
+ import { atomicWrite } from './utils/atomic-write.js';
6
7
  import { existsSync } from 'fs';
7
8
  import path from 'path';
8
9
  import os from 'os';
@@ -33,5 +34,5 @@ export async function loadBatchState() {
33
34
  export async function saveBatchState(map) {
34
35
  await mkdir(STATE_DIR, { recursive: true });
35
36
  const obj = Object.fromEntries(map);
36
- await writeFile(STATE_FILE, JSON.stringify(obj, null, 2), 'utf8');
37
+ await atomicWrite(STATE_FILE, JSON.stringify(obj, null, 2), { backup: false });
37
38
  }
package/src/watcher.js CHANGED
@@ -6,6 +6,7 @@ import os from 'os';
6
6
  import path from 'path';
7
7
  import { webSearch } from './search.js';
8
8
  import { readPackageDeps, buildQuery, groupIntoBatches, batchHash } from './packages.js';
9
+ import { sanitizeWebContent } from './sanitize.js';
9
10
  import { updateGeminiFiles, removeStaleBlocks } from './inject.js';
10
11
  import { chalk, label, log, LOG_WARN, LOG_REFRESH } from './logger.js';
11
12
  import { version } from './cli.js';
@@ -84,16 +85,16 @@ export function startWatcher({ intervalMinutes, usePackageJson, batchSize }) {
84
85
  globalMd += `**Query:** ${query}\n\n`;
85
86
  if (results.length > 0) {
86
87
  globalMd += `### ${results[0].title}\n`;
87
- globalMd += `${results[0].snippet.slice(0, 300)} — ${results[0].url}\n`;
88
+ globalMd += `${sanitizeWebContent(results[0].snippet, 300)} — ${results[0].url}\n`;
88
89
  }
89
90
 
90
91
  let md = `## Live Context — ${batchTitle} (${nowStr})\n`;
91
92
  md += `**Query:** ${query}\n\n`;
92
93
  for (const r of results) {
93
- md += `### ${r.title}\n${r.snippet} — ${r.url}\n\n`;
94
+ md += `### ${r.title}\n${sanitizeWebContent(r.snippet, 500)} — ${r.url}\n\n`;
94
95
  }
95
96
  if (pageText) {
96
- md += `FULL TEXT: ${pageText}\n`;
97
+ md += `FULL TEXT: ${sanitizeWebContent(pageText)}\n`;
97
98
  }
98
99
 
99
100
  await updateGeminiFiles([{
package/assets/banner.png DELETED
Binary file