akademia 0.0.1 → 0.1.1

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,42 @@
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
+ Prosty dostęp do publicznych promptów i checklist Akademia.pl z terminala.
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
10
+ ```
11
+
12
+ Po instalacji wpisz:
13
+
14
+ ```bash
15
+ akademia pomoc
16
+ ```
17
+
18
+ ## Podstawowe użycie
19
+
20
+ ```bash
21
+ akademia szukaj "landing page"
22
+ akademia pokaz wywiad-przed-startem-projektu
23
+ akademia status
24
+ ```
25
+
26
+ ## Prywatność
27
+
28
+ CLI V1 pobiera tylko publiczne pliki JSON. Nie wysyła kodu projektu, plików, maili ani danych klientów do Akademii.
29
+
30
+ ## API
31
+
32
+ Domyślnie CLI łączy się z:
33
+
34
+ ```bash
35
+ https://akademia.pl
36
+ ```
37
+
38
+ Technicznie możesz podmienić endpoint:
39
+
40
+ ```bash
41
+ AKADEMIA_API_BASE=http://127.0.0.1:8898 akademia status
9
42
  ```
@@ -0,0 +1,218 @@
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.1";
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 (flags.help || isHelpCommand(command)) {
20
+ printHelp();
21
+ return;
22
+ }
23
+
24
+ if (!command) {
25
+ await printStart(client);
26
+ return;
27
+ }
28
+
29
+ const action = normalizeCommand(command);
30
+
31
+ if (action === "doctor") {
32
+ const catalog = await client.catalog();
33
+ printDoctor(catalog, client.baseUrl);
34
+ return;
35
+ }
36
+
37
+ if (action === "search") {
38
+ const query = args.join(" ").trim();
39
+ if (!query) throw new Error("Podaj frazę, np. akademia szukaj \"landing page\".");
40
+ const index = await client.searchIndex();
41
+ const results = searchResources(index, query, {
42
+ limit: flags.limit || 10,
43
+ type: flags.type
44
+ });
45
+ printSearchResults(results);
46
+ return;
47
+ }
48
+
49
+ if (action === "show") {
50
+ const id = args[0];
51
+ if (!id) throw new Error("Podaj ID zasobu, np. akademia pokaz wywiad-przed-startem-projektu.");
52
+ const resource = await client.resource(id);
53
+ printResource(resource, { full: flags.full });
54
+ return;
55
+ }
56
+
57
+ if (action === "scan") {
58
+ const target = args[0] || ".";
59
+ const index = await client.searchIndex();
60
+ const result = await scanProject(target, index, {
61
+ limit: flags.limit || 12,
62
+ maxFiles: flags.maxFiles || 250
63
+ });
64
+ if (flags.json) {
65
+ console.log(JSON.stringify(result, null, 2));
66
+ return;
67
+ }
68
+ printScanResults(result);
69
+ return;
70
+ }
71
+
72
+ if (action === "report") {
73
+ const target = args[0] || ".";
74
+ const index = await client.searchIndex();
75
+ const result = await scanProject(target, index, {
76
+ limit: flags.limit || 12,
77
+ maxFiles: flags.maxFiles || 250
78
+ });
79
+ const outputPath = await writeReport(result, { out: flags.out });
80
+ console.log(`Raport zapisany: ${outputPath}`);
81
+ console.log(`Znaleziska: ${result.findings.length}`);
82
+ return;
83
+ }
84
+
85
+ throw new Error(`Nie znam tej komendy. Wpisz: akademia pomoc`);
86
+ }
87
+
88
+ function parseArgs(argv) {
89
+ const args = [];
90
+ const flags = {};
91
+ let command = "";
92
+
93
+ for (let index = 0; index < argv.length; index += 1) {
94
+ const value = argv[index];
95
+ if (!command && !value.startsWith("-")) {
96
+ command = value;
97
+ continue;
98
+ }
99
+ if (value === "--help" || value === "-h") {
100
+ flags.help = true;
101
+ continue;
102
+ }
103
+ if (value === "--version" || value === "-v") {
104
+ flags.version = true;
105
+ continue;
106
+ }
107
+ if (value === "--full") {
108
+ flags.full = true;
109
+ continue;
110
+ }
111
+ if (value === "--json") {
112
+ flags.json = true;
113
+ continue;
114
+ }
115
+ if (value === "--type") {
116
+ flags.type = argv[index + 1] || "";
117
+ index += 1;
118
+ continue;
119
+ }
120
+ if (value === "--limit") {
121
+ flags.limit = Number(argv[index + 1] || 10);
122
+ index += 1;
123
+ continue;
124
+ }
125
+ if (value === "--max-files") {
126
+ flags.maxFiles = Number(argv[index + 1] || 250);
127
+ index += 1;
128
+ continue;
129
+ }
130
+ if (value === "--out") {
131
+ flags.out = argv[index + 1] || "";
132
+ index += 1;
133
+ continue;
134
+ }
135
+ if (value === "--base-url") {
136
+ flags.baseUrl = argv[index + 1] || "";
137
+ index += 1;
138
+ continue;
139
+ }
140
+ args.push(value);
141
+ }
142
+
143
+ return { command, args, flags };
144
+ }
145
+
146
+ function normalizeCommand(command) {
147
+ const commands = new Map([
148
+ ["doctor", "doctor"],
149
+ ["status", "doctor"],
150
+ ["search", "search"],
151
+ ["szukaj", "search"],
152
+ ["show", "show"],
153
+ ["pokaz", "show"],
154
+ ["pokaż", "show"],
155
+ ["scan", "scan"],
156
+ ["report", "report"]
157
+ ]);
158
+
159
+ return commands.get(command) || command;
160
+ }
161
+
162
+ function isHelpCommand(command) {
163
+ return command === "help" || command === "pomoc";
164
+ }
165
+
166
+ async function printStart(client) {
167
+ try {
168
+ const catalog = await client.catalog();
169
+ const resources = catalog?.resources || [];
170
+ const total = resources.length;
171
+ const prompts = resources.filter((item) => item.type === "prompt").length;
172
+ const checklists = resources.filter((item) => item.type === "checklist").length;
173
+
174
+ console.log(`Akademia.pl CLI ${VERSION}
175
+
176
+ Łączy terminal z publicznymi promptami i checklistami Akademii.
177
+
178
+ Status: działa
179
+ Zasoby: ${total} publicznych materiałów, ${prompts} promptów, ${checklists} checklist
180
+
181
+ Następny krok:
182
+ akademia pomoc
183
+ `);
184
+ return;
185
+ } catch (error) {
186
+ console.log(`Akademia.pl CLI ${VERSION}
187
+
188
+ CLI jest zainstalowane, ale nie udało się połączyć z Akademia.pl.
189
+
190
+ Następny krok:
191
+ akademia pomoc
192
+
193
+ Szczegół techniczny: ${error.message}
194
+ `);
195
+ }
196
+ }
197
+
198
+ function printHelp() {
199
+ console.log(`Akademia.pl CLI ${VERSION}
200
+
201
+ Najprościej:
202
+ akademia
203
+
204
+ Szukaj promptu albo checklisty:
205
+ akademia szukaj "landing page"
206
+
207
+ Pokaż konkretny materiał:
208
+ akademia pokaz wywiad-przed-startem-projektu
209
+
210
+ Sprawdź połączenie:
211
+ akademia status
212
+ `);
213
+ }
214
+
215
+ main(process.argv.slice(2)).catch((error) => {
216
+ console.error(`Błąd: ${error.message}`);
217
+ process.exitCode = 1;
218
+ });
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.1",
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,106 @@
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
+ const connection = resources.length ? "działa" : "brak publicznych zasobów";
8
+
9
+ console.log("Akademia.pl CLI");
10
+ console.log(`Status: ${connection}`);
11
+ console.log(`API: ${baseUrl}`);
12
+ console.log(`Zasoby: ${resources.length} publicznych materiałów`);
13
+ console.log(`Prompty: ${prompts}`);
14
+ console.log(`Checklisty: ${checklists}`);
15
+ console.log(`Pliki z Twojego komputera: ${security.acceptsProjectFiles ? "wysyłane" : "nie są wysyłane"}`);
16
+ }
17
+
18
+ export function printSearchResults(results) {
19
+ if (!results.length) {
20
+ console.log("Brak wyników.");
21
+ return;
22
+ }
23
+
24
+ for (const [index, item] of results.entries()) {
25
+ console.log(`${index + 1}. ${item.title}`);
26
+ console.log(` id: ${item.id}`);
27
+ console.log(` typ: ${item.type}, zastosowanie: ${item.application || item.applicationSlug || "brak"}`);
28
+ console.log(` opis: ${item.description || item.seoDescription || ""}`);
29
+ console.log(` score: ${item.score}`);
30
+ }
31
+ }
32
+
33
+ export function printResource(resource, options = {}) {
34
+ console.log(`${resource.title}`);
35
+ console.log(`id: ${resource.id}`);
36
+ console.log(`typ: ${resource.type}`);
37
+ console.log(`url: https://akademia.pl${resource.url || ""}`);
38
+ if (resource.description) console.log(`opis: ${resource.description}`);
39
+ if (resource.usage) {
40
+ console.log("");
41
+ console.log("Jak użyć:");
42
+ console.log(wrap(resource.usage));
43
+ }
44
+
45
+ const content = resource.prompt || checklistText(resource);
46
+ if (content) {
47
+ console.log("");
48
+ console.log(resource.prompt ? "Prompt:" : "Checklista:");
49
+ console.log(options.full ? content : truncate(content, 1800));
50
+ }
51
+ }
52
+
53
+ export function printScanResults(result) {
54
+ console.log("Skan Akademii");
55
+ console.log(`Projekt: ${result.root}`);
56
+ console.log(`Przeczytane pliki: ${result.scannedFiles}`);
57
+ console.log(`Znaleziska: ${result.findings.length}`);
58
+
59
+ if (!result.findings.length) {
60
+ console.log("");
61
+ console.log("Brak znalezisk z dowodem w plikach.");
62
+ return;
63
+ }
64
+
65
+ for (const [index, item] of result.findings.entries()) {
66
+ console.log("");
67
+ console.log(`${index + 1}. ${item.area}`);
68
+ console.log(`Znalezisko: ${item.finding}`);
69
+ console.log(`Dowód: ${item.evidence.file}:${item.evidence.line}`);
70
+ console.log(`Fragment: ${item.evidence.text}`);
71
+ console.log(`Ryzyko: ${item.risk}`);
72
+ console.log(`Priorytet: ${item.priority || "brak"}`);
73
+ console.log(`Wysiłek: ${item.effort || "brak"}`);
74
+ if (item.recommendedResource) {
75
+ console.log(`Zasób Akademii: ${item.recommendedResource.title}`);
76
+ console.log(`ID zasobu: ${item.recommendedResource.id}`);
77
+ } else {
78
+ console.log("Zasób Akademii: brak dopasowania");
79
+ }
80
+ console.log(`Pierwsze 15 minut: ${item.action}`);
81
+ console.log(`Test jakości: ${item.qualityTest || "brak"}`);
82
+ }
83
+ }
84
+
85
+ function checklistText(resource) {
86
+ if (Array.isArray(resource.checklist)) return resource.checklist.map((item) => `- ${item}`).join("\n");
87
+ if (Array.isArray(resource.checklistSections)) {
88
+ return resource.checklistSections
89
+ .map((section) => {
90
+ const items = (section.items || []).map((item) => `- ${item}`).join("\n");
91
+ return `${section.title || "Sekcja"}\n${items}`;
92
+ })
93
+ .join("\n\n");
94
+ }
95
+ return "";
96
+ }
97
+
98
+ function truncate(value, maxLength) {
99
+ const text = String(value || "");
100
+ if (text.length <= maxLength) return text;
101
+ return `${text.slice(0, maxLength).trim()}\n\n[ucięte, użyj --full żeby zobaczyć całość]`;
102
+ }
103
+
104
+ function wrap(value) {
105
+ return String(value || "").trim();
106
+ }
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
- };