fidelizare-integrate 0.3.0 → 0.5.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/package.json +1 -1
- package/src/cli.js +11 -4
- package/src/core/agent-loop.js +50 -17
- package/src/core/detect.js +48 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fidelizare-integrate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Asistent de integrare Fidelizare pentru softuri de gestiune si case de marcat. Scaneaza codul, gaseste locul potrivit si propune integrarea API-ului, in siguranta.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/cli.js
CHANGED
|
@@ -52,7 +52,7 @@ async function main() {
|
|
|
52
52
|
rawKey = 's2s_beta_demo_0000000000000000000000000000';
|
|
53
53
|
note('Cheie API demo incarcata in seif (nu apare niciodata in cod sau in agent).', C.ink);
|
|
54
54
|
} else {
|
|
55
|
-
rawKey = await ask('Lipeste cheia ta API Fidelizare:');
|
|
55
|
+
rawKey = val('--key') || process.env.FIDELIZARE_API_KEY || (await ask('Lipeste cheia ta API Fidelizare:'));
|
|
56
56
|
if (!rawKey) { note('Fara cheie API. Inchidem.', C.amber); rl.close(); return; }
|
|
57
57
|
}
|
|
58
58
|
const keyRef = vaultPut(rawKey, 'Fidelizare API key');
|
|
@@ -87,14 +87,21 @@ async function main() {
|
|
|
87
87
|
|
|
88
88
|
const git = gitState(root);
|
|
89
89
|
|
|
90
|
-
// 5a. Agentic tool-loop (real coding agent)
|
|
91
|
-
// applies the integration itself,
|
|
92
|
-
|
|
90
|
+
// 5a. Agentic tool-loop (real coding agent) — the DEFAULT when an LLM key is
|
|
91
|
+
// available. The model finds the files and applies the integration itself,
|
|
92
|
+
// each write gated. `--no-agent` forces the basic deterministic proposer.
|
|
93
|
+
const llmKey = process.env.OPENROUTER_API_KEY || process.env.FIDELIZARE_LLM_KEY;
|
|
94
|
+
if (!has('--no-agent') && !llmKey) {
|
|
95
|
+
note('Pentru integrare cu AI (recomandat), seteaza OPENROUTER_API_KEY. Acum folosesc modul determinist de baza.', C.amber);
|
|
96
|
+
rule();
|
|
97
|
+
}
|
|
98
|
+
if (!has('--no-agent') && llmKey) {
|
|
93
99
|
out(' ' + paint('Agent ', C.ink) + bold(paint(val('--model') || 'Opus 4.8', C.red)) +
|
|
94
100
|
paint(' · scrierile cer confirmare (y / a / N)', C.dim) + '\n\n');
|
|
95
101
|
const r = await runAgentLoop({
|
|
96
102
|
stack: det.stack, rootDir: root, files,
|
|
97
103
|
askRaw: ask, auto: AUTO, model: val('--model'),
|
|
104
|
+
guideUrl: val('--guide') || process.env.FIDELIZARE_GUIDE_URL,
|
|
98
105
|
});
|
|
99
106
|
if (r.ok) {
|
|
100
107
|
writeEnv(root, vaultResolve(keyRef));
|
package/src/core/agent-loop.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// Runs on OpenRouter (function-calling), default Opus 4.8.
|
|
9
9
|
import { readFileSync, existsSync } from 'fs';
|
|
10
10
|
import { relative, join, resolve, extname } from 'path';
|
|
11
|
-
import { execFileSync } from 'child_process';
|
|
11
|
+
import { execFileSync, execSync } from 'child_process';
|
|
12
12
|
import { redact } from './vault.js';
|
|
13
13
|
import { safeCreateFile, safeInsert, insideRoot } from './apply.js';
|
|
14
14
|
import { C, paint, out } from '../ui/ansi.js';
|
|
@@ -16,21 +16,30 @@ import { thinking, toolLine } from '../ui/anim.js';
|
|
|
16
16
|
|
|
17
17
|
const URL = 'https://openrouter.ai/api/v1/chat/completions';
|
|
18
18
|
const DEFAULT_MODEL = 'anthropic/claude-opus-4.8';
|
|
19
|
-
const
|
|
19
|
+
const DEFAULT_GUIDE = 'https://mcp.fidelizare.ro/assets/api-guide.md';
|
|
20
|
+
const MAX_STEPS = 30;
|
|
21
|
+
// Only test/compile runners may be executed via run_test (no arbitrary shell).
|
|
22
|
+
const ALLOW_TEST = /^(node |npm (test|run |ci\b)|npx (jest|mocha|vitest)|python3? |pytest|php |phpunit|ruby |rspec|go (test|build|run)|dotnet (test|build)|javac |mvn )/i;
|
|
20
23
|
|
|
21
|
-
const SYSTEM = `Esti un agent de integrare care lucreaza DIRECT in codul unui soft de
|
|
22
|
-
|
|
23
|
-
Fidelizare astfel incat, cand scannerul citeste un cod de fidelitate (nu un
|
|
24
|
-
produs), softul sa apeleze POST https://fidelizare.ro/api/integration/s2s/submitReceipt.
|
|
24
|
+
const SYSTEM = `Esti un agent de integrare care lucreaza DIRECT in codul unui soft de gestiune
|
|
25
|
+
/ casa de marcat, prin apeluri de tool, ca un coding agent autonom.
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
apeleaza
|
|
27
|
+
Pasi:
|
|
28
|
+
1. Citeste documentatia API cu web_fetch pe URL-ul primit, ca sa stii endpoint,
|
|
29
|
+
antet, corp si reguli.
|
|
30
|
+
2. Orienteaza-te SINGUR in cod (list_files, read_file, grep) si gaseste fisierul si
|
|
31
|
+
linia unde codul detecteaza un cod de fidelitate (cod scanat care NU e produs).
|
|
32
|
+
3. Creeaza un helper (create_file) care apeleaza API-ul conform documentatiei, cu
|
|
33
|
+
cheia din variabila de mediu FIDELIZARE_API_KEY (NICIODATA in cod), in try/catch
|
|
34
|
+
ce nu blocheaza vanzarea.
|
|
35
|
+
4. Insereaza apelul (insert_code) exact pe acea ramura, plus import-ul necesar.
|
|
36
|
+
5. Scrie un test cu mock (create_file) care verifica: apelul se face cu fidelity_card
|
|
37
|
+
= codul scanat; o eroare de retea NU arunca si NU blocheaza fluxul. Ruleaza-l cu
|
|
38
|
+
run_test pana trece.
|
|
39
|
+
6. Verifica sintaxa (check_syntax) si apeleaza finish cu un rezumat scurt.
|
|
32
40
|
|
|
33
|
-
Reguli: modificari minime, fara refactor; pastreaza stilul
|
|
41
|
+
Reguli: modificari minime, fara refactor; pastreaza stilul si limbajul proiectului;
|
|
42
|
+
nu scrie chei in cod; nu inventa cai de fisier.`;
|
|
34
43
|
|
|
35
44
|
const TOOLS = [
|
|
36
45
|
{ type: 'function', function: { name: 'list_files', description: 'Listeaza fisierele sursa din proiect.', parameters: { type: 'object', properties: {} } } },
|
|
@@ -38,14 +47,17 @@ const TOOLS = [
|
|
|
38
47
|
{ type: 'function', function: { name: 'grep', description: 'Cauta un pattern (regex) in tot codul.', parameters: { type: 'object', properties: { pattern: { type: 'string' } }, required: ['pattern'] } } },
|
|
39
48
|
{ type: 'function', function: { name: 'create_file', description: 'Creeaza un fisier nou (gated: confirmare + backup).', parameters: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] } } },
|
|
40
49
|
{ type: 'function', function: { name: 'insert_code', description: 'Insereaza linii dupa atLine (gated: diff + confirmare + backup).', parameters: { type: 'object', properties: { path: { type: 'string' }, atLine: { type: 'number' }, lines: { type: 'array', items: { type: 'string' } }, reason: { type: 'string' } }, required: ['path', 'atLine', 'lines'] } } },
|
|
50
|
+
{ type: 'function', function: { name: 'web_fetch', description: 'Descarca un document de pe web (https), ex. documentatia API.', parameters: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] } } },
|
|
51
|
+
{ type: 'function', function: { name: 'run_test', description: 'Ruleaza o comanda de test/compilare in proiect (gated, doar runnere cunoscute).', parameters: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] } } },
|
|
41
52
|
{ type: 'function', function: { name: 'check_syntax', description: 'Verifica sintaxa unui fisier (doar Node).', parameters: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } } },
|
|
42
53
|
{ type: 'function', function: { name: 'finish', description: 'Termina integrarea.', parameters: { type: 'object', properties: { summary: { type: 'string' } }, required: ['summary'] } } },
|
|
43
54
|
];
|
|
44
55
|
|
|
45
|
-
export async function runAgentLoop({ stack, rootDir, files, askRaw, auto, model,
|
|
56
|
+
export async function runAgentLoop({ stack, rootDir, files, askRaw, auto, model, guideUrl }) {
|
|
46
57
|
const key = process.env.OPENROUTER_API_KEY || process.env.FIDELIZARE_LLM_KEY;
|
|
47
58
|
if (!key) return { ok: false, reason: 'no_key' };
|
|
48
59
|
const usedModel = model || DEFAULT_MODEL;
|
|
60
|
+
const guide = guideUrl || DEFAULT_GUIDE;
|
|
49
61
|
|
|
50
62
|
const rel = (p) => relative(rootDir, p);
|
|
51
63
|
const abs = (p) => resolve(join(rootDir, p));
|
|
@@ -54,7 +66,7 @@ export async function runAgentLoop({ stack, rootDir, files, askRaw, auto, model,
|
|
|
54
66
|
|
|
55
67
|
// Permission harness (opencode-style): read/grep/list/check are auto-allowed;
|
|
56
68
|
// writes ask, with an "always" reply that approves that action for the run.
|
|
57
|
-
const policy = { create_file: 'ask', insert_code: 'ask' };
|
|
69
|
+
const policy = { create_file: 'ask', insert_code: 'ask', run_test: 'ask' };
|
|
58
70
|
const permit = async (action, label) => {
|
|
59
71
|
if (auto || policy[action] === 'allow') return true;
|
|
60
72
|
const a = (await askRaw(label + ' [y=da / a=mereu / N=nu]')).toLowerCase();
|
|
@@ -94,6 +106,25 @@ export async function runAgentLoop({ stack, rootDir, files, askRaw, auto, model,
|
|
|
94
106
|
const r = safeInsert(rootDir, a, atLine, lines);
|
|
95
107
|
changes.push(rel(a)); return 'OK inserat ' + r.added + ' linii dupa ' + r.atLine;
|
|
96
108
|
},
|
|
109
|
+
web_fetch: async ({ url }) => {
|
|
110
|
+
if (!/^https:\/\//i.test(url || '')) return 'doar URL https';
|
|
111
|
+
try {
|
|
112
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(15_000) });
|
|
113
|
+
if (!r.ok) return 'HTTP ' + r.status;
|
|
114
|
+
return redact(await r.text()).slice(0, 9000);
|
|
115
|
+
} catch (e) { return 'eroare fetch: ' + (e?.message || e); }
|
|
116
|
+
},
|
|
117
|
+
run_test: async ({ command }) => {
|
|
118
|
+
const cmd = String(command || '').trim();
|
|
119
|
+
if (!ALLOW_TEST.test(cmd)) return 'comanda nepermisa (doar runnere de test/compilare): ' + cmd;
|
|
120
|
+
if (!(await permit('run_test', ' ↳ rulez `' + cmd + '`?'))) return 'REFUZAT de utilizator';
|
|
121
|
+
try {
|
|
122
|
+
const o = execSync(cmd, { cwd: rootDir, timeout: 90_000, stdio: 'pipe' });
|
|
123
|
+
return 'OK\n' + o.toString().slice(0, 1500);
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return 'ESUAT (cod ' + (e.status ?? '?') + ')\n' + (((e.stdout?.toString() || '') + (e.stderr?.toString() || '')).slice(0, 1500));
|
|
126
|
+
}
|
|
127
|
+
},
|
|
97
128
|
check_syntax: ({ path }) => {
|
|
98
129
|
const a = abs(path);
|
|
99
130
|
if (!existsSync(a)) return 'fisier inexistent';
|
|
@@ -106,12 +137,14 @@ export async function runAgentLoop({ stack, rootDir, files, askRaw, auto, model,
|
|
|
106
137
|
|
|
107
138
|
const messages = [
|
|
108
139
|
{ role: 'system', content: SYSTEM },
|
|
109
|
-
{ role: 'user', content: `Stack: ${stack}. Proiect cu ${files.length} fisiere sursa.
|
|
140
|
+
{ role: 'user', content: `Stack: ${stack}. Documentatie API: ${guide}. Proiect cu ${files.length} fisiere sursa. Incepe cu web_fetch pe documentatie, apoi orienteaza-te in cod, integreaza si scrie + ruleaza un test cu mock.` },
|
|
110
141
|
];
|
|
111
142
|
|
|
112
|
-
const NAMES = { list_files: 'List', read_file: 'Read', grep: 'Grep', create_file: 'Write', insert_code: 'Edit', check_syntax: 'Check', finish: 'Done' };
|
|
143
|
+
const NAMES = { list_files: 'List', read_file: 'Read', grep: 'Grep', create_file: 'Write', insert_code: 'Edit', web_fetch: 'Fetch', run_test: 'Test', check_syntax: 'Check', finish: 'Done' };
|
|
113
144
|
const brief = (name, a) => {
|
|
114
145
|
if (name === 'grep') return a.pattern ? '"' + a.pattern + '"' : '';
|
|
146
|
+
if (name === 'web_fetch') return a.url || '';
|
|
147
|
+
if (name === 'run_test') return a.command ? '`' + a.command + '`' : '';
|
|
115
148
|
if (name === 'insert_code') return (a.path ? rel(abs(a.path)) : '') + (a.atLine ? ' @' + a.atLine : '');
|
|
116
149
|
if (a.path) return rel(abs(a.path));
|
|
117
150
|
return '';
|
package/src/core/detect.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Detect the vendor's stack from manifest/extension signals. Read-only.
|
|
2
|
-
import { readdirSync, existsSync, statSync } from 'fs';
|
|
3
|
-
import { join, extname } from 'path';
|
|
2
|
+
import { readdirSync, existsSync, statSync, readFileSync } from 'fs';
|
|
3
|
+
import { join, extname, basename } from 'path';
|
|
4
4
|
|
|
5
5
|
const MANIFESTS = [
|
|
6
6
|
{ file: 'composer.json', stack: 'php', label: 'PHP' },
|
|
@@ -9,6 +9,9 @@ const MANIFESTS = [
|
|
|
9
9
|
{ file: 'pyproject.toml', stack: 'python', label: 'Python' },
|
|
10
10
|
{ file: 'pom.xml', stack: 'java', label: 'Java' },
|
|
11
11
|
{ file: 'build.gradle', stack: 'java', label: 'Java' },
|
|
12
|
+
{ file: 'Gemfile', stack: 'ruby', label: 'Ruby' },
|
|
13
|
+
{ file: 'go.mod', stack: 'go', label: 'Go' },
|
|
14
|
+
{ file: 'Cargo.toml', stack: 'rust', label: 'Rust' },
|
|
12
15
|
];
|
|
13
16
|
const EXT_STACK = {
|
|
14
17
|
'.cs': { stack: 'csharp', label: 'C# / .NET' },
|
|
@@ -21,11 +24,51 @@ const EXT_STACK = {
|
|
|
21
24
|
'.py': { stack: 'python', label: 'Python' },
|
|
22
25
|
'.ts': { stack: 'node', label: 'Node.js' },
|
|
23
26
|
'.js': { stack: 'node', label: 'Node.js' },
|
|
27
|
+
'.rb': { stack: 'ruby', label: 'Ruby' },
|
|
28
|
+
'.go': { stack: 'go', label: 'Go' },
|
|
29
|
+
'.rs': { stack: 'rust', label: 'Rust' },
|
|
24
30
|
};
|
|
25
|
-
|
|
31
|
+
// Build artifacts, dependencies and tooling dirs — never integration targets.
|
|
32
|
+
// Mirrors what ripgrep skips via .gitignore (opencode uses ripgrep).
|
|
33
|
+
const IGNORE = new Set([
|
|
34
|
+
'node_modules', '.git', '.next', '.nuxt', '.svelte-kit', '.parcel-cache', '.turbo', '.cache',
|
|
35
|
+
'dist', 'build', 'out', 'bin', 'obj', 'target', 'vendor', 'coverage', '__pycache__',
|
|
36
|
+
'.venv', 'venv', 'env', '.idea', '.vscode', '.gradle', 'static', 'tmp', '.expo',
|
|
37
|
+
]);
|
|
38
|
+
// Minified / generated bundles by filename.
|
|
39
|
+
const MINIFIED_NAME = /(\.min\.|\.bundle\.|\.chunk\.|[-_.]esm[-_.]|node_modules_|\.d\.ts$|\.map$)/i;
|
|
40
|
+
|
|
41
|
+
function loadGitignoreNames(dir) {
|
|
42
|
+
// Cheap .gitignore support: collect simple directory/file names to skip.
|
|
43
|
+
const names = new Set();
|
|
44
|
+
try {
|
|
45
|
+
for (const line of readFileSync(join(dir, '.gitignore'), 'utf8').split('\n')) {
|
|
46
|
+
const t = line.trim();
|
|
47
|
+
if (!t || t.startsWith('#') || t.startsWith('!')) continue;
|
|
48
|
+
const name = t.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
49
|
+
if (name && !name.includes('/') && !name.includes('*')) names.add(name);
|
|
50
|
+
}
|
|
51
|
+
} catch { /* no .gitignore */ }
|
|
52
|
+
return names;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// A real source file looks hand-written: not minified, not huge, sane line length.
|
|
56
|
+
function looksLikeSource(file) {
|
|
57
|
+
if (MINIFIED_NAME.test(basename(file))) return false;
|
|
58
|
+
try {
|
|
59
|
+
if (statSync(file).size > 200_000) return false;
|
|
60
|
+
const head = readFileSync(file, 'utf8').slice(0, 4000);
|
|
61
|
+
if (!head.includes('\n') && head.length > 1200) return false; // single huge line
|
|
62
|
+
const lines = head.split('\n').slice(0, 40);
|
|
63
|
+
const avg = lines.reduce((s, l) => s + l.length, 0) / Math.max(1, lines.length);
|
|
64
|
+
if (avg > 400) return false; // minified
|
|
65
|
+
} catch { return false; }
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
26
68
|
|
|
27
69
|
export function listSourceFiles(dir, max = 4000) {
|
|
28
70
|
const out = [];
|
|
71
|
+
const ignored = new Set([...IGNORE, ...loadGitignoreNames(dir)]);
|
|
29
72
|
const walk = (d) => {
|
|
30
73
|
if (out.length >= max) return;
|
|
31
74
|
let entries;
|
|
@@ -33,10 +76,10 @@ export function listSourceFiles(dir, max = 4000) {
|
|
|
33
76
|
for (const e of entries) {
|
|
34
77
|
if (out.length >= max) return;
|
|
35
78
|
if (e.name.startsWith('.') && e.name !== '.') continue;
|
|
36
|
-
if (
|
|
79
|
+
if (ignored.has(e.name)) continue;
|
|
37
80
|
const p = join(d, e.name);
|
|
38
81
|
if (e.isDirectory()) walk(p);
|
|
39
|
-
else if (EXT_STACK[extname(e.name).toLowerCase()]) out.push(p);
|
|
82
|
+
else if (EXT_STACK[extname(e.name).toLowerCase()] && looksLikeSource(p)) out.push(p);
|
|
40
83
|
}
|
|
41
84
|
};
|
|
42
85
|
walk(dir);
|