akademia 0.0.1 → 0.1.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/README.md CHANGED
@@ -1,9 +1,34 @@
1
- # akademia
1
+ # Akademia CLI
2
2
 
3
- Minimalny pakiet Akademia.pl zabezpieczający nazwę w rejestrze npm.
4
- Pełna biblioteka i API pojawią się wraz ze startem produktu w kwietniu 2026.
3
+ CLI do publicznego API Akademia.pl.
5
4
 
6
- ```js
7
- const akademia = require('akademia');
8
- console.log(akademia());
5
+ ## Instalacja
6
+
7
+ ```bash
8
+ npm install -g akademia
9
+ akademia --help
10
+ ```
11
+
12
+ ## Komendy
13
+
14
+ ```bash
15
+ akademia doctor
16
+ akademia search "landing page"
17
+ akademia show wywiad-przed-startem-projektu
18
+ ```
19
+
20
+ ## Konfiguracja
21
+
22
+ Domyślne API:
23
+
24
+ ```bash
25
+ https://akademia.pl
26
+ ```
27
+
28
+ Możesz podmienić endpoint:
29
+
30
+ ```bash
31
+ AKADEMIA_API_BASE=http://127.0.0.1:8898 akademia doctor
9
32
  ```
33
+
34
+ CLI V1 pobiera tylko publiczne pliki JSON. Nie wysyła kodu projektu, plików, maili ani danych klientów do Akademii.
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ import { AkademiaClient } from "../src/client.js";
3
+ import { writeReport } from "../src/report.js";
4
+ import { scanProject } from "../src/scan.js";
5
+ import { searchResources } from "../src/search.js";
6
+ import { printDoctor, printResource, printScanResults, printSearchResults } from "../src/format.js";
7
+
8
+ const VERSION = "0.1.0";
9
+
10
+ async function main(argv) {
11
+ const { command, args, flags } = parseArgs(argv);
12
+ const client = new AkademiaClient({ baseUrl: flags.baseUrl });
13
+
14
+ if (flags.version) {
15
+ console.log(VERSION);
16
+ return;
17
+ }
18
+
19
+ if (!command || flags.help) {
20
+ printHelp();
21
+ return;
22
+ }
23
+
24
+ if (command === "doctor") {
25
+ const catalog = await client.catalog();
26
+ printDoctor(catalog, client.baseUrl);
27
+ return;
28
+ }
29
+
30
+ if (command === "search") {
31
+ const query = args.join(" ").trim();
32
+ if (!query) throw new Error("Podaj frazę, np. akademia search \"landing page\".");
33
+ const index = await client.searchIndex();
34
+ const results = searchResources(index, query, {
35
+ limit: flags.limit || 10,
36
+ type: flags.type
37
+ });
38
+ printSearchResults(results);
39
+ return;
40
+ }
41
+
42
+ if (command === "show") {
43
+ const id = args[0];
44
+ if (!id) throw new Error("Podaj ID zasobu, np. akademia show wywiad-przed-startem-projektu.");
45
+ const resource = await client.resource(id);
46
+ printResource(resource, { full: flags.full });
47
+ return;
48
+ }
49
+
50
+ if (command === "scan") {
51
+ const target = args[0] || ".";
52
+ const index = await client.searchIndex();
53
+ const result = await scanProject(target, index, {
54
+ limit: flags.limit || 12,
55
+ maxFiles: flags.maxFiles || 250
56
+ });
57
+ if (flags.json) {
58
+ console.log(JSON.stringify(result, null, 2));
59
+ return;
60
+ }
61
+ printScanResults(result);
62
+ return;
63
+ }
64
+
65
+ if (command === "report") {
66
+ const target = args[0] || ".";
67
+ const index = await client.searchIndex();
68
+ const result = await scanProject(target, index, {
69
+ limit: flags.limit || 12,
70
+ maxFiles: flags.maxFiles || 250
71
+ });
72
+ const outputPath = await writeReport(result, { out: flags.out });
73
+ console.log(`Raport zapisany: ${outputPath}`);
74
+ console.log(`Znaleziska: ${result.findings.length}`);
75
+ return;
76
+ }
77
+
78
+ throw new Error(`Nieznana komenda: ${command}`);
79
+ }
80
+
81
+ function parseArgs(argv) {
82
+ const args = [];
83
+ const flags = {};
84
+ let command = "";
85
+
86
+ for (let index = 0; index < argv.length; index += 1) {
87
+ const value = argv[index];
88
+ if (!command && !value.startsWith("-")) {
89
+ command = value;
90
+ continue;
91
+ }
92
+ if (value === "--help" || value === "-h") {
93
+ flags.help = true;
94
+ continue;
95
+ }
96
+ if (value === "--version" || value === "-v") {
97
+ flags.version = true;
98
+ continue;
99
+ }
100
+ if (value === "--full") {
101
+ flags.full = true;
102
+ continue;
103
+ }
104
+ if (value === "--json") {
105
+ flags.json = true;
106
+ continue;
107
+ }
108
+ if (value === "--type") {
109
+ flags.type = argv[index + 1] || "";
110
+ index += 1;
111
+ continue;
112
+ }
113
+ if (value === "--limit") {
114
+ flags.limit = Number(argv[index + 1] || 10);
115
+ index += 1;
116
+ continue;
117
+ }
118
+ if (value === "--max-files") {
119
+ flags.maxFiles = Number(argv[index + 1] || 250);
120
+ index += 1;
121
+ continue;
122
+ }
123
+ if (value === "--out") {
124
+ flags.out = argv[index + 1] || "";
125
+ index += 1;
126
+ continue;
127
+ }
128
+ if (value === "--base-url") {
129
+ flags.baseUrl = argv[index + 1] || "";
130
+ index += 1;
131
+ continue;
132
+ }
133
+ args.push(value);
134
+ }
135
+
136
+ return { command, args, flags };
137
+ }
138
+
139
+ function printHelp() {
140
+ console.log(`Akademia CLI ${VERSION}
141
+
142
+ Użycie:
143
+ akademia doctor
144
+ akademia search "landing page"
145
+ akademia search "umowa" --type prompt --limit 5
146
+ akademia show wywiad-przed-startem-projektu
147
+ akademia show wywiad-przed-startem-projektu --full
148
+ akademia scan .
149
+ akademia scan . --limit 5 --max-files 120
150
+ akademia report .
151
+ akademia report . --out raport-akademii.md
152
+
153
+ Flagi:
154
+ --base-url URL Nadpisuje API, domyślnie https://akademia.pl
155
+ --type TYPE Filtruje search po typie: prompt albo checklist
156
+ --limit N Limit wyników search
157
+ --max-files N Limit plików czytanych przez scan
158
+ --out PATH Ścieżka raportu Markdown dla report
159
+ --full Pokazuje pełną treść w show
160
+ --json Zwraca wynik scan jako JSON
161
+ `);
162
+ }
163
+
164
+ main(process.argv.slice(2)).catch((error) => {
165
+ console.error(`Błąd: ${error.message}`);
166
+ process.exitCode = 1;
167
+ });
package/package.json CHANGED
@@ -1,16 +1,28 @@
1
1
  {
2
2
  "name": "akademia",
3
- "version": "0.0.1",
4
- "description": "Pakiet placeholder Akademia.pl rezerwacja nazwy.",
5
- "main": "index.js",
3
+ "version": "0.1.0",
4
+ "description": "CLI Akademia.pl do lokalnego dostępu do promptów i checklist.",
5
+ "type": "module",
6
+ "bin": {
7
+ "akademia": "bin/akademia.js"
8
+ },
6
9
  "files": [
7
- "index.js"
10
+ "bin",
11
+ "src",
12
+ "README.md"
8
13
  ],
9
- "keywords": [
10
- "akademia",
11
- "aibl",
12
- "skills"
13
- ],
14
- "author": "Mirek Burnejko",
15
- "license": "MIT"
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "scripts": {
21
+ "check": "node --check bin/akademia.js && node --check src/client.js && node --check src/search.js && node --check src/format.js && node --check src/scan.js && node --check src/report.js",
22
+ "doctor": "node bin/akademia.js doctor",
23
+ "search": "node bin/akademia.js search",
24
+ "show": "node bin/akademia.js show",
25
+ "scan": "node bin/akademia.js scan",
26
+ "report": "node bin/akademia.js report"
27
+ }
16
28
  }
package/src/client.js ADDED
@@ -0,0 +1,57 @@
1
+ const DEFAULT_BASE_URL = "https://akademia.pl";
2
+
3
+ export function resolveBaseUrl(value = process.env.AKADEMIA_API_BASE) {
4
+ const raw = String(value || DEFAULT_BASE_URL).trim().replace(/\/+$/, "");
5
+ if (!raw) return DEFAULT_BASE_URL;
6
+ try {
7
+ const url = new URL(raw);
8
+ if (!["http:", "https:"].includes(url.protocol)) {
9
+ throw new Error("invalid protocol");
10
+ }
11
+ return url.toString().replace(/\/+$/, "");
12
+ } catch {
13
+ throw new Error(`Nieprawidłowy AKADEMIA_API_BASE: ${raw}`);
14
+ }
15
+ }
16
+
17
+ export class AkademiaClient {
18
+ constructor(options = {}) {
19
+ this.baseUrl = resolveBaseUrl(options.baseUrl);
20
+ }
21
+
22
+ async catalog() {
23
+ return this.getJson("/api/v1/catalog.json");
24
+ }
25
+
26
+ async searchIndex() {
27
+ return this.getJson("/api/v1/search-index.json");
28
+ }
29
+
30
+ async resource(id) {
31
+ const safeId = sanitizeResourceId(id);
32
+ if (!safeId) throw new Error("Podaj poprawne ID zasobu.");
33
+ return this.getJson(`/api/v1/resources/${safeId}.json`);
34
+ }
35
+
36
+ async getJson(path) {
37
+ const url = `${this.baseUrl}${path}`;
38
+ const response = await fetch(url, {
39
+ headers: {
40
+ Accept: "application/json",
41
+ "User-Agent": "akademia-cli/0.1"
42
+ }
43
+ });
44
+ if (!response.ok) {
45
+ throw new Error(`API zwróciło ${response.status} dla ${url}`);
46
+ }
47
+ return response.json();
48
+ }
49
+ }
50
+
51
+ export function sanitizeResourceId(value) {
52
+ return String(value || "")
53
+ .trim()
54
+ .toLowerCase()
55
+ .replace(/[^a-z0-9-]/g, "")
56
+ .slice(0, 160);
57
+ }
package/src/format.js ADDED
@@ -0,0 +1,105 @@
1
+ export function printDoctor(catalog, baseUrl) {
2
+ const meta = catalog.meta || {};
3
+ const resources = catalog.resources || [];
4
+ const prompts = resources.filter((item) => item.type === "prompt").length;
5
+ const checklists = resources.filter((item) => item.type === "checklist").length;
6
+ const security = meta.security || {};
7
+
8
+ console.log("Akademia CLI");
9
+ console.log(`API: ${baseUrl}`);
10
+ console.log(`Wersja API: ${meta.version || "brak"}`);
11
+ console.log(`Zasoby: ${resources.length}, prompty: ${prompts}, checklisty: ${checklists}`);
12
+ console.log(`Tryb: ${security.mode || "brak"}`);
13
+ console.log(`Przyjmuje dane projektu: ${security.acceptsProjectFiles ? "tak" : "nie"}`);
14
+ console.log(`LLM po stronie serwera: ${security.serverSideLlm ? "tak" : "nie"}`);
15
+ }
16
+
17
+ export function printSearchResults(results) {
18
+ if (!results.length) {
19
+ console.log("Brak wyników.");
20
+ return;
21
+ }
22
+
23
+ for (const [index, item] of results.entries()) {
24
+ console.log(`${index + 1}. ${item.title}`);
25
+ console.log(` id: ${item.id}`);
26
+ console.log(` typ: ${item.type}, zastosowanie: ${item.application || item.applicationSlug || "brak"}`);
27
+ console.log(` opis: ${item.description || item.seoDescription || ""}`);
28
+ console.log(` score: ${item.score}`);
29
+ }
30
+ }
31
+
32
+ export function printResource(resource, options = {}) {
33
+ console.log(`${resource.title}`);
34
+ console.log(`id: ${resource.id}`);
35
+ console.log(`typ: ${resource.type}`);
36
+ console.log(`url: https://akademia.pl${resource.url || ""}`);
37
+ if (resource.description) console.log(`opis: ${resource.description}`);
38
+ if (resource.usage) {
39
+ console.log("");
40
+ console.log("Jak użyć:");
41
+ console.log(wrap(resource.usage));
42
+ }
43
+
44
+ const content = resource.prompt || checklistText(resource);
45
+ if (content) {
46
+ console.log("");
47
+ console.log(resource.prompt ? "Prompt:" : "Checklista:");
48
+ console.log(options.full ? content : truncate(content, 1800));
49
+ }
50
+ }
51
+
52
+ export function printScanResults(result) {
53
+ console.log("Skan Akademii");
54
+ console.log(`Projekt: ${result.root}`);
55
+ console.log(`Przeczytane pliki: ${result.scannedFiles}`);
56
+ console.log(`Znaleziska: ${result.findings.length}`);
57
+
58
+ if (!result.findings.length) {
59
+ console.log("");
60
+ console.log("Brak znalezisk z dowodem w plikach.");
61
+ return;
62
+ }
63
+
64
+ for (const [index, item] of result.findings.entries()) {
65
+ console.log("");
66
+ console.log(`${index + 1}. ${item.area}`);
67
+ console.log(`Znalezisko: ${item.finding}`);
68
+ console.log(`Dowód: ${item.evidence.file}:${item.evidence.line}`);
69
+ console.log(`Fragment: ${item.evidence.text}`);
70
+ console.log(`Ryzyko: ${item.risk}`);
71
+ console.log(`Priorytet: ${item.priority || "brak"}`);
72
+ console.log(`Wysiłek: ${item.effort || "brak"}`);
73
+ if (item.recommendedResource) {
74
+ console.log(`Zasób Akademii: ${item.recommendedResource.title}`);
75
+ console.log(`ID zasobu: ${item.recommendedResource.id}`);
76
+ } else {
77
+ console.log("Zasób Akademii: brak dopasowania");
78
+ }
79
+ console.log(`Pierwsze 15 minut: ${item.action}`);
80
+ console.log(`Test jakości: ${item.qualityTest || "brak"}`);
81
+ }
82
+ }
83
+
84
+ function checklistText(resource) {
85
+ if (Array.isArray(resource.checklist)) return resource.checklist.map((item) => `- ${item}`).join("\n");
86
+ if (Array.isArray(resource.checklistSections)) {
87
+ return resource.checklistSections
88
+ .map((section) => {
89
+ const items = (section.items || []).map((item) => `- ${item}`).join("\n");
90
+ return `${section.title || "Sekcja"}\n${items}`;
91
+ })
92
+ .join("\n\n");
93
+ }
94
+ return "";
95
+ }
96
+
97
+ function truncate(value, maxLength) {
98
+ const text = String(value || "");
99
+ if (text.length <= maxLength) return text;
100
+ return `${text.slice(0, maxLength).trim()}\n\n[ucięte, użyj --full żeby zobaczyć całość]`;
101
+ }
102
+
103
+ function wrap(value) {
104
+ return String(value || "").trim();
105
+ }
package/src/report.js ADDED
@@ -0,0 +1,60 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export async function writeReport(result, options = {}) {
5
+ const outputPath = path.resolve(process.cwd(), options.out || "akademia-report.md");
6
+ await mkdir(path.dirname(outputPath), { recursive: true });
7
+ await writeFile(outputPath, renderReportMarkdown(result), "utf8");
8
+ return outputPath;
9
+ }
10
+
11
+ export function renderReportMarkdown(result) {
12
+ const lines = [
13
+ "# Raport Akademii",
14
+ "",
15
+ `Projekt: ${result.root}`,
16
+ `Przeczytane pliki: ${result.scannedFiles}`,
17
+ `Znaleziska: ${result.findings.length}`,
18
+ ""
19
+ ];
20
+
21
+ if (!result.findings.length) {
22
+ lines.push("Brak znalezisk z dowodem w plikach.");
23
+ lines.push("");
24
+ return lines.join("\n");
25
+ }
26
+
27
+ result.findings.forEach((item, index) => {
28
+ lines.push(`## ${index + 1}. ${item.area}`);
29
+ lines.push("");
30
+ lines.push(`Znalezisko: ${item.finding}`);
31
+ lines.push("");
32
+ lines.push(`Dowód: ${item.evidence.file}:${item.evidence.line}`);
33
+ lines.push("");
34
+ lines.push("Fragment:");
35
+ lines.push("");
36
+ lines.push("```text");
37
+ lines.push(item.evidence.text);
38
+ lines.push("```");
39
+ lines.push("");
40
+ lines.push(`Ryzyko biznesowe: ${item.risk}`);
41
+ lines.push("");
42
+ lines.push(`Priorytet: ${item.priority || "brak"}`);
43
+ lines.push(`Wysiłek: ${item.effort || "brak"}`);
44
+ lines.push("");
45
+ if (item.recommendedResource) {
46
+ lines.push(`Rekomendowany zasób Akademii: ${item.recommendedResource.title}`);
47
+ lines.push(`ID zasobu: ${item.recommendedResource.id}`);
48
+ lines.push(`Typ zasobu: ${item.recommendedResource.type}`);
49
+ lines.push(`URL: https://akademia.pl${item.recommendedResource.url || ""}`);
50
+ } else {
51
+ lines.push("Rekomendowany zasób Akademii: brak dopasowania");
52
+ }
53
+ lines.push("");
54
+ lines.push(`Pierwsze 15 minut pracy: ${item.action}`);
55
+ lines.push(`Test jakości: ${item.qualityTest || "brak"}`);
56
+ lines.push("");
57
+ });
58
+
59
+ return lines.join("\n");
60
+ }
package/src/scan.js ADDED
@@ -0,0 +1,276 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { searchResources } from "./search.js";
4
+
5
+ const DEFAULT_MAX_FILES = 250;
6
+ const MAX_FILE_BYTES = 120000;
7
+ const MAX_FINDINGS_PER_AREA = 1;
8
+
9
+ const IGNORED_DIRS = new Set([
10
+ ".cache",
11
+ ".git",
12
+ ".next",
13
+ ".serverless",
14
+ ".turbo",
15
+ "backups",
16
+ "build",
17
+ "coverage",
18
+ "dist",
19
+ "golden",
20
+ "node_modules",
21
+ "tmp",
22
+ "TMP",
23
+ "vendor"
24
+ ]);
25
+
26
+ const IGNORED_FILES = new Set([
27
+ ".DS_Store",
28
+ ".env",
29
+ ".env.local",
30
+ ".env.production",
31
+ "akademia-report.md",
32
+ "CHANGELOG.md",
33
+ "package-lock.json",
34
+ "pnpm-lock.yaml",
35
+ "yarn.lock"
36
+ ]);
37
+
38
+ const TEXT_EXTENSIONS = new Set([
39
+ ".css",
40
+ ".html",
41
+ ".js",
42
+ ".json",
43
+ ".jsx",
44
+ ".md",
45
+ ".mdx",
46
+ ".mjs",
47
+ ".ts",
48
+ ".tsx",
49
+ ".txt",
50
+ ".yaml",
51
+ ".yml"
52
+ ]);
53
+
54
+ const AREAS = [
55
+ {
56
+ id: "start-projektu",
57
+ area: "Start projektu",
58
+ query: "wywiad przed startem projektu brief decyzja",
59
+ preferredResourceId: "wywiad-przed-startem-projektu",
60
+ pattern: /\b(mvp|roadmap|sprint|start projektu|scope|zakres|todo|tbd|wip|decyzja)\b/i,
61
+ finding: "Projekt ma miejsce, które warto doprecyzować przed dalszą pracą.",
62
+ risk: "Bez mocnego briefu agent może optymalizować zadania zamiast właściwej decyzji biznesowej.",
63
+ action: "Zbierz cel, odbiorcę, miernik sukcesu, zakres poza projektem i warunek stop.",
64
+ priority: "średni",
65
+ effort: "30 do 60 minut",
66
+ qualityTest: "Po poprawce projekt ma cel, właściciela, miernik sukcesu, zakres poza projektem i warunek stop."
67
+ },
68
+ {
69
+ id: "strona-i-konwersja",
70
+ area: "Strona i konwersja",
71
+ query: "landing page lead magnet thank you copywriting strona sprzedażowa",
72
+ preferredResourceId: "lead-magnet-popup-thank-you-page",
73
+ pattern: /\b(landing|hero|cta|lead magnet|newsletter|formularz|zapis|popup|thank you|strona sprzedażowa|checkout)\b/i,
74
+ finding: "W projekcie jest powierzchnia konwersji, którą warto sprawdzić checklistą.",
75
+ risk: "Strona może zbierać ruch, ale gubić jasną obietnicę, CTA albo następny krok użytkownika.",
76
+ action: "Sprawdź nagłówek, obietnicę, formularz, CTA, dowody społeczne i stronę po zapisie.",
77
+ priority: "wysoki",
78
+ effort: "45 do 90 minut",
79
+ qualityTest: "Po poprawce użytkownik rozumie obietnicę, widzi jedno główne CTA i wie, co stanie się po kliknięciu."
80
+ },
81
+ {
82
+ id: "oferta-i-sprzedaz",
83
+ area: "Oferta i sprzedaż",
84
+ query: "copywriting oferta pricing sprzedaż negocjacje rezultaty zamiast funkcji",
85
+ preferredResourceId: "oferta-b2b-do-100k-pln",
86
+ pattern: /\b(oferta|cennik|pricing|sprzedaż|sprzedaz|rabat|usługa|usluga|kup|demo)\b/i,
87
+ finding: "Projekt dotyka oferty albo sprzedaży i warto przejść z funkcji na rezultat.",
88
+ risk: "Komunikacja może opisywać produkt, ale nie pokazywać powodu zakupu i kryterium decyzji.",
89
+ action: "Przepisz kluczowe sekcje na rezultat, obiekcje, dowody, zakres i warunki decyzji.",
90
+ priority: "wysoki",
91
+ effort: "60 do 120 minut",
92
+ qualityTest: "Po poprawce oferta ma wynik dla klienta, cenę albo zakres, wyłączenia, dowody i jasny kolejny krok."
93
+ },
94
+ {
95
+ id: "marketing-i-research",
96
+ area: "Marketing i research",
97
+ query: "strategia marketingowa deep research persona kampania content",
98
+ preferredResourceId: "badania-psychograficzne-deep-research",
99
+ pattern: /\b(persona|icp|kampania|content|seo|linkedin|meta ads|research|rynek|segment|grupa docelowa)\b/i,
100
+ finding: "W projekcie jest marketing albo research, który może wymagać mocniejszego kontekstu rynku.",
101
+ risk: "Agent może pisać treści bez języka klienta, obiekcji, alternatyw i realnych triggerów zakupu.",
102
+ action: "Zbierz język rynku, pytania, obiekcje, konkurencję, kanały i hipotezy do testu.",
103
+ priority: "średni",
104
+ effort: "60 do 120 minut",
105
+ qualityTest: "Po poprawce materiał zawiera język klienta, obiekcje, alternatywy, trigger zakupu i hipotezę do sprawdzenia."
106
+ },
107
+ {
108
+ id: "proces-i-crm",
109
+ area: "Proces i CRM",
110
+ query: "workflow proces guard system follow up onboarding",
111
+ preferredResourceId: "wnioski-z-dyskusji-do-poprawy-systemu",
112
+ pattern: /\b(follow up|pipeline|deal|onboarding|renewal|mailing)\b|\bcrm\b.*\b(proces|pipeline|follow|sprzedaż|sprzedaz|segment|mailing)\b|\b(proces|pipeline|follow|sprzedaż|sprzedaz|segment|mailing)\b.*\bcrm\b/i,
113
+ finding: "Projekt ma proces sprzedaży, obsługi albo CRM, który warto sprawdzić operacyjnie.",
114
+ risk: "Bez procesu i statusów łatwo zgubić odpowiedzialność, follow up albo sygnał do decyzji.",
115
+ action: "Nazwij etapy, właściciela, trigger, następny krok, status i minimalny log decyzji.",
116
+ priority: "średni",
117
+ effort: "45 do 90 minut",
118
+ qualityTest: "Po poprawce proces ma etapy, właściciela, trigger wejścia, następny krok i status końcowy."
119
+ },
120
+ {
121
+ id: "bezpieczenstwo-i-api",
122
+ area: "Bezpieczeństwo i API",
123
+ query: "api bezpieczeństwo code review",
124
+ preferredResourceId: "agent-team-code-review",
125
+ pattern: /\b(api|token|secret|auth|waf|rate limit|rodo|privacy|dane|hasło|hasla|webhook)\b/i,
126
+ finding: "Projekt dotyka API, danych albo dostępu i wymaga prostego przeglądu bezpieczeństwa.",
127
+ risk: "Niewidoczne założenia o dostępie, limitach i danych mogą później stać się incydentem.",
128
+ action: "Sprawdź dane wejściowe, limity, sekrety, logi, retencję, auth i komunikat dla użytkownika.",
129
+ priority: "wysoki",
130
+ effort: "60 do 120 minut",
131
+ qualityTest: "Po poprawce wiadomo, jakie dane wchodzą do systemu, gdzie są limity, gdzie jest auth i czego nie logujemy."
132
+ }
133
+ ];
134
+
135
+ export async function scanProject(target, resourceIndex, options = {}) {
136
+ const root = path.resolve(process.cwd(), target || ".");
137
+ const maxFiles = Number(options.maxFiles || DEFAULT_MAX_FILES);
138
+ const files = await collectFiles(root, { maxFiles });
139
+ const findings = [];
140
+
141
+ for (const file of files) {
142
+ const lines = await readInterestingLines(file, root);
143
+ for (const line of lines) {
144
+ for (const area of AREAS) {
145
+ if (!area.pattern.test(line.text)) continue;
146
+ if (countArea(findings, area.id) >= MAX_FINDINGS_PER_AREA) continue;
147
+ findings.push(buildFinding(area, line, resourceIndex));
148
+ }
149
+ }
150
+ }
151
+
152
+ return {
153
+ root,
154
+ scannedFiles: files.length,
155
+ findings: dedupeFindings(findings).slice(0, Number(options.limit || 12))
156
+ };
157
+ }
158
+
159
+ async function collectFiles(root, options) {
160
+ const files = [];
161
+
162
+ async function walk(current) {
163
+ if (files.length >= options.maxFiles) return;
164
+ const entries = await readdir(current, { withFileTypes: true });
165
+ for (const entry of entries) {
166
+ if (files.length >= options.maxFiles) return;
167
+ const fullPath = path.join(current, entry.name);
168
+ if (entry.isDirectory()) {
169
+ if (!shouldSkipDir(entry.name)) await walk(fullPath);
170
+ continue;
171
+ }
172
+ if (!entry.isFile() || shouldSkipFile(entry.name)) continue;
173
+ const info = await stat(fullPath);
174
+ if (info.size > MAX_FILE_BYTES || !isTextFile(fullPath)) continue;
175
+ files.push(fullPath);
176
+ }
177
+ }
178
+
179
+ await walk(root);
180
+ return files;
181
+ }
182
+
183
+ function shouldSkipDir(name) {
184
+ return IGNORED_DIRS.has(name) || name.startsWith(".");
185
+ }
186
+
187
+ function shouldSkipFile(name) {
188
+ if (IGNORED_FILES.has(name)) return true;
189
+ if (name.endsWith(".pem") || name.endsWith(".key") || name.endsWith(".crt")) return true;
190
+ return false;
191
+ }
192
+
193
+ function isTextFile(filePath) {
194
+ return TEXT_EXTENSIONS.has(path.extname(filePath));
195
+ }
196
+
197
+ async function readInterestingLines(filePath, root) {
198
+ const text = await readFile(filePath, "utf8");
199
+ return text
200
+ .split(/\r?\n/)
201
+ .map((textLine, index) => ({
202
+ file: path.relative(root, filePath) || path.basename(filePath),
203
+ line: index + 1,
204
+ text: cleanLine(textLine)
205
+ }))
206
+ .filter((line) => line.text.length >= 8 && line.text.length <= 220)
207
+ .filter((line) => isUsefulEvidenceLine(line.text));
208
+ }
209
+
210
+ function buildFinding(area, line, resourceIndex) {
211
+ const resource = findPreferredResource(resourceIndex, area) || searchResources(resourceIndex, area.query, { limit: 1 })[0];
212
+ return {
213
+ areaId: area.id,
214
+ area: area.area,
215
+ finding: area.finding,
216
+ evidence: line,
217
+ risk: area.risk,
218
+ priority: area.priority,
219
+ effort: area.effort,
220
+ recommendedResource: resource ? {
221
+ id: resource.id,
222
+ title: resource.title,
223
+ type: resource.type,
224
+ url: resource.url
225
+ } : null,
226
+ action: area.action,
227
+ qualityTest: area.qualityTest
228
+ };
229
+ }
230
+
231
+ function findPreferredResource(resourceIndex, area) {
232
+ if (!area.preferredResourceId) return null;
233
+ return resourceIndex.find((item) => item.id === area.preferredResourceId) || null;
234
+ }
235
+
236
+ function countArea(findings, areaId) {
237
+ return findings.filter((item) => item.areaId === areaId).length;
238
+ }
239
+
240
+ function dedupeFindings(findings) {
241
+ const seen = new Set();
242
+ const result = [];
243
+ for (const finding of findings) {
244
+ const key = `${finding.evidence.file}:${finding.evidence.line}`;
245
+ if (seen.has(key)) continue;
246
+ seen.add(key);
247
+ result.push(finding);
248
+ }
249
+ return result;
250
+ }
251
+
252
+ function cleanLine(value) {
253
+ return String(value || "")
254
+ .replace(/\s+/g, " ")
255
+ .replace(/[ \t]+$/g, "")
256
+ .trim();
257
+ }
258
+
259
+ function isUsefulEvidenceLine(value) {
260
+ if (/^\s*(area|id|query|pattern|finding|risk|action|priority|effort|qualityTest|preferredResourceId):/i.test(value)) return false;
261
+ if (/^"(id|slug|title|type|url|applicationSlug|applicationSlugs|industrySlug|industrySlugs|canonicalCluster|intent|keywords|tags)":/i.test(value)) return false;
262
+ if (/^"(draft_not_contains|finding_contains|finding_not_contains|expected|actual)":/i.test(value)) return false;
263
+ if (/^<meta\b/i.test(value) || /<meta\s/i.test(value)) return false;
264
+ if (/\bclass="/i.test(value) && !/>[^<]{12,}</.test(value)) return false;
265
+ if (/^\.[a-z0-9_-]+\s*\{/i.test(value)) return false;
266
+ if (/^content-type:/i.test(value)) return false;
267
+ if (/^[a-z0-9_.-]+==\d/i.test(value)) return false;
268
+ if (/\b(cookie|pliki cookie)\b/i.test(value)) return false;
269
+ if (/\bFIXED\b/i.test(value) || /^\s*[-*]\s*\[x\]/i.test(value)) return false;
270
+ if (/\b(tokens|fonts|components|pages\/\*)\b/i.test(value) && /\.(css|js|jsx|ts|tsx|html|md|json)\b/i.test(value)) return false;
271
+ if (/\bCSS\s*(→|->)\b/i.test(value) || /\btokens,\s*fonts,\s*core,\s*components\b/i.test(value)) return false;
272
+ if (/^\|[^|]+\|[^|]+\|$/.test(value) && value.length < 90) return false;
273
+ if (value.includes("\\b(") || value.includes("RegExp")) return false;
274
+ if (value.startsWith("const ") || value.startsWith("let ") || value.startsWith("function ")) return false;
275
+ return true;
276
+ }
package/src/search.js ADDED
@@ -0,0 +1,59 @@
1
+ const TYPE_WEIGHT = 8;
2
+ const TITLE_WEIGHT = 12;
3
+ const KEYWORD_WEIGHT = 10;
4
+ const TAG_WEIGHT = 6;
5
+ const TEXT_WEIGHT = 1;
6
+
7
+ export function searchResources(index, query, options = {}) {
8
+ const terms = tokenize(query);
9
+ const type = options.type ? String(options.type).toLowerCase() : "";
10
+ const limit = Number(options.limit || 10);
11
+ if (!terms.length) return [];
12
+
13
+ return index
14
+ .filter((item) => !type || item.type === type)
15
+ .map((item) => ({ item, score: scoreItem(item, terms) }))
16
+ .filter((result) => result.score > 0)
17
+ .sort((a, b) => b.score - a.score || a.item.title.localeCompare(b.item.title, "pl"))
18
+ .slice(0, limit)
19
+ .map((result) => ({
20
+ ...result.item,
21
+ score: result.score
22
+ }));
23
+ }
24
+
25
+ export function tokenize(value) {
26
+ return String(value || "")
27
+ .toLowerCase()
28
+ .normalize("NFKD")
29
+ .replace(/[\u0300-\u036f]/g, "")
30
+ .split(/[^a-z0-9]+/)
31
+ .map((term) => term.trim())
32
+ .filter((term) => term.length >= 2);
33
+ }
34
+
35
+ function scoreItem(item, terms) {
36
+ const title = normalize(item.title);
37
+ const type = normalize(item.type);
38
+ const keywords = normalize((item.keywords || []).join(" "));
39
+ const tags = normalize((item.tags || []).join(" "));
40
+ const text = normalize(item.text || "");
41
+ let score = 0;
42
+
43
+ for (const term of terms) {
44
+ if (type === term) score += TYPE_WEIGHT;
45
+ if (title.includes(term)) score += TITLE_WEIGHT;
46
+ if (keywords.includes(term)) score += KEYWORD_WEIGHT;
47
+ if (tags.includes(term)) score += TAG_WEIGHT;
48
+ if (text.includes(term)) score += TEXT_WEIGHT;
49
+ }
50
+
51
+ return score;
52
+ }
53
+
54
+ function normalize(value) {
55
+ return String(value || "")
56
+ .toLowerCase()
57
+ .normalize("NFKD")
58
+ .replace(/[\u0300-\u036f]/g, "");
59
+ }
package/index.js DELETED
@@ -1,3 +0,0 @@
1
- module.exports = function () {
2
- return "Akademia – start produktu w kwietniu 2026. Więcej na akademia.pl";
3
- };