@abelionorg/core 3.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 +32 -0
- package/src/Arsip.ts +513 -0
- package/src/AuditLog.ts +55 -0
- package/src/Brankas.ts +322 -0
- package/src/CrashReporter.ts +45 -0
- package/src/Gudang.ts +63 -0
- package/src/GunakanTunda.ts +19 -0
- package/src/Indera.ts +61 -0
- package/src/Integritas.ts +36 -0
- package/src/Kalender.ts +59 -0
- package/src/KataSandi.ts +27 -0
- package/src/Laras.ts +179 -0
- package/src/Layanan.ts +45 -0
- package/src/Linguis.ts +73 -0
- package/src/Penjaga.ts +70 -0
- package/src/Penyaring.ts +10 -0
- package/src/Penyelaras.ts +40 -0
- package/src/Pujangga.ts +66 -0
- package/src/Pundi.ts +69 -0
- package/src/Rumus.ts +55 -0
- package/src/Sentinel.ts +129 -0
- package/src/Waktu.ts +18 -0
- package/src/ai/GeminiProvider.ts +36 -0
- package/src/ai/LocalProvider.ts +42 -0
- package/src/ai/PenyaringRahasia.ts +31 -0
- package/src/ai/types.ts +12 -0
- package/src/index.ts +23 -0
- package/src/scripts/bench-all-notes.ts +23 -0
- package/src/scripts/inject-perf-data.ts +51 -0
- package/src/storage/BrowserAdapter.ts +68 -0
- package/src/storage/FileAdapter.shim.ts +5 -0
- package/src/storage/FileAdapter.ts +121 -0
- package/src/storage/types.ts +31 -0
package/src/Laras.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { Hasil } from './Brankas';
|
|
2
|
+
|
|
3
|
+
export type KonteksLaras = 'saku' | 'pelataran';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Laras: Context and Path Resolver.
|
|
7
|
+
* Decoupled from Node.js top-level imports to support browser bundles.
|
|
8
|
+
*/
|
|
9
|
+
export class Laras {
|
|
10
|
+
private static readonly SAKU_FILE = 'saku.json';
|
|
11
|
+
private static readonly PELATARAN_DIR = '.lembaran';
|
|
12
|
+
private static readonly PELATARAN_FILE = 'pelataran.json';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolves the absolute path for the given context.
|
|
16
|
+
* Works only in Node.js environment.
|
|
17
|
+
*/
|
|
18
|
+
static async temukanJalur(konteks: KonteksLaras): Promise<string> {
|
|
19
|
+
if (typeof window !== 'undefined') return '';
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Dynamic imports for ESM/Node compatibility
|
|
23
|
+
const path = await import('path');
|
|
24
|
+
const os = await import('os');
|
|
25
|
+
const fs = await import('fs');
|
|
26
|
+
|
|
27
|
+
const SAKU_DIR = path.join(os.homedir(), '.lembaran');
|
|
28
|
+
|
|
29
|
+
let jalur: string;
|
|
30
|
+
if (konteks === 'saku') {
|
|
31
|
+
if (!fs.existsSync(SAKU_DIR)) {
|
|
32
|
+
fs.mkdirSync(SAKU_DIR, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
jalur = path.join(SAKU_DIR, this.SAKU_FILE);
|
|
35
|
+
} else {
|
|
36
|
+
const root = (await this.temukanAkarProyek()) || process.cwd();
|
|
37
|
+
const localDir = path.join(root, this.PELATARAN_DIR);
|
|
38
|
+
if (!fs.existsSync(localDir)) {
|
|
39
|
+
fs.mkdirSync(localDir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
jalur = path.join(localDir, this.PELATARAN_FILE);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (process.env.DEBUG === 'true') {
|
|
45
|
+
console.log(`[LARAS] Jalur ${konteks}: ${jalur}`);
|
|
46
|
+
}
|
|
47
|
+
return jalur;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error('[LARAS] Gagal menemukan jalur:', err);
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Detects the project root. Node.js only.
|
|
56
|
+
*/
|
|
57
|
+
private static async temukanAkarProyek(dir: string = (typeof process !== 'undefined' ? process.cwd() : '')): Promise<string | null> {
|
|
58
|
+
if (typeof window !== 'undefined') return null;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const path = await import('path');
|
|
62
|
+
const fs = await import('fs');
|
|
63
|
+
|
|
64
|
+
const check = (curr: string): string | null => {
|
|
65
|
+
if (fs.existsSync(path.join(curr, '.git')) || fs.existsSync(path.join(curr, 'package.json'))) {
|
|
66
|
+
return curr;
|
|
67
|
+
}
|
|
68
|
+
const parent = path.dirname(curr);
|
|
69
|
+
if (parent === curr) return null;
|
|
70
|
+
return check(parent);
|
|
71
|
+
};
|
|
72
|
+
return check(dir);
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Smart context detection. Node.js only.
|
|
80
|
+
*/
|
|
81
|
+
static async deteksiKonteksOtomatis(): Promise<KonteksLaras> {
|
|
82
|
+
if (typeof window !== 'undefined') return 'saku';
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const path = await import('path');
|
|
86
|
+
const fs = await import('fs');
|
|
87
|
+
|
|
88
|
+
const root = await this.temukanAkarProyek();
|
|
89
|
+
if (root && fs.existsSync(path.join(root, this.PELATARAN_DIR, this.PELATARAN_FILE))) {
|
|
90
|
+
return 'pelataran';
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// Default to saku on error
|
|
94
|
+
}
|
|
95
|
+
return 'saku';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Reads local .env. Node.js only.
|
|
100
|
+
*/
|
|
101
|
+
static async bacaEnv(): Promise<Record<string, string>> {
|
|
102
|
+
if (typeof window !== 'undefined') return {};
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const path = await import('path');
|
|
106
|
+
const fs = await import('fs');
|
|
107
|
+
|
|
108
|
+
const root = await this.temukanAkarProyek();
|
|
109
|
+
if (!root) return {};
|
|
110
|
+
const envPath = path.join(root, '.env');
|
|
111
|
+
if (!fs.existsSync(envPath)) return {};
|
|
112
|
+
|
|
113
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
114
|
+
const lines = content.split('\n');
|
|
115
|
+
const env: Record<string, string> = {};
|
|
116
|
+
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
const trimmed = line.trim();
|
|
119
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
120
|
+
const match = trimmed.match(/^([^=]+)=(.*)$/);
|
|
121
|
+
if (match) {
|
|
122
|
+
const key = match[1].trim();
|
|
123
|
+
let val = match[2].trim();
|
|
124
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
125
|
+
val = val.substring(1, val.length - 1);
|
|
126
|
+
}
|
|
127
|
+
env[key] = val;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return env;
|
|
131
|
+
} catch {
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Writes or updates a local .env variable. Node.js only.
|
|
138
|
+
*/
|
|
139
|
+
static async simpanEnv(key: string, value: string): Promise<Hasil<boolean>> {
|
|
140
|
+
if (typeof window !== 'undefined') return { data: null, error: new Error('Bukan lingkungan Node.js') };
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const path = await import('path');
|
|
144
|
+
const fs = await import('fs');
|
|
145
|
+
|
|
146
|
+
const root = await this.temukanAkarProyek();
|
|
147
|
+
if (!root) return { data: null, error: new Error('Akar proyek tidak ditemukan.') };
|
|
148
|
+
const envPath = path.join(root, '.env');
|
|
149
|
+
|
|
150
|
+
let content = '';
|
|
151
|
+
if (fs.existsSync(envPath)) {
|
|
152
|
+
content = fs.readFileSync(envPath, 'utf8');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const lines = content.split('\n');
|
|
156
|
+
let found = false;
|
|
157
|
+
const newLines = lines.map(line => {
|
|
158
|
+
const trimmed = line.trim();
|
|
159
|
+
if (trimmed.startsWith(`${key}=`)) {
|
|
160
|
+
found = true;
|
|
161
|
+
return `${key}=${value}`;
|
|
162
|
+
}
|
|
163
|
+
return line;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
if (!found) {
|
|
167
|
+
if (content.length > 0 && !content.endsWith('\n')) {
|
|
168
|
+
newLines.push('');
|
|
169
|
+
}
|
|
170
|
+
newLines.push(`${key}=${value}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fs.writeFileSync(envPath, newLines.join('\n'), 'utf8');
|
|
174
|
+
return { data: true, error: null };
|
|
175
|
+
} catch (err) {
|
|
176
|
+
return { data: null, error: err instanceof Error ? err : new Error(String(err)) };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
package/src/Layanan.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layanan Layer: The gateway for future Backend/API communications.
|
|
3
|
+
* Handles synchronization, authentication, and remote backup logic.
|
|
4
|
+
* Currently stubbed for local-only operation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Note, UserProfile } from './Rumus';
|
|
8
|
+
|
|
9
|
+
export const Layanan = {
|
|
10
|
+
/**
|
|
11
|
+
* Push a note to the remote server
|
|
12
|
+
*/
|
|
13
|
+
async syncNote(note: Note): Promise<boolean> {
|
|
14
|
+
// TODO: Implement API call to backend
|
|
15
|
+
console.log('[Layanan] Syncing note to remote...', note.id);
|
|
16
|
+
return true;
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Pull updates from the remote server
|
|
21
|
+
*/
|
|
22
|
+
async fetchUpdates(): Promise<Note[]> {
|
|
23
|
+
// TODO: Implement API call to backend
|
|
24
|
+
return [];
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Authenticate with the remote server for sync
|
|
29
|
+
*/
|
|
30
|
+
async authenticate(_token: string): Promise<UserProfile | null> {
|
|
31
|
+
// TODO: Implement auth logic
|
|
32
|
+
return null;
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Broadcast a synchronization event to other clients
|
|
37
|
+
*/
|
|
38
|
+
broadcastSync(noteId: string) {
|
|
39
|
+
if (typeof window !== 'undefined') {
|
|
40
|
+
const channel = new BroadcastChannel('lembaran-sync');
|
|
41
|
+
channel.postMessage({ type: 'NOTE_SYNCED', id: noteId });
|
|
42
|
+
channel.close();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
package/src/Linguis.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { GoogleGenerativeAI } from '@google/generative-ai';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Linguis: AI translation engine for Lembaran.
|
|
5
|
+
* Designed to maintain the "poetic & professional" vibes.
|
|
6
|
+
*/
|
|
7
|
+
export class Linguis {
|
|
8
|
+
private genAI: GoogleGenerativeAI;
|
|
9
|
+
private model: ReturnType<GoogleGenerativeAI['getGenerativeModel']>;
|
|
10
|
+
|
|
11
|
+
constructor(apiKey: string) {
|
|
12
|
+
this.genAI = new GoogleGenerativeAI(apiKey);
|
|
13
|
+
this.model = this.genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Translates a string (UI element) with context.
|
|
18
|
+
*/
|
|
19
|
+
async terjemahkanTeks(teks: string, targetLang: 'id' | 'en'): Promise<string> {
|
|
20
|
+
const prompt = `
|
|
21
|
+
You are "Linguis", the translation spirit of Lembaran, a high-end personal archive platform.
|
|
22
|
+
Your vibe is: Poetic, professional, minimal, and elegant.
|
|
23
|
+
|
|
24
|
+
Translate the following text into ${targetLang === 'id' ? 'Indonesian' : 'English'}.
|
|
25
|
+
Maintain the tone:
|
|
26
|
+
- If Indonesian: Use "puitis", "elegan", and "profesional".
|
|
27
|
+
- If English: Use "sophisticated", "clean", and "minimalist".
|
|
28
|
+
|
|
29
|
+
Original Text: "${teks}"
|
|
30
|
+
|
|
31
|
+
Return ONLY the translated string. No extra words or quotes.
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const result = await this.model.generateContent(prompt);
|
|
36
|
+
return result.response.text().trim().replace(/^"(.*)"$/, '$1');
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('[Linguis] Translation error:', error);
|
|
39
|
+
return teks; // Fallback to original
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Translates Markdown documentation.
|
|
45
|
+
*/
|
|
46
|
+
async terjemahkanDokumen(markdown: string, targetLang: 'id' | 'en'): Promise<string> {
|
|
47
|
+
const prompt = `
|
|
48
|
+
You are "Linguis", the translation spirit of Lembaran.
|
|
49
|
+
Translate this documentation into ${targetLang === 'id' ? 'Indonesian' : 'English'}.
|
|
50
|
+
|
|
51
|
+
CRITICAL RULES:
|
|
52
|
+
1. Keep ALL Markdown structure (headers, links, code blocks) intact.
|
|
53
|
+
2. Do NOT translate technical terms inside code blocks or specific command names like "lembaran", "ukir", "pantau".
|
|
54
|
+
3. Use a professional, elegant, and poetic tone.
|
|
55
|
+
4. Keep GitHub-style alerts (e.g., > [!NOTE]) exactly as they are.
|
|
56
|
+
|
|
57
|
+
Markdown to translate:
|
|
58
|
+
---
|
|
59
|
+
${markdown}
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
Return ONLY the translated Markdown.
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const result = await this.model.generateContent(prompt);
|
|
67
|
+
return result.response.text().trim();
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('[Linguis] Doc translation error:', error);
|
|
70
|
+
return markdown;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/Penjaga.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { usePundi } from './Pundi';
|
|
5
|
+
import { Brankas } from './Brankas';
|
|
6
|
+
import { audio, haptic } from './Indera';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* usePenjaga: Hook untuk memantau keamanan sesi.
|
|
10
|
+
* Menangani penguncian otomatis berdasarkan waktu idle (sessionTimeout)
|
|
11
|
+
* dan perpindahan tab (visibilitychange).
|
|
12
|
+
*/
|
|
13
|
+
export const usePenjaga = () => {
|
|
14
|
+
const isVaultLocked = usePundi(s => s.isVaultLocked);
|
|
15
|
+
const setVaultLocked = usePundi(s => s.setVaultLocked);
|
|
16
|
+
const settings = usePundi(s => s.settings);
|
|
17
|
+
|
|
18
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
19
|
+
const hideTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
20
|
+
|
|
21
|
+
const gembokBrankas = () => {
|
|
22
|
+
if (!isVaultLocked) {
|
|
23
|
+
Brankas.clearKey();
|
|
24
|
+
setVaultLocked(true);
|
|
25
|
+
audio.lock();
|
|
26
|
+
haptic.medium();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// 1. Session Timeout (Time-based lock while active)
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!isVaultLocked && settings.sessionTimeout) {
|
|
33
|
+
// Bersihkan timeout lama jika ada
|
|
34
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
35
|
+
|
|
36
|
+
// Set timeout baru sesuai pengaturan (menit -> ms)
|
|
37
|
+
timeoutRef.current = setTimeout(() => {
|
|
38
|
+
gembokBrankas();
|
|
39
|
+
}, settings.sessionTimeout * 60 * 1000);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
44
|
+
};
|
|
45
|
+
}, [isVaultLocked, settings.sessionTimeout, setVaultLocked]);
|
|
46
|
+
|
|
47
|
+
// 2. Visibility Change (Auto-lock when tab hidden)
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const handleVisibilityChange = () => {
|
|
50
|
+
if (document.visibilityState === 'hidden' && !isVaultLocked) {
|
|
51
|
+
// Beri toleransi 1 menit sebelum mengunci saat tab disembunyikan
|
|
52
|
+
hideTimeoutRef.current = setTimeout(() => {
|
|
53
|
+
gembokBrankas();
|
|
54
|
+
}, 60000);
|
|
55
|
+
} else if (document.visibilityState === 'visible') {
|
|
56
|
+
// Batalkan penguncian jika user kembali sebelum 1 menit
|
|
57
|
+
if (hideTimeoutRef.current) {
|
|
58
|
+
clearTimeout(hideTimeoutRef.current);
|
|
59
|
+
hideTimeoutRef.current = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
65
|
+
return () => {
|
|
66
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
67
|
+
if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
|
|
68
|
+
};
|
|
69
|
+
}, [isVaultLocked, setVaultLocked]);
|
|
70
|
+
};
|
package/src/Penyaring.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const stripHtml = (html: string): string => {
|
|
2
|
+
if (!html) return '';
|
|
3
|
+
// Use a simple regex for universal support (SSR safe)
|
|
4
|
+
return html.replace(/<[^>]*>?/gm, '');
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const truncate = (str: string, length: number): string => {
|
|
8
|
+
if (str.length <= length) return str;
|
|
9
|
+
return str.slice(0, length) + '...';
|
|
10
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { usePundi } from './Pundi';
|
|
5
|
+
import { Brankas } from './Brankas';
|
|
6
|
+
|
|
7
|
+
export const usePenyelaras = () => {
|
|
8
|
+
const isVaultLocked = usePundi(s => s.isVaultLocked);
|
|
9
|
+
const setVaultLocked = usePundi(s => s.setVaultLocked);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const channel = new BroadcastChannel('lembaran-vault-sync');
|
|
13
|
+
|
|
14
|
+
const handleMessage = (event: MessageEvent) => {
|
|
15
|
+
if (event.data.type === 'VAULT_LOCK_STATUS') {
|
|
16
|
+
const newStatus = event.data.locked;
|
|
17
|
+
if (newStatus !== isVaultLocked) {
|
|
18
|
+
if (newStatus) {
|
|
19
|
+
Brankas.clearKey();
|
|
20
|
+
}
|
|
21
|
+
setVaultLocked(newStatus);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
channel.addEventListener('message', handleMessage);
|
|
27
|
+
|
|
28
|
+
return () => {
|
|
29
|
+
channel.removeEventListener('message', handleMessage);
|
|
30
|
+
channel.close();
|
|
31
|
+
};
|
|
32
|
+
}, [isVaultLocked, setVaultLocked]);
|
|
33
|
+
|
|
34
|
+
// Kita panggil ini saat status berubah di tab ini
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const channel = new BroadcastChannel('lembaran-vault-sync');
|
|
37
|
+
channel.postMessage({ type: 'VAULT_LOCK_STATUS', locked: isVaultLocked });
|
|
38
|
+
channel.close();
|
|
39
|
+
}, [isVaultLocked]);
|
|
40
|
+
};
|
package/src/Pujangga.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { PujanggaProvider, PujanggaPlan } from './ai/types';
|
|
2
|
+
import { GeminiProvider } from './ai/GeminiProvider';
|
|
3
|
+
import { LocalProvider } from './ai/LocalProvider';
|
|
4
|
+
import { PenyaringRahasia } from './ai/PenyaringRahasia';
|
|
5
|
+
import { AuditLog } from './AuditLog';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pujangga Engine: Modular & Private AI Architecture.
|
|
9
|
+
* Standardized as a class for maximum compatibility.
|
|
10
|
+
*/
|
|
11
|
+
export class Pujangga {
|
|
12
|
+
static _provider: PujanggaProvider = new LocalProvider();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Sets the active AI provider.
|
|
16
|
+
*/
|
|
17
|
+
static setProvider(type: 'gemini' | 'openai' | 'claude' | 'none') {
|
|
18
|
+
switch (type) {
|
|
19
|
+
case 'gemini': this._provider = new GeminiProvider(); break;
|
|
20
|
+
case 'none': this._provider = new LocalProvider(); break;
|
|
21
|
+
default: this._provider = new LocalProvider();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Smart Brain with Privacy Scrubbing.
|
|
27
|
+
*/
|
|
28
|
+
static async berpikir(konteks: string, instruksi: string): Promise<PujanggaPlan> {
|
|
29
|
+
// Step 1: Scrub Secrets
|
|
30
|
+
const safeKonteks = PenyaringRahasia.saring(konteks);
|
|
31
|
+
const safeInstruksi = PenyaringRahasia.saring(instruksi);
|
|
32
|
+
|
|
33
|
+
// Step 2: Laporan Transparansi (Audit)
|
|
34
|
+
await AuditLog.catat('PERMINTAAN_KECERDASAN', {
|
|
35
|
+
model: this._provider.name,
|
|
36
|
+
konteks: safeKonteks,
|
|
37
|
+
instruksi: safeInstruksi
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Step 3: Delegate to Provider
|
|
41
|
+
const plan = await this._provider.berpikir(safeKonteks, safeInstruksi);
|
|
42
|
+
|
|
43
|
+
// Step 4: Catat Keputusan
|
|
44
|
+
await AuditLog.catat('KEPUTUSAN_SENTINEL', plan);
|
|
45
|
+
|
|
46
|
+
return plan;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Heuristic methods.
|
|
51
|
+
*/
|
|
52
|
+
static async sarankanTag(konten: string): Promise<string[]> {
|
|
53
|
+
const clean = konten.toLowerCase();
|
|
54
|
+
const tags: string[] = [];
|
|
55
|
+
if (clean.includes('koding') || clean.includes('bug')) tags.push('Developer');
|
|
56
|
+
return tags;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static async sarankanJudul(konten: string): Promise<string> {
|
|
60
|
+
return konten.substring(0, 30);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static async ringkasCerdas(konten: string): Promise<string> {
|
|
64
|
+
return konten.substring(0, 150);
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/Pundi.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { create } from 'zustand';
|
|
4
|
+
import { persist } from 'zustand/middleware';
|
|
5
|
+
import { AppSettings, UserProfile } from './Rumus';
|
|
6
|
+
|
|
7
|
+
export interface LembaranState {
|
|
8
|
+
settings: AppSettings;
|
|
9
|
+
profile: UserProfile;
|
|
10
|
+
isVaultLocked: boolean;
|
|
11
|
+
|
|
12
|
+
// Actions
|
|
13
|
+
updateSettings: (settings: Partial<AppSettings>) => void;
|
|
14
|
+
updateProfile: (profile: Partial<UserProfile>) => void;
|
|
15
|
+
setVaultLocked: (isLocked: boolean) => void;
|
|
16
|
+
addCustomTheme: (name: string, color: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEFAULT_SETTINGS: AppSettings = {
|
|
20
|
+
language: 'id',
|
|
21
|
+
theme: 'auto',
|
|
22
|
+
accentColor: '#135bec',
|
|
23
|
+
encryptionEnabled: false,
|
|
24
|
+
syncEnabled: false,
|
|
25
|
+
secretMode: "none",
|
|
26
|
+
lastSyncAt: null,
|
|
27
|
+
sessionTimeout: 1,
|
|
28
|
+
vimMode: false,
|
|
29
|
+
biometricEnabled: false,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const DEFAULT_PROFILE: UserProfile = {
|
|
33
|
+
name: 'Arsiparis',
|
|
34
|
+
bio: 'Menyusun fragmen memori dalam harmoni.',
|
|
35
|
+
avatarUrl: '',
|
|
36
|
+
level: 1,
|
|
37
|
+
xp: 0,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const usePundi = create<LembaranState>()(
|
|
41
|
+
persist(
|
|
42
|
+
(set) => ({
|
|
43
|
+
settings: DEFAULT_SETTINGS,
|
|
44
|
+
profile: DEFAULT_PROFILE,
|
|
45
|
+
isVaultLocked: true,
|
|
46
|
+
|
|
47
|
+
updateSettings: (newSettings) =>
|
|
48
|
+
set((state) => ({ settings: { ...state.settings, ...newSettings } })),
|
|
49
|
+
|
|
50
|
+
updateProfile: (newProfile) =>
|
|
51
|
+
set((state) => ({ profile: { ...state.profile, ...newProfile } })),
|
|
52
|
+
|
|
53
|
+
setVaultLocked: (isLocked) =>
|
|
54
|
+
set({ isVaultLocked: isLocked }),
|
|
55
|
+
|
|
56
|
+
addCustomTheme: (name, color) =>
|
|
57
|
+
set((state) => ({
|
|
58
|
+
settings: {
|
|
59
|
+
...state.settings,
|
|
60
|
+
customThemes: { ...(state.settings.customThemes || {}), [name]: color }
|
|
61
|
+
}
|
|
62
|
+
})),
|
|
63
|
+
}),
|
|
64
|
+
{
|
|
65
|
+
name: 'lembaran:pundi',
|
|
66
|
+
partialize: (state) => ({ settings: state.settings, profile: state.profile }),
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
);
|
package/src/Rumus.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type EntityId = string;
|
|
2
|
+
|
|
3
|
+
export interface Note {
|
|
4
|
+
id: EntityId;
|
|
5
|
+
title: string;
|
|
6
|
+
content: string; // Encrypted blob (iv|data)
|
|
7
|
+
preview?: string; // Plain-text snippet for list view
|
|
8
|
+
folderId: EntityId | null;
|
|
9
|
+
isPinned: boolean;
|
|
10
|
+
isFavorite: boolean;
|
|
11
|
+
tags: string[];
|
|
12
|
+
createdAt: string;
|
|
13
|
+
updatedAt: string;
|
|
14
|
+
isCredentials?: boolean;
|
|
15
|
+
kredensial?: string | {
|
|
16
|
+
username?: string;
|
|
17
|
+
password?: string;
|
|
18
|
+
url?: string;
|
|
19
|
+
};
|
|
20
|
+
_hash?: string;
|
|
21
|
+
_timestamp?: string;
|
|
22
|
+
syncStatus?: "synced" | "pending" | "error";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Folder {
|
|
26
|
+
id: EntityId;
|
|
27
|
+
name: string;
|
|
28
|
+
parentId: EntityId | null;
|
|
29
|
+
icon?: string;
|
|
30
|
+
color?: string;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AppSettings {
|
|
35
|
+
language: 'id' | 'en';
|
|
36
|
+
theme: 'light' | 'dark' | 'auto';
|
|
37
|
+
accentColor: string;
|
|
38
|
+
encryptionEnabled: boolean;
|
|
39
|
+
syncEnabled: boolean;
|
|
40
|
+
lastSyncAt: string | null;
|
|
41
|
+
secretMode: "none" | "gmail";
|
|
42
|
+
panicKeyHash?: string;
|
|
43
|
+
sessionTimeout?: number;
|
|
44
|
+
customThemes?: Record<string, string>;
|
|
45
|
+
vimMode?: boolean;
|
|
46
|
+
biometricEnabled?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface UserProfile {
|
|
50
|
+
name: string;
|
|
51
|
+
bio: string;
|
|
52
|
+
avatarUrl: string;
|
|
53
|
+
level: number;
|
|
54
|
+
xp: number;
|
|
55
|
+
}
|