@mazees/mark 5.0.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/LICENSE +29 -0
- package/README.md +209 -0
- package/bin/mark.js +27 -0
- package/out/renderer/assets/icon-DHo2CYph.svg +1 -0
- package/out/renderer/assets/index-G2fADWO-.js +4768 -0
- package/out/renderer/assets/index-f3mVse3c.css +1 -0
- package/out/renderer/assets/music-cover-CG4vi3kZ.png +0 -0
- package/out/renderer/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-500-normal-BIdkeU1p.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-500-normal-DMPDjHtT.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-600-normal-ClASKHrr.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-600-normal-STEjXBNN.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-700-normal-O-jipLrW.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-700-normal-fHs-vx92.woff +0 -0
- package/out/renderer/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
- package/out/renderer/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
- package/out/renderer/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
- package/out/renderer/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
- package/out/renderer/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-500-normal-CK-6C4Hw.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-500-normal-CgAe2rWW.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-600-normal-CAhIAdZj.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-600-normal-Df5ffKXP.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-700-normal-DctTR6Tg.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-700-normal-cby-RkWa.woff2 +0 -0
- package/out/renderer/favicon.ico +0 -0
- package/out/renderer/icon-256.png +0 -0
- package/out/renderer/icon.svg +1 -0
- package/out/renderer/index.html +25 -0
- package/out/renderer/manifest.json +25 -0
- package/out/renderer/robot-simulator.html +1640 -0
- package/package.json +106 -0
- package/src/cli/index.js +332 -0
- package/src/cli/monitor.js +413 -0
- package/src/cli/theme.js +347 -0
- package/src/launcher/MarkTray.cs +227 -0
- package/src/main/browser-agent.js +1132 -0
- package/src/main/git-service.js +90 -0
- package/src/main/google/google-calendar.js +69 -0
- package/src/main/google/google-drive.js +263 -0
- package/src/main/google/google-gmail.js +131 -0
- package/src/main/google/google-service.js +241 -0
- package/src/main/node-tools.js +23 -0
- package/src/main/pc-agent-scripts/mouse-locker.ps1 +76 -0
- package/src/main/pc-agent-scripts/ocr-region.ps1 +185 -0
- package/src/main/pc-agent-scripts/pc-daemon.ps1 +726 -0
- package/src/main/pc-agent-scripts/pc-overlay.ps1 +253 -0
- package/src/main/pc-agent-scripts/pick-folder.ps1 +33 -0
- package/src/main/pc-agent-scripts/read-ui.ps1 +181 -0
- package/src/main/pc-agent-scripts/take-screenshot.ps1 +24 -0
- package/src/main/pc-agent-scripts/win-action.ps1 +380 -0
- package/src/main/plugins/plugin-loader.js +306 -0
- package/src/main/skills/skill-manager.js +265 -0
- package/src/main/syntax-validator.js +455 -0
- package/src/main/task-daemon.js +117 -0
- package/src/main/telegram/telegram-service.js +835 -0
- package/src/main/tools/browser-tools.js +516 -0
- package/src/main/tools/file-tools.js +1126 -0
- package/src/main/tools/git-tools.js +79 -0
- package/src/main/tools/google-tools.js +566 -0
- package/src/main/tools/index.js +36 -0
- package/src/main/tools/media-tools.js +42 -0
- package/src/main/tools/system-tools.js +295 -0
- package/src/main/tools/task-tools.js +73 -0
- package/src/main/tools/telegram-tools.js +101 -0
- package/src/renderer/index.html +24 -0
- package/src/renderer/public/favicon.ico +0 -0
- package/src/renderer/public/icon-256.png +0 -0
- package/src/renderer/public/icon.svg +1 -0
- package/src/renderer/public/manifest.json +25 -0
- package/src/renderer/public/robot-simulator.html +1640 -0
- package/src/renderer/src/App.jsx +304 -0
- package/src/renderer/src/api/ai/awareness.js +294 -0
- package/src/renderer/src/api/ai/chatSummarizer.js +79 -0
- package/src/renderer/src/api/ai/contextCompactor.js +129 -0
- package/src/renderer/src/api/ai/contextManager.js +1130 -0
- package/src/renderer/src/api/ai/core.js +167 -0
- package/src/renderer/src/api/ai/index.js +4 -0
- package/src/renderer/src/api/ai/memoryGroomer.js +101 -0
- package/src/renderer/src/api/ai/persona.js +115 -0
- package/src/renderer/src/api/ai/planning.js +317 -0
- package/src/renderer/src/api/ai/relationship.js +177 -0
- package/src/renderer/src/api/ai/skillSynthesizer.js +168 -0
- package/src/renderer/src/api/ai/tools.js +167 -0
- package/src/renderer/src/api/ai/utils.js +359 -0
- package/src/renderer/src/api/db.js +1013 -0
- package/src/renderer/src/api/oramaStore.js +592 -0
- package/src/renderer/src/api/ragPipeline.js +87 -0
- package/src/renderer/src/api/subagent/subagentExecutor.js +780 -0
- package/src/renderer/src/api/subagent/subagentPrompt.js +44 -0
- package/src/renderer/src/api/subagent/subagentStore.js +173 -0
- package/src/renderer/src/api/taskExecutor.js +18 -0
- package/src/renderer/src/api/taskStore.js +315 -0
- package/src/renderer/src/api/tools/core-tools.js +707 -0
- package/src/renderer/src/api/tools/index.js +151 -0
- package/src/renderer/src/api/turnPairMigrator.js +200 -0
- package/src/renderer/src/api/vectorMemory.js +136 -0
- package/src/renderer/src/api/wakeWord.js +101 -0
- package/src/renderer/src/api/web-bridge.js +885 -0
- package/src/renderer/src/api/webSpeech.js +178 -0
- package/src/renderer/src/api/workspaceRag.js +76 -0
- package/src/renderer/src/assets/electron.svg +10 -0
- package/src/renderer/src/assets/icon.svg +1 -0
- package/src/renderer/src/assets/main.css +589 -0
- package/src/renderer/src/assets/music-cover.png +0 -0
- package/src/renderer/src/assets/wavy-lines.svg +25 -0
- package/src/renderer/src/components/Chat/ApprovalBubble.jsx +127 -0
- package/src/renderer/src/components/Chat/CodeBlock.jsx +83 -0
- package/src/renderer/src/components/Chat/DurableTaskBubble.jsx +455 -0
- package/src/renderer/src/components/Chat/FileDiffModal.jsx +328 -0
- package/src/renderer/src/components/Chat/GeminiWidgets.jsx +82 -0
- package/src/renderer/src/components/Chat/MemoryFooterBubble.jsx +34 -0
- package/src/renderer/src/components/Chat/MessageBubble.jsx +459 -0
- package/src/renderer/src/components/Chat/PluginExecutionBubble.jsx +41 -0
- package/src/renderer/src/components/Chat/ThinkingBubble.jsx +316 -0
- package/src/renderer/src/components/Chat/YoutubeSearchBubble.jsx +43 -0
- package/src/renderer/src/components/Chat/YoutubeSummaryBubble.jsx +40 -0
- package/src/renderer/src/components/Chat/index.js +11 -0
- package/src/renderer/src/components/ChatList.jsx +434 -0
- package/src/renderer/src/components/GlobalCameraManager.jsx +67 -0
- package/src/renderer/src/components/Versions.jsx +15 -0
- package/src/renderer/src/components/YoutubeMusicPlayer.jsx +375 -0
- package/src/renderer/src/components/camera/CameraPreview.jsx +236 -0
- package/src/renderer/src/components/config/AiEngineSection.jsx +363 -0
- package/src/renderer/src/components/config/CameraSection.jsx +100 -0
- package/src/renderer/src/components/config/PermissionsSection.jsx +130 -0
- package/src/renderer/src/components/config/ShortcutSection.jsx +102 -0
- package/src/renderer/src/components/config/VoiceSection.jsx +110 -0
- package/src/renderer/src/components/config/index.js +5 -0
- package/src/renderer/src/components/core/Avatar.jsx +2197 -0
- package/src/renderer/src/components/core/ConfirmModal.jsx +80 -0
- package/src/renderer/src/components/core/DraggableHoloCard.jsx +125 -0
- package/src/renderer/src/components/core/FloatingMenu.jsx +272 -0
- package/src/renderer/src/components/core/HoloCard.jsx +64 -0
- package/src/renderer/src/components/core/InputBar.jsx +1346 -0
- package/src/renderer/src/components/core/ProcessPanel.jsx +216 -0
- package/src/renderer/src/components/core/ResponseArea.jsx +149 -0
- package/src/renderer/src/components/core/StatusIndicator.jsx +59 -0
- package/src/renderer/src/components/core/ThoughtNeuralFlow.jsx +168 -0
- package/src/renderer/src/components/core/ToolClustersDeck.jsx +210 -0
- package/src/renderer/src/components/core/native-skills.js +43 -0
- package/src/renderer/src/components/neural-core/EmotionalCortexLobe.jsx +359 -0
- package/src/renderer/src/components/neural-core/NodeDissectionModal.jsx +57 -0
- package/src/renderer/src/components/neural-core/ProceduralMatrixLobe.jsx +193 -0
- package/src/renderer/src/components/neural-core/ProceduralSkillModal.jsx +53 -0
- package/src/renderer/src/components/neural-core/ResetAiModal.jsx +198 -0
- package/src/renderer/src/components/neural-core/SynapticMemoryLobe.jsx +204 -0
- package/src/renderer/src/components/neural-core/traitMeta.js +102 -0
- package/src/renderer/src/components/subagent/AgentChatFeed.jsx +603 -0
- package/src/renderer/src/components/subagent/AgentInspector.jsx +224 -0
- package/src/renderer/src/components/subagent/AgentSidebar.jsx +216 -0
- package/src/renderer/src/components/subagent/SubagentIntercom.jsx +572 -0
- package/src/renderer/src/components/subagent/SubagentTopologyMap.jsx +423 -0
- package/src/renderer/src/contexts/ApprovalContext.jsx +425 -0
- package/src/renderer/src/contexts/ChatContext.jsx +61 -0
- package/src/renderer/src/contexts/YoutubeMusicContext.jsx +333 -0
- package/src/renderer/src/data/faqData.js +202 -0
- package/src/renderer/src/hooks/agent/executeAgentTool.js +895 -0
- package/src/renderer/src/hooks/agent/index.js +4 -0
- package/src/renderer/src/hooks/agent/useMarkMusic.js +75 -0
- package/src/renderer/src/hooks/agent/useMarkPlan.js +2739 -0
- package/src/renderer/src/hooks/agent/useMarkState.js +173 -0
- package/src/renderer/src/hooks/agent/useMarkYoutube.js +82 -0
- package/src/renderer/src/hooks/agent/useRelationalGrowth.js +201 -0
- package/src/renderer/src/hooks/telegram/useTelegramBot.js +63 -0
- package/src/renderer/src/hooks/useAwareness.js +289 -0
- package/src/renderer/src/hooks/useChatArchiver.js +52 -0
- package/src/renderer/src/hooks/useConfirm.jsx +68 -0
- package/src/renderer/src/hooks/useMarkAgent.js +454 -0
- package/src/renderer/src/hooks/useMemoryGroomer.js +152 -0
- package/src/renderer/src/hooks/useVAD.js +511 -0
- package/src/renderer/src/hooks/useWebSpeechWakeWord.js +122 -0
- package/src/renderer/src/main.jsx +12 -0
- package/src/renderer/src/pages/ChatStudio.jsx +922 -0
- package/src/renderer/src/pages/Configuration.jsx +360 -0
- package/src/renderer/src/pages/GoogleWorkspace.jsx +293 -0
- package/src/renderer/src/pages/Guidebook.jsx +917 -0
- package/src/renderer/src/pages/Knowledge.jsx +241 -0
- package/src/renderer/src/pages/MarkHome.jsx +491 -0
- package/src/renderer/src/pages/NeuralCore.jsx +786 -0
- package/src/renderer/src/pages/Plugins.jsx +630 -0
- package/src/renderer/src/pages/SkillEditor.jsx +380 -0
- package/src/renderer/src/pages/Skills.jsx +219 -0
- package/src/renderer/src/pages/Subagents.jsx +342 -0
- package/src/renderer/src/pages/TelegramBot.jsx +343 -0
- package/src/renderer/src/utils/chatContentParser.js +203 -0
- package/src/renderer/src/utils/toolClusters.js +201 -0
- package/src/server/bin/sha3_wasm_bg.wasm +0 -0
- package/src/server/config-manager.js +57 -0
- package/src/server/index.js +195 -0
- package/src/server/launcher.js +87 -0
- package/src/server/memory/db-store.js +758 -0
- package/src/server/memory/orama-store.js +177 -0
- package/src/server/memory/vector-engine.js +109 -0
- package/src/server/routes/ai.routes.js +280 -0
- package/src/server/routes/awareness.routes.js +62 -0
- package/src/server/routes/chat.routes.js +61 -0
- package/src/server/routes/config.routes.js +41 -0
- package/src/server/routes/index.js +35 -0
- package/src/server/routes/integrations.routes.js +116 -0
- package/src/server/routes/memory.routes.js +256 -0
- package/src/server/routes/plugins.routes.js +72 -0
- package/src/server/routes/skills.routes.js +139 -0
- package/src/server/routes/subagents.routes.js +70 -0
- package/src/server/routes/tasks.routes.js +175 -0
- package/src/server/routes/workspace.routes.js +127 -0
- package/src/server/services/ai/ai-utils.js +161 -0
- package/src/server/services/ai/openai-provider.js +757 -0
- package/src/server/services/ai/web-provider.js +729 -0
- package/src/server/services/ai-bridge.js +46 -0
- package/src/server/services/deepseek-web.js +956 -0
- package/src/server/services/gemini-web.js +170 -0
- package/src/server/services/notification-service.js +91 -0
- package/src/server/services/updater.js +125 -0
- package/src/server/tools/awareness-tracker.js +292 -0
- package/src/server/tools/group-tools.js +1164 -0
- package/src/server/tools/media-tools.js +244 -0
- package/src/server/tools/pc-agent.js +327 -0
- package/src/server/tools/screen-service.js +51 -0
- package/src/server/ws-hub.js +168 -0
- package/src/shared/parsers/mark-tag-parser.js +275 -0
|
@@ -0,0 +1,1126 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import { exec } from 'child_process'
|
|
5
|
+
import util from 'util'
|
|
6
|
+
import { fileURLToPath } from 'url'
|
|
7
|
+
import { validateFileSyntax } from '../syntax-validator.js'
|
|
8
|
+
import { dbStore } from '../../server/memory/db-store.js'
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
11
|
+
const __dirname = path.dirname(__filename)
|
|
12
|
+
const execPromise = util.promisify(exec)
|
|
13
|
+
|
|
14
|
+
export const fileTools = {
|
|
15
|
+
'read-skill': {
|
|
16
|
+
needsApproval: false,
|
|
17
|
+
handler: async (args) => {
|
|
18
|
+
const skillName = (
|
|
19
|
+
typeof args === 'object' && args !== null
|
|
20
|
+
? args.skill_name || args.name
|
|
21
|
+
: String(args || '')
|
|
22
|
+
).trim()
|
|
23
|
+
if (!skillName) return { success: false, error: 'Nama skill kosong' }
|
|
24
|
+
const skillDir = path.join(os.homedir(), 'Documents', 'Mark Skills')
|
|
25
|
+
|
|
26
|
+
// 1. Cek jika folder skill berisi SKILL.md
|
|
27
|
+
const folderSkillPath = path.join(skillDir, skillName, 'SKILL.md')
|
|
28
|
+
if (fs.existsSync(folderSkillPath)) {
|
|
29
|
+
const content = await fs.promises.readFile(folderSkillPath, 'utf8')
|
|
30
|
+
return { success: true, content, data: content }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 2. Cek jika file standalone .md
|
|
34
|
+
const fileSkillPath = path.join(skillDir, `${skillName}.md`)
|
|
35
|
+
if (fs.existsSync(fileSkillPath)) {
|
|
36
|
+
const content = await fs.promises.readFile(fileSkillPath, 'utf8')
|
|
37
|
+
return { success: true, content, data: content }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 3. Cek direct file path jika query mengandung sub-path
|
|
41
|
+
const directPath = path.join(skillDir, skillName)
|
|
42
|
+
if (fs.existsSync(directPath) && !fs.statSync(directPath).isDirectory()) {
|
|
43
|
+
const content = await fs.promises.readFile(directPath, 'utf8')
|
|
44
|
+
return { success: true, content, data: content }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 4. Fallback: Cek apakah tersimpan di SQLite database learned_skills
|
|
48
|
+
if (dbStore && dbStore.learnedSkills) {
|
|
49
|
+
try {
|
|
50
|
+
const allLearned = dbStore.learnedSkills.getAll()
|
|
51
|
+
const matched = allLearned.find(
|
|
52
|
+
(s) =>
|
|
53
|
+
s.name?.toLowerCase() === skillName.toLowerCase() ||
|
|
54
|
+
s.id?.toLowerCase() === skillName.toLowerCase()
|
|
55
|
+
)
|
|
56
|
+
if (matched && matched.content) {
|
|
57
|
+
return {
|
|
58
|
+
success: true,
|
|
59
|
+
content: matched.content,
|
|
60
|
+
data: matched.content,
|
|
61
|
+
source: 'learned_skills_db'
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (_) {}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
success: false,
|
|
69
|
+
error: `Skill '${skillName}' tidak ditemukan di folder 'Documents/Mark Skills' maupun di basis data Learned Skills.`
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
'read-file': {
|
|
75
|
+
needsApproval: false,
|
|
76
|
+
handler: async (args, config) => {
|
|
77
|
+
try {
|
|
78
|
+
let filePath = ''
|
|
79
|
+
let startLine = null
|
|
80
|
+
let endLine = null
|
|
81
|
+
|
|
82
|
+
if (typeof args === 'object' && args !== null) {
|
|
83
|
+
filePath = (args.path || '').trim()
|
|
84
|
+
startLine = args.start_line !== undefined ? parseInt(args.start_line, 10) : null
|
|
85
|
+
endLine = args.end_line !== undefined ? parseInt(args.end_line, 10) : null
|
|
86
|
+
} else {
|
|
87
|
+
const parts = String(args || '').split('||')
|
|
88
|
+
filePath = parts[0].trim()
|
|
89
|
+
if (parts.length >= 3) {
|
|
90
|
+
startLine = parseInt(parts[1].trim(), 10)
|
|
91
|
+
endLine = parseInt(parts[2].trim(), 10)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const activeRoot =
|
|
96
|
+
config?.workspaceRoot || path.join(os.homedir(), 'Documents', 'Mark Workspace')
|
|
97
|
+
if (!path.isAbsolute(filePath)) {
|
|
98
|
+
filePath = path.join(activeRoot, filePath)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const isRaw = typeof args === 'object' && args !== null && Boolean(args.raw)
|
|
102
|
+
|
|
103
|
+
if (!fs.existsSync(filePath)) {
|
|
104
|
+
if (isRaw) {
|
|
105
|
+
return { success: true, content: '', isNewFile: true, exists: false }
|
|
106
|
+
}
|
|
107
|
+
return { success: false, message: `File tidak ditemukan di path: ${filePath}` }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
111
|
+
const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.bmp']
|
|
112
|
+
if (IMAGE_EXTENSIONS.includes(ext)) {
|
|
113
|
+
const fileBuffer = await fs.promises.readFile(filePath)
|
|
114
|
+
const mimeType =
|
|
115
|
+
ext === '.png' ? 'image/png' : ext === '.webp' ? 'image/webp' : 'image/jpeg'
|
|
116
|
+
const b64 = fileBuffer.toString('base64')
|
|
117
|
+
return {
|
|
118
|
+
success: true,
|
|
119
|
+
isImage: true,
|
|
120
|
+
message: `File '${path.basename(filePath)}' adalah gambar visual (${ext}). Konten visual telah dikonversi dan dikirim ke mesin AI Vision.`,
|
|
121
|
+
dataUrl: `data:${mimeType};base64,${b64}`
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const content = await fs.promises.readFile(filePath, 'utf8')
|
|
126
|
+
|
|
127
|
+
if (isRaw) {
|
|
128
|
+
return {
|
|
129
|
+
success: true,
|
|
130
|
+
content,
|
|
131
|
+
totalLines: content.split('\n').length,
|
|
132
|
+
exists: true
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const lines = content.split('\n')
|
|
137
|
+
const totalLines = lines.length
|
|
138
|
+
|
|
139
|
+
if (startLine !== null && endLine !== null && !isNaN(startLine) && !isNaN(endLine)) {
|
|
140
|
+
const sliceLines = lines.slice(Math.max(0, startLine - 1), Math.min(totalLines, endLine))
|
|
141
|
+
const sliceContent = sliceLines.map((l, i) => `[${startLine + i}] ${l}`).join('\n')
|
|
142
|
+
return {
|
|
143
|
+
success: true,
|
|
144
|
+
totalLines,
|
|
145
|
+
showing: `Baris ${startLine} - ${Math.min(totalLines, endLine)} (dari total ${totalLines} baris)`,
|
|
146
|
+
content: sliceContent
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Jika file <= 800 baris, sajikan 100% UTUH tanpa potongan buatan
|
|
151
|
+
if (totalLines <= 800) {
|
|
152
|
+
const fullContent = lines.map((l, i) => `[${i + 1}] ${l}`).join('\n')
|
|
153
|
+
return {
|
|
154
|
+
success: true,
|
|
155
|
+
totalLines,
|
|
156
|
+
showing: `Baris 1 - ${totalLines} (Lengkap 100%)`,
|
|
157
|
+
content: fullContent
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Jika file > 800 baris, sajikan 800 baris awal (standar Antigravity)
|
|
162
|
+
const defaultLines = lines.slice(0, 800)
|
|
163
|
+
const defaultContent = defaultLines.map((l, i) => `[${i + 1}] ${l}`).join('\n')
|
|
164
|
+
return {
|
|
165
|
+
success: true,
|
|
166
|
+
totalLines,
|
|
167
|
+
showing: `Baris 1 - 800 (dari total ${totalLines} baris)`,
|
|
168
|
+
content: defaultContent,
|
|
169
|
+
note: `File panjang (${totalLines} baris). Menampilkan 800 baris awal. Gunakan read-file dengan argumen start_line & end_line, atau gunakan grep-search untuk mencari fungsi/variabel tertentu secara presisi.`
|
|
170
|
+
}
|
|
171
|
+
} catch (e) {
|
|
172
|
+
return { success: false, error: e.message }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
'read-image': {
|
|
178
|
+
needsApproval: false,
|
|
179
|
+
handler: async (args, config) => {
|
|
180
|
+
try {
|
|
181
|
+
let filePath = ''
|
|
182
|
+
let query = null
|
|
183
|
+
if (typeof args === 'object' && args !== null) {
|
|
184
|
+
filePath = (args.file_path || args.path || '').trim()
|
|
185
|
+
query = args.query ? String(args.query).trim() : null
|
|
186
|
+
} else {
|
|
187
|
+
filePath = String(args || '').trim()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!filePath) {
|
|
191
|
+
return { success: false, message: 'Path berkas gambar wajib diisi.' }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const activeRoot =
|
|
195
|
+
config?.workspaceRoot || path.join(os.homedir(), 'Documents', 'Mark Workspace')
|
|
196
|
+
if (!path.isAbsolute(filePath)) {
|
|
197
|
+
filePath = path.join(activeRoot, filePath)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!fs.existsSync(filePath)) {
|
|
201
|
+
return { success: false, message: `Berkas gambar tidak ditemukan di path: ${filePath}` }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
205
|
+
const mimeMap = {
|
|
206
|
+
'.png': 'image/png',
|
|
207
|
+
'.jpg': 'image/jpeg',
|
|
208
|
+
'.jpeg': 'image/jpeg',
|
|
209
|
+
'.webp': 'image/webp',
|
|
210
|
+
'.gif': 'image/gif',
|
|
211
|
+
'.bmp': 'image/bmp',
|
|
212
|
+
'.svg': 'image/svg+xml'
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const mimeType = mimeMap[ext] || 'image/png'
|
|
216
|
+
const fileBuffer = await fs.promises.readFile(filePath)
|
|
217
|
+
const b64 = fileBuffer.toString('base64')
|
|
218
|
+
const dataUrl = `data:${mimeType};base64,${b64}`
|
|
219
|
+
const fileName = path.basename(filePath)
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
success: true,
|
|
223
|
+
isImage: true,
|
|
224
|
+
path: filePath,
|
|
225
|
+
filename: fileName,
|
|
226
|
+
query,
|
|
227
|
+
dataUrl,
|
|
228
|
+
data: `File gambar '${fileName}' berhasil dibaca. Siap dianalisis secara visual.`,
|
|
229
|
+
message: `File gambar '${fileName}' berhasil dibaca.`
|
|
230
|
+
}
|
|
231
|
+
} catch (e) {
|
|
232
|
+
return { success: false, error: e.message }
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
'file-outline': {
|
|
238
|
+
needsApproval: false,
|
|
239
|
+
handler: async (args, config) => {
|
|
240
|
+
try {
|
|
241
|
+
let filePath = (
|
|
242
|
+
typeof args === 'object' && args !== null ? args.path : String(args || '')
|
|
243
|
+
).trim()
|
|
244
|
+
const activeRoot =
|
|
245
|
+
config?.workspaceRoot || path.join(os.homedir(), 'Documents', 'Mark Workspace')
|
|
246
|
+
if (!path.isAbsolute(filePath)) {
|
|
247
|
+
filePath = path.join(activeRoot, filePath)
|
|
248
|
+
}
|
|
249
|
+
if (!fs.existsSync(filePath))
|
|
250
|
+
return { success: false, message: `File tidak ditemukan di path: ${filePath}` }
|
|
251
|
+
|
|
252
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
253
|
+
const lines = content.split('\n')
|
|
254
|
+
const totalLines = lines.length
|
|
255
|
+
|
|
256
|
+
const structuralRegex =
|
|
257
|
+
/^(?:\s*)(?:export\s+|async\s+|function\s+|class\s+|const\s+\w+\s*=\s*(?:async\s*)?\(|let\s+\w+\s*=\s*(?:async\s*)?\(|var\s+\w+\s*=\s*(?:async\s*)?\(|def\s+|type\s+|interface\s+|struct\s+|#+\s+|ipcMain\.|window\.api\.|return\s+\()/i
|
|
258
|
+
|
|
259
|
+
const outlineItems = []
|
|
260
|
+
lines.forEach((line, index) => {
|
|
261
|
+
if (structuralRegex.test(line)) {
|
|
262
|
+
const trimmed = line.trim()
|
|
263
|
+
if (trimmed.length > 0) {
|
|
264
|
+
outlineItems.push(`[Baris ${index + 1}] ${trimmed.slice(0, 120)}`)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
if (outlineItems.length === 0) {
|
|
270
|
+
const step = Math.max(1, Math.floor(totalLines / 20))
|
|
271
|
+
for (let i = 0; i < totalLines; i += step) {
|
|
272
|
+
const trimmed = lines[i].trim()
|
|
273
|
+
if (trimmed) {
|
|
274
|
+
outlineItems.push(`[Baris ${i + 1}] ${trimmed.slice(0, 100)}`)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
success: true,
|
|
281
|
+
totalLines,
|
|
282
|
+
outlineCount: outlineItems.length,
|
|
283
|
+
outline: outlineItems.join('\n')
|
|
284
|
+
}
|
|
285
|
+
} catch (e) {
|
|
286
|
+
return { success: false, error: e.message }
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
'read-document': {
|
|
292
|
+
needsApproval: false,
|
|
293
|
+
handler: async (args) => {
|
|
294
|
+
try {
|
|
295
|
+
let filePath = ''
|
|
296
|
+
let searchQuery = ''
|
|
297
|
+
let startLine = null
|
|
298
|
+
let endLine = null
|
|
299
|
+
|
|
300
|
+
if (typeof args === 'object' && args !== null) {
|
|
301
|
+
filePath = (args.path || '').trim()
|
|
302
|
+
searchQuery = (args.keyword || '').trim()
|
|
303
|
+
startLine = args.start_line !== undefined ? parseInt(args.start_line, 10) : null
|
|
304
|
+
endLine = args.end_line !== undefined ? parseInt(args.end_line, 10) : null
|
|
305
|
+
} else {
|
|
306
|
+
const parts = String(args || '').split('||')
|
|
307
|
+
filePath = parts[0].trim()
|
|
308
|
+
const param2 = parts[1] ? parts[1].trim() : ''
|
|
309
|
+
const param3 = parts[2] ? parts[2].trim() : ''
|
|
310
|
+
if (param2 && !isNaN(param2) && param3 && !isNaN(param3)) {
|
|
311
|
+
startLine = parseInt(param2, 10)
|
|
312
|
+
endLine = parseInt(param3, 10)
|
|
313
|
+
} else {
|
|
314
|
+
searchQuery = param2
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!fs.existsSync(filePath))
|
|
319
|
+
return { success: false, message: 'File tidak ditemukan di path tersebut.' }
|
|
320
|
+
|
|
321
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
322
|
+
let rawText = ''
|
|
323
|
+
|
|
324
|
+
if (ext === '.pdf') {
|
|
325
|
+
const buffer = fs.readFileSync(filePath)
|
|
326
|
+
try {
|
|
327
|
+
const pdfParseModule = await import('pdf-parse')
|
|
328
|
+
const pdfFn = pdfParseModule.default || pdfParseModule
|
|
329
|
+
if (typeof pdfFn === 'function') {
|
|
330
|
+
const res = await pdfFn(buffer)
|
|
331
|
+
rawText = res.text
|
|
332
|
+
} else if (pdfParseModule.PDFParse) {
|
|
333
|
+
const parser = new pdfParseModule.PDFParse({ data: buffer })
|
|
334
|
+
const res = await parser.getText()
|
|
335
|
+
rawText = res.text
|
|
336
|
+
}
|
|
337
|
+
} catch (pdfErr) {
|
|
338
|
+
return { success: false, error: `Gagal membaca PDF: ${pdfErr.message}` }
|
|
339
|
+
}
|
|
340
|
+
} else if (ext === '.docx') {
|
|
341
|
+
const buffer = fs.readFileSync(filePath)
|
|
342
|
+
try {
|
|
343
|
+
const mammoth = await import('mammoth')
|
|
344
|
+
const result = await (mammoth.default || mammoth).extractRawText({ buffer })
|
|
345
|
+
rawText = result.value
|
|
346
|
+
} catch (docxErr) {
|
|
347
|
+
return { success: false, error: `Gagal membaca DOCX: ${docxErr.message}` }
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
rawText = fs.readFileSync(filePath, 'utf8')
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let cleanText = rawText.replace(/\r\n/g, '\n').trim()
|
|
354
|
+
cleanText = cleanText.replace(/([^\n]{150,250})\s+/g, '$1\n')
|
|
355
|
+
const totalChars = cleanText.length
|
|
356
|
+
|
|
357
|
+
if (totalChars === 0) {
|
|
358
|
+
return { success: true, totalChars: 0, content: 'Dokumen kosong.' }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const allLines = cleanText.split('\n')
|
|
362
|
+
const totalLines = allLines.length
|
|
363
|
+
|
|
364
|
+
// MODE 1: Line Slicing
|
|
365
|
+
if (startLine !== null && endLine !== null && !isNaN(startLine) && !isNaN(endLine)) {
|
|
366
|
+
const s = Math.max(1, startLine)
|
|
367
|
+
const e = Math.min(totalLines, endLine)
|
|
368
|
+
const sliced = allLines
|
|
369
|
+
.slice(s - 1, e)
|
|
370
|
+
.map((line, idx) => `${s + idx}: ${line}`)
|
|
371
|
+
.join('\n')
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
success: true,
|
|
375
|
+
filePath,
|
|
376
|
+
totalLines,
|
|
377
|
+
startLine: s,
|
|
378
|
+
endLine: e,
|
|
379
|
+
content: `[RENTANG BARIS ${s} s/d ${e} DARI TOTAL ${totalLines} BARIS]:\n${sliced}`
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// MODE 2: Keyword Search
|
|
384
|
+
if (searchQuery) {
|
|
385
|
+
let resultsHeader = `[PENCARIAN PADA DOKUMEN: "${searchQuery}"]\n`
|
|
386
|
+
let matchedSections = []
|
|
387
|
+
|
|
388
|
+
const searchLower = searchQuery.toLowerCase()
|
|
389
|
+
for (let i = 0; i < allLines.length; i++) {
|
|
390
|
+
if (allLines[i].toLowerCase().includes(searchLower)) {
|
|
391
|
+
const ctxStart = Math.max(0, i - 2)
|
|
392
|
+
const ctxEnd = Math.min(allLines.length, i + 8)
|
|
393
|
+
const snippet = allLines
|
|
394
|
+
.slice(ctxStart, ctxEnd)
|
|
395
|
+
.map((l, idx) => `${ctxStart + idx + 1}: ${l}`)
|
|
396
|
+
.join('\n')
|
|
397
|
+
matchedSections.push(`[COCOK PERSIS PADA BARIS ${i + 1}]:\n${snippet}`)
|
|
398
|
+
if (matchedSections.length >= 4) break
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let combinedContent = ''
|
|
403
|
+
if (matchedSections.length > 0) {
|
|
404
|
+
combinedContent += `--- HASIL PENCOCOKAN KATAKUNCI PERSIS ---\n${matchedSections.join('\n\n')}\n\n`
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (combinedContent) {
|
|
408
|
+
return {
|
|
409
|
+
success: true,
|
|
410
|
+
filePath,
|
|
411
|
+
totalLines,
|
|
412
|
+
totalChars,
|
|
413
|
+
searchQuery,
|
|
414
|
+
content: resultsHeader + combinedContent
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
return {
|
|
418
|
+
success: true,
|
|
419
|
+
filePath,
|
|
420
|
+
totalLines,
|
|
421
|
+
totalChars,
|
|
422
|
+
searchQuery,
|
|
423
|
+
content: `Tidak ditemukan baris atau paragraf yang cocok dengan kata kunci "${searchQuery}".`
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// MODE 3: Default Overview Read
|
|
429
|
+
if (totalLines > 80) {
|
|
430
|
+
const firstBlock = allLines
|
|
431
|
+
.slice(0, 40)
|
|
432
|
+
.map((l, idx) => `${idx + 1}: ${l}`)
|
|
433
|
+
.join('\n')
|
|
434
|
+
|
|
435
|
+
const middleStart = 40
|
|
436
|
+
const middleEnd = Math.max(middleStart + 1, totalLines - 30)
|
|
437
|
+
|
|
438
|
+
const structuralHeadings = []
|
|
439
|
+
for (let i = middleStart; i < middleEnd; i++) {
|
|
440
|
+
const line = allLines[i].trim()
|
|
441
|
+
if (!line) continue
|
|
442
|
+
|
|
443
|
+
const isMdHeading = /^#{1,6}\s+/.test(line) || /^<h[1-6]>/i.test(line)
|
|
444
|
+
const isNumberedSection = /^([0-9]+\.[0-9.]*|[A-Z]\.|[IVXLCDM]+\.)\s+[A-Z0-9]/i.test(
|
|
445
|
+
line
|
|
446
|
+
)
|
|
447
|
+
const isTitleStyle =
|
|
448
|
+
line.length > 3 &&
|
|
449
|
+
line.length < 65 &&
|
|
450
|
+
((line === line.toUpperCase() && /[A-Z]/.test(line)) || line.endsWith(':'))
|
|
451
|
+
|
|
452
|
+
if (isMdHeading || isNumberedSection || isTitleStyle) {
|
|
453
|
+
const snippetEnd = Math.min(totalLines, i + 3)
|
|
454
|
+
const snippetText = allLines
|
|
455
|
+
.slice(i, snippetEnd)
|
|
456
|
+
.map((l, idx) => `${i + idx + 1}: ${l}`)
|
|
457
|
+
.join('\n')
|
|
458
|
+
structuralHeadings.push(`[HEADING BARIS ${i + 1}]:\n${snippetText}`)
|
|
459
|
+
if (structuralHeadings.length >= 12) break
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const sampledBody = []
|
|
464
|
+
if (structuralHeadings.length < 4) {
|
|
465
|
+
const middleTotal = middleEnd - middleStart
|
|
466
|
+
const numSamples = 8
|
|
467
|
+
const stepSize = Math.max(1, Math.floor(middleTotal / numSamples))
|
|
468
|
+
|
|
469
|
+
for (let i = 0; i < numSamples; i++) {
|
|
470
|
+
const targetLineIdx = middleStart + Math.min(i * stepSize, middleTotal - 1)
|
|
471
|
+
const snippetStart = targetLineIdx
|
|
472
|
+
const snippetEnd = Math.min(totalLines, snippetStart + 3)
|
|
473
|
+
const snippetText = allLines
|
|
474
|
+
.slice(snippetStart, snippetEnd)
|
|
475
|
+
.map((l, idx) => `${snippetStart + idx + 1}: ${l}`)
|
|
476
|
+
.join('\n')
|
|
477
|
+
sampledBody.push(`[CUPLIKAN INTERVAL BARIS ${snippetStart + 1}]:\n${snippetText}`)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const lastStart = Math.max(40, totalLines - 30)
|
|
482
|
+
const lastBlock = allLines
|
|
483
|
+
.slice(lastStart)
|
|
484
|
+
.map((l, idx) => `${lastStart + idx + 1}: ${l}`)
|
|
485
|
+
.join('\n')
|
|
486
|
+
|
|
487
|
+
let summaryContent = `[RINGKASAN STRUKTUR DOKUMEN: Total ${totalLines} baris / ${totalChars} karakter]\n\n`
|
|
488
|
+
summaryContent += `--- BAGIAN AWAL (BARIS 1 - 40) ---\n${firstBlock}\n\n`
|
|
489
|
+
|
|
490
|
+
if (structuralHeadings.length > 0) {
|
|
491
|
+
summaryContent += `--- STRUKTUR BAB & HEADINGS UTAMA DOKUMEN ---\n${structuralHeadings.join('\n\n')}\n\n`
|
|
492
|
+
}
|
|
493
|
+
if (sampledBody.length > 0) {
|
|
494
|
+
summaryContent += `--- CUPLIKAN INTERVAL DARI SELURUH ISI DOKUMEN ---\n${sampledBody.join('\n\n')}\n\n`
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
summaryContent += `--- BAGIAN AKHIR / KESIMPULAN (BARIS ${lastStart + 1} - ${totalLines}) ---\n${lastBlock}\n\n`
|
|
498
|
+
summaryContent += `[PERINTAH KELENGKAPAN SELESAI]: INFORMASI DI ATAS SUDAH MENCAKUP AWAL, TENGAH, DAN AKHIR DOKUMEN! JANGAN MEMBACA ULANG POTONGAN BARIS! BILA TUGASMU MEMBUAT FILE (.md/.txt), LANGSUNG PANGGIL 'write-file' SEKARANG JUGA!`
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
success: true,
|
|
502
|
+
filePath,
|
|
503
|
+
totalLines,
|
|
504
|
+
totalChars,
|
|
505
|
+
content: summaryContent
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
success: true,
|
|
511
|
+
filePath,
|
|
512
|
+
totalLines,
|
|
513
|
+
totalChars,
|
|
514
|
+
content: allLines.map((l, idx) => `${idx + 1}: ${l}`).join('\n')
|
|
515
|
+
}
|
|
516
|
+
} catch (e) {
|
|
517
|
+
return { success: false, error: e.message }
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
'write-file': {
|
|
523
|
+
needsApproval: true,
|
|
524
|
+
approvalMessage: (args) => {
|
|
525
|
+
const p =
|
|
526
|
+
typeof args === 'object' && args !== null ? args.path : String(args || '').split('||')[0]
|
|
527
|
+
return `Mark ingin menulis/membuat file:\n${(p || '').trim()}`
|
|
528
|
+
},
|
|
529
|
+
handler: async (args, config) => {
|
|
530
|
+
try {
|
|
531
|
+
let filePath = ''
|
|
532
|
+
let content = ''
|
|
533
|
+
|
|
534
|
+
if (typeof args === 'object' && args !== null) {
|
|
535
|
+
filePath = (args.path || '').trim()
|
|
536
|
+
content =
|
|
537
|
+
typeof args.content === 'string'
|
|
538
|
+
? args.content
|
|
539
|
+
: JSON.stringify(args.content ?? '', null, 2)
|
|
540
|
+
} else {
|
|
541
|
+
const parts = String(args || '').split('||')
|
|
542
|
+
if (parts.length < 2) {
|
|
543
|
+
return {
|
|
544
|
+
success: false,
|
|
545
|
+
message: 'Argumen write-file tidak lengkap (memerlukan path dan content).'
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
filePath = parts[0].trim()
|
|
549
|
+
content = parts.slice(1).join('||')
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (!filePath) {
|
|
553
|
+
return { success: false, message: 'Path file tidak boleh kosong.' }
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const activeRoot =
|
|
557
|
+
config?.workspaceRoot || path.join(os.homedir(), 'Documents', 'Mark Workspace')
|
|
558
|
+
if (!path.isAbsolute(filePath)) {
|
|
559
|
+
filePath = path.join(activeRoot, filePath)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const dir = path.dirname(filePath)
|
|
563
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
564
|
+
|
|
565
|
+
await fs.promises.writeFile(filePath, content, 'utf8')
|
|
566
|
+
|
|
567
|
+
// Validasi sintaks otomatis (Self-Healing Hook)
|
|
568
|
+
const syntaxCheck = await validateFileSyntax(filePath, content)
|
|
569
|
+
if (!syntaxCheck.valid) {
|
|
570
|
+
return {
|
|
571
|
+
success: true,
|
|
572
|
+
warning: 'FILE_CREATED_WITH_SYNTAX_ERROR',
|
|
573
|
+
message: `File berhasil disimpan ke ${filePath}, TETAPI terdeteksi SYNTAX ERROR:\n${syntaxCheck.error}\nKamu WAJIB segera memperbaiki error ini sekarang!`,
|
|
574
|
+
syntaxError: syntaxCheck.error
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
success: true,
|
|
580
|
+
message: `Berhasil menyimpan file ke ${filePath} tanpa error sintaks.`
|
|
581
|
+
}
|
|
582
|
+
} catch (e) {
|
|
583
|
+
return { success: false, error: e.message }
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
|
|
588
|
+
'replace-content': {
|
|
589
|
+
needsApproval: true,
|
|
590
|
+
approvalMessage: (args) => {
|
|
591
|
+
const p =
|
|
592
|
+
typeof args === 'object' && args !== null ? args.path : String(args || '').split('||')[0]
|
|
593
|
+
return `Mark ingin mengedit isi kode pada berkas:\n${(p || '').trim()}`
|
|
594
|
+
},
|
|
595
|
+
handler: async (args, config) => {
|
|
596
|
+
try {
|
|
597
|
+
let filePath = ''
|
|
598
|
+
let targetContent = ''
|
|
599
|
+
let replacementContent = ''
|
|
600
|
+
|
|
601
|
+
if (typeof args === 'object' && args !== null) {
|
|
602
|
+
filePath = (args.path || '').trim()
|
|
603
|
+
targetContent = args.target_content ?? ''
|
|
604
|
+
replacementContent = args.replacement_content ?? ''
|
|
605
|
+
} else {
|
|
606
|
+
const parts = String(args || '').split('||')
|
|
607
|
+
if (parts.length < 3) {
|
|
608
|
+
return {
|
|
609
|
+
success: false,
|
|
610
|
+
message: 'Format salah. Memerlukan path, target_content, dan replacement_content.'
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
filePath = parts[0].trim()
|
|
614
|
+
targetContent = parts[1]
|
|
615
|
+
replacementContent = parts.slice(2).join('||')
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const activeRoot =
|
|
619
|
+
config?.workspaceRoot || path.join(os.homedir(), 'Documents', 'Mark Workspace')
|
|
620
|
+
if (!path.isAbsolute(filePath)) {
|
|
621
|
+
filePath = path.join(activeRoot, filePath)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (!fs.existsSync(filePath)) {
|
|
625
|
+
return { success: false, message: `File tidak ditemukan di path: ${filePath}` }
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
let fileContent = await fs.promises.readFile(filePath, 'utf8')
|
|
629
|
+
|
|
630
|
+
const occurrences = fileContent.split(targetContent).length - 1
|
|
631
|
+
if (occurrences === 0) {
|
|
632
|
+
return {
|
|
633
|
+
success: false,
|
|
634
|
+
message: `targetContent tidak ditemukan di dalam berkas. Pastikan karakter/spasi sama persis. Disarankan memanggil 'read-file' terlebih dahulu.`
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (occurrences > 1) {
|
|
639
|
+
return {
|
|
640
|
+
success: false,
|
|
641
|
+
message: `targetContent ditemukan sebanyak ${occurrences} kali (tidak unik). Sertakan beberapa baris kode sebelum/sesudahnya agar targetContent menjadi unik.`
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const updatedContent = fileContent.replace(targetContent, replacementContent)
|
|
646
|
+
await fs.promises.writeFile(filePath, updatedContent, 'utf8')
|
|
647
|
+
|
|
648
|
+
// Validasi sintaks otomatis (Self-Healing Hook)
|
|
649
|
+
const syntaxCheck = await validateFileSyntax(filePath, updatedContent)
|
|
650
|
+
if (!syntaxCheck.valid) {
|
|
651
|
+
return {
|
|
652
|
+
success: true,
|
|
653
|
+
warning: 'FILE_UPDATED_WITH_SYNTAX_ERROR',
|
|
654
|
+
message: `File berhasil diubah, TETAPI terdeteksi SYNTAX ERROR:\n${syntaxCheck.error}\nKamu WAJIB segera memperbaiki error ini sekarang!`,
|
|
655
|
+
syntaxError: syntaxCheck.error
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
success: true,
|
|
661
|
+
message: `Berhasil mengganti konten pada ${path.basename(filePath)} tanpa error sintaks.`
|
|
662
|
+
}
|
|
663
|
+
} catch (e) {
|
|
664
|
+
return { success: false, error: e.message }
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
|
|
669
|
+
'replace-lines': {
|
|
670
|
+
needsApproval: true,
|
|
671
|
+
approvalMessage: (args) => {
|
|
672
|
+
if (typeof args === 'object' && args !== null) {
|
|
673
|
+
return `Mark ingin mengganti baris ${args.start_line} hingga ${args.end_line} di file:\n${(args.path || '').trim()}`
|
|
674
|
+
}
|
|
675
|
+
const parts = String(args || '').split('||')
|
|
676
|
+
return `Mark ingin mengganti baris ${parts[1]} hingga ${parts[2]} di file:\n${parts[0].trim()}`
|
|
677
|
+
},
|
|
678
|
+
handler: async (args, config) => {
|
|
679
|
+
try {
|
|
680
|
+
let filePath = ''
|
|
681
|
+
let startLine = 0
|
|
682
|
+
let endLine = 0
|
|
683
|
+
let newContent = ''
|
|
684
|
+
|
|
685
|
+
if (typeof args === 'object' && args !== null) {
|
|
686
|
+
filePath = (args.path || '').trim()
|
|
687
|
+
startLine = parseInt(args.start_line, 10)
|
|
688
|
+
endLine = parseInt(args.end_line, 10)
|
|
689
|
+
newContent = args.new_code !== undefined ? args.new_code : args.content || ''
|
|
690
|
+
} else {
|
|
691
|
+
const parts = String(args || '').split('||')
|
|
692
|
+
if (parts.length < 4) {
|
|
693
|
+
return {
|
|
694
|
+
success: false,
|
|
695
|
+
message: 'Format salah. Memerlukan path, start_line, end_line, dan new_code.'
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
filePath = parts[0].trim()
|
|
699
|
+
startLine = parseInt(parts[1].trim(), 10)
|
|
700
|
+
endLine = parseInt(parts[2].trim(), 10)
|
|
701
|
+
newContent = parts.slice(3).join('||')
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const activeRoot =
|
|
705
|
+
config?.workspaceRoot || path.join(os.homedir(), 'Documents', 'Mark Workspace')
|
|
706
|
+
if (!path.isAbsolute(filePath)) {
|
|
707
|
+
filePath = path.join(activeRoot, filePath)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (!fs.existsSync(filePath))
|
|
711
|
+
return { success: false, message: `File tidak ditemukan di path: ${filePath}` }
|
|
712
|
+
|
|
713
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
714
|
+
const lines = content.split('\n')
|
|
715
|
+
|
|
716
|
+
if (startLine < 1 || startLine > lines.length || endLine < startLine) {
|
|
717
|
+
return { success: false, message: 'Range baris tidak valid' }
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
lines.splice(startLine - 1, endLine - startLine + 1, newContent)
|
|
721
|
+
|
|
722
|
+
fs.writeFileSync(filePath, lines.join('\n'), 'utf8')
|
|
723
|
+
return {
|
|
724
|
+
success: true,
|
|
725
|
+
message: `Berhasil mengganti baris ${startLine}-${endLine} di ${filePath}`
|
|
726
|
+
}
|
|
727
|
+
} catch (e) {
|
|
728
|
+
return { success: false, error: e.message }
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
|
|
733
|
+
'delete-file': {
|
|
734
|
+
needsApproval: true,
|
|
735
|
+
approvalMessage: (args) => {
|
|
736
|
+
const p = typeof args === 'object' && args !== null ? args.path : String(args || '')
|
|
737
|
+
return `Mark ingin MENGHAPUS file secara permanen:\n${(p || '').trim()}`
|
|
738
|
+
},
|
|
739
|
+
handler: async (args, config) => {
|
|
740
|
+
try {
|
|
741
|
+
let filePath = (
|
|
742
|
+
typeof args === 'object' && args !== null ? args.path : String(args || '')
|
|
743
|
+
).trim()
|
|
744
|
+
const activeRoot =
|
|
745
|
+
config?.workspaceRoot || path.join(os.homedir(), 'Documents', 'Mark Workspace')
|
|
746
|
+
if (!path.isAbsolute(filePath)) {
|
|
747
|
+
filePath = path.join(activeRoot, filePath)
|
|
748
|
+
}
|
|
749
|
+
if (!fs.existsSync(filePath))
|
|
750
|
+
return { success: false, message: `File tidak ditemukan di path: ${filePath}` }
|
|
751
|
+
fs.unlinkSync(filePath)
|
|
752
|
+
return { success: true, message: `Berhasil menghapus file ${filePath}` }
|
|
753
|
+
} catch (e) {
|
|
754
|
+
return { success: false, error: e.message }
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
|
|
759
|
+
'list-dir': {
|
|
760
|
+
needsApproval: false,
|
|
761
|
+
handler: async (args, config) => {
|
|
762
|
+
try {
|
|
763
|
+
let targetDir = (
|
|
764
|
+
typeof args === 'object' && args !== null ? args.path || '' : String(args || '')
|
|
765
|
+
).trim()
|
|
766
|
+
const activeRoot =
|
|
767
|
+
config?.workspaceRoot || path.join(os.homedir(), 'Documents', 'Mark Workspace')
|
|
768
|
+
if (!path.isAbsolute(targetDir)) {
|
|
769
|
+
targetDir = targetDir ? path.join(activeRoot, targetDir) : activeRoot
|
|
770
|
+
}
|
|
771
|
+
if (!fs.existsSync(targetDir))
|
|
772
|
+
return { success: false, message: `Folder tidak ditemukan di path: ${targetDir}` }
|
|
773
|
+
const files = fs.readdirSync(targetDir)
|
|
774
|
+
return { success: true, total_files: files.length, contents: files.join('\n') }
|
|
775
|
+
} catch (e) {
|
|
776
|
+
return { success: false, error: e.message }
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
|
|
781
|
+
'find-files': {
|
|
782
|
+
needsApproval: false,
|
|
783
|
+
handler: async (args, config) => {
|
|
784
|
+
try {
|
|
785
|
+
let pattern = '*'
|
|
786
|
+
let subDir = ''
|
|
787
|
+
|
|
788
|
+
if (typeof args === 'object' && args !== null) {
|
|
789
|
+
pattern = args.pattern || '*'
|
|
790
|
+
subDir = args.subfolder || args.path || ''
|
|
791
|
+
} else {
|
|
792
|
+
const parts = String(args || '').split('||')
|
|
793
|
+
pattern = parts[0]?.trim() || '*'
|
|
794
|
+
subDir = parts[1]?.trim() || ''
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const activeRoot =
|
|
798
|
+
config?.workspaceRoot || path.join(os.homedir(), 'Documents', 'Mark Workspace')
|
|
799
|
+
const targetDir = path.isAbsolute(subDir)
|
|
800
|
+
? subDir
|
|
801
|
+
: subDir
|
|
802
|
+
? path.join(activeRoot, subDir)
|
|
803
|
+
: activeRoot
|
|
804
|
+
|
|
805
|
+
if (!fs.existsSync(targetDir)) {
|
|
806
|
+
return { success: false, message: `Direktori tidak ditemukan: ${targetDir}` }
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const IGNORED_DIRS = new Set([
|
|
810
|
+
'node_modules',
|
|
811
|
+
'.git',
|
|
812
|
+
'dist',
|
|
813
|
+
'build',
|
|
814
|
+
'.next',
|
|
815
|
+
'.output',
|
|
816
|
+
'out',
|
|
817
|
+
'.vscode',
|
|
818
|
+
'.idea',
|
|
819
|
+
'coverage',
|
|
820
|
+
'target',
|
|
821
|
+
'vendor'
|
|
822
|
+
])
|
|
823
|
+
|
|
824
|
+
const matchedFiles = []
|
|
825
|
+
const MAX_MATCHES = 80
|
|
826
|
+
|
|
827
|
+
function scan(dir, relativePrefix = '') {
|
|
828
|
+
if (matchedFiles.length >= MAX_MATCHES) return
|
|
829
|
+
|
|
830
|
+
let entries = []
|
|
831
|
+
try {
|
|
832
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
833
|
+
} catch (readErr) {
|
|
834
|
+
return
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
for (const entry of entries) {
|
|
838
|
+
if (matchedFiles.length >= MAX_MATCHES) break
|
|
839
|
+
|
|
840
|
+
const relPath = relativePrefix ? `${relativePrefix}/${entry.name}` : entry.name
|
|
841
|
+
|
|
842
|
+
if (entry.isDirectory()) {
|
|
843
|
+
if (!IGNORED_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
|
844
|
+
scan(path.join(dir, entry.name), relPath)
|
|
845
|
+
}
|
|
846
|
+
} else {
|
|
847
|
+
const cleanPattern = pattern.toLowerCase().replace(/\*/g, '')
|
|
848
|
+
if (pattern === '*' || relPath.toLowerCase().includes(cleanPattern)) {
|
|
849
|
+
matchedFiles.push(relPath)
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
scan(targetDir)
|
|
856
|
+
|
|
857
|
+
return {
|
|
858
|
+
success: true,
|
|
859
|
+
total: matchedFiles.length,
|
|
860
|
+
files: matchedFiles,
|
|
861
|
+
result:
|
|
862
|
+
matchedFiles.length > 0
|
|
863
|
+
? `Ditemukan ${matchedFiles.length} berkas di '${path.basename(targetDir)}':\n${matchedFiles.map((f) => `- ${f}`).join('\n')}`
|
|
864
|
+
: `Tidak ditemukan berkas yang cocok dengan pola "${pattern}" di folder tersebut.`
|
|
865
|
+
}
|
|
866
|
+
} catch (e) {
|
|
867
|
+
return { success: false, error: e.message }
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
},
|
|
871
|
+
|
|
872
|
+
'grep-search': {
|
|
873
|
+
needsApproval: false,
|
|
874
|
+
handler: async (args, config) => {
|
|
875
|
+
try {
|
|
876
|
+
let targetPath = ''
|
|
877
|
+
let keyword = ''
|
|
878
|
+
|
|
879
|
+
if (typeof args === 'object' && args !== null) {
|
|
880
|
+
targetPath = (args.path || args.file || args.filepath || args.target || '').trim()
|
|
881
|
+
keyword = (args.keyword || args.query || args.pattern || '').trim()
|
|
882
|
+
} else {
|
|
883
|
+
const rawStr = String(args || '').trim()
|
|
884
|
+
if (rawStr.includes('||')) {
|
|
885
|
+
const parts = rawStr.split('||')
|
|
886
|
+
targetPath = parts[0].trim()
|
|
887
|
+
keyword = parts.slice(1).join('||').trim()
|
|
888
|
+
} else {
|
|
889
|
+
// Jika hanya 1 string argumen tanpa pemisah, anggap sebagai keyword
|
|
890
|
+
keyword = rawStr
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (!keyword) {
|
|
895
|
+
return { success: false, message: 'Kata kunci pencarian tidak boleh kosong.' }
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const activeRoot =
|
|
899
|
+
config?.workspaceRoot || path.join(os.homedir(), 'Documents', 'Mark Workspace')
|
|
900
|
+
if (!targetPath) {
|
|
901
|
+
targetPath = activeRoot
|
|
902
|
+
} else if (!path.isAbsolute(targetPath)) {
|
|
903
|
+
targetPath = path.join(activeRoot, targetPath)
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (!fs.existsSync(targetPath)) {
|
|
907
|
+
return { success: false, message: `Path tidak ditemukan: ${targetPath}` }
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const stat = await fs.promises.stat(targetPath)
|
|
911
|
+
const matches = []
|
|
912
|
+
const lowerKeyword = keyword.toLowerCase()
|
|
913
|
+
|
|
914
|
+
// SKENARIO 1: Pencarian pada SATU BERKAS SPESIFIK
|
|
915
|
+
if (stat.isFile()) {
|
|
916
|
+
const content = await fs.promises.readFile(targetPath, 'utf8')
|
|
917
|
+
const lines = content.split('\n')
|
|
918
|
+
const fileName = path.basename(targetPath)
|
|
919
|
+
|
|
920
|
+
for (let i = 0; i < lines.length; i++) {
|
|
921
|
+
if (lines[i].toLowerCase().includes(lowerKeyword)) {
|
|
922
|
+
matches.push(`${fileName}:${i + 1}: ${lines[i].trim()}`)
|
|
923
|
+
if (matches.length >= 60) break
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (matches.length === 0) {
|
|
928
|
+
return {
|
|
929
|
+
success: true,
|
|
930
|
+
total_matches: 0,
|
|
931
|
+
result: `Tidak ditemukan baris yang cocok dengan "${keyword}" di dalam berkas ${fileName}.`
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
return {
|
|
936
|
+
success: true,
|
|
937
|
+
total_matches: matches.length,
|
|
938
|
+
result: `Ditemukan ${matches.length} kecocokan di ${fileName}:\n${matches.join('\n')}`
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// SKENARIO 2: Pencarian rekursif pada DIREKTORI
|
|
943
|
+
const IGNORED_GREP_DIRS = new Set([
|
|
944
|
+
'node_modules',
|
|
945
|
+
'.git',
|
|
946
|
+
'dist',
|
|
947
|
+
'build',
|
|
948
|
+
'.next',
|
|
949
|
+
'.vite',
|
|
950
|
+
'.nuxt',
|
|
951
|
+
'coverage',
|
|
952
|
+
'.cache',
|
|
953
|
+
'out',
|
|
954
|
+
'.idea',
|
|
955
|
+
'.vscode',
|
|
956
|
+
'target',
|
|
957
|
+
'bin',
|
|
958
|
+
'obj'
|
|
959
|
+
])
|
|
960
|
+
|
|
961
|
+
const TEXT_EXTENSIONS = new Set([
|
|
962
|
+
'.js',
|
|
963
|
+
'.jsx',
|
|
964
|
+
'.ts',
|
|
965
|
+
'.tsx',
|
|
966
|
+
'.mjs',
|
|
967
|
+
'.cjs',
|
|
968
|
+
'.json',
|
|
969
|
+
'.html',
|
|
970
|
+
'.htm',
|
|
971
|
+
'.css',
|
|
972
|
+
'.scss',
|
|
973
|
+
'.less',
|
|
974
|
+
'.py',
|
|
975
|
+
'.md',
|
|
976
|
+
'.markdown',
|
|
977
|
+
'.txt',
|
|
978
|
+
'.rs',
|
|
979
|
+
'.go',
|
|
980
|
+
'.java',
|
|
981
|
+
'.c',
|
|
982
|
+
'.cpp',
|
|
983
|
+
'.h',
|
|
984
|
+
'.hpp',
|
|
985
|
+
'.cs',
|
|
986
|
+
'.sh',
|
|
987
|
+
'.ps1',
|
|
988
|
+
'.bat',
|
|
989
|
+
'.cmd',
|
|
990
|
+
'.yml',
|
|
991
|
+
'.yaml',
|
|
992
|
+
'.xml',
|
|
993
|
+
'.env',
|
|
994
|
+
'.sql',
|
|
995
|
+
'.toml',
|
|
996
|
+
'.ini',
|
|
997
|
+
'.cfg',
|
|
998
|
+
'.vue',
|
|
999
|
+
'.svelte'
|
|
1000
|
+
])
|
|
1001
|
+
|
|
1002
|
+
async function walk(dir) {
|
|
1003
|
+
if (matches.length >= 60) return
|
|
1004
|
+
|
|
1005
|
+
let entries
|
|
1006
|
+
try {
|
|
1007
|
+
entries = await fs.promises.readdir(dir, { withFileTypes: true })
|
|
1008
|
+
} catch (_) {
|
|
1009
|
+
return
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
for (const entry of entries) {
|
|
1013
|
+
if (matches.length >= 60) break
|
|
1014
|
+
|
|
1015
|
+
const fullPath = path.join(dir, entry.name)
|
|
1016
|
+
|
|
1017
|
+
if (entry.isDirectory()) {
|
|
1018
|
+
if (!IGNORED_GREP_DIRS.has(entry.name.toLowerCase())) {
|
|
1019
|
+
await walk(fullPath)
|
|
1020
|
+
}
|
|
1021
|
+
} else if (entry.isFile()) {
|
|
1022
|
+
const ext = path.extname(entry.name).toLowerCase()
|
|
1023
|
+
if (TEXT_EXTENSIONS.has(ext) || !ext || entry.name.startsWith('.')) {
|
|
1024
|
+
try {
|
|
1025
|
+
const fileStat = await fs.promises.stat(fullPath)
|
|
1026
|
+
if (fileStat.size > 2 * 1024 * 1024) continue
|
|
1027
|
+
|
|
1028
|
+
const content = await fs.promises.readFile(fullPath, 'utf8')
|
|
1029
|
+
if (content.toLowerCase().includes(lowerKeyword)) {
|
|
1030
|
+
const lines = content.split('\n')
|
|
1031
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1032
|
+
if (lines[i].toLowerCase().includes(lowerKeyword)) {
|
|
1033
|
+
const relPath = path.relative(targetPath, fullPath)
|
|
1034
|
+
matches.push(`${relPath}:${i + 1}: ${lines[i].trim()}`)
|
|
1035
|
+
if (matches.length >= 60) break
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
} catch (_) {}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
await walk(targetPath)
|
|
1046
|
+
|
|
1047
|
+
if (matches.length === 0) {
|
|
1048
|
+
return {
|
|
1049
|
+
success: true,
|
|
1050
|
+
total_matches: 0,
|
|
1051
|
+
result: `Pencarian "${keyword}" tidak menemukan hasil di ${path.basename(targetPath)}.`
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
return {
|
|
1056
|
+
success: true,
|
|
1057
|
+
total_matches: matches.length,
|
|
1058
|
+
result: `Ditemukan ${matches.length} kecocokan di ${path.basename(targetPath)}:\n${matches.join('\n')}`
|
|
1059
|
+
}
|
|
1060
|
+
} catch (e) {
|
|
1061
|
+
return { success: false, error: e.message }
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
|
|
1066
|
+
'select-directory': {
|
|
1067
|
+
needsApproval: false,
|
|
1068
|
+
handler: async (args) => {
|
|
1069
|
+
try {
|
|
1070
|
+
const descText =
|
|
1071
|
+
typeof args === 'object' && args !== null
|
|
1072
|
+
? args.description || 'Pilih Folder Workspace Proyek'
|
|
1073
|
+
: String(args || 'Pilih Folder Workspace Proyek')
|
|
1074
|
+
const safeDesc = descText.replace(/['`"\\]/g, ' ')
|
|
1075
|
+
|
|
1076
|
+
const scriptPath = path.resolve(__dirname, '../pc-agent-scripts/pick-folder.ps1')
|
|
1077
|
+
let selectedPath = ''
|
|
1078
|
+
|
|
1079
|
+
if (fs.existsSync(scriptPath)) {
|
|
1080
|
+
const { stdout } = await execPromise(
|
|
1081
|
+
`powershell.exe -NoProfile -STA -ExecutionPolicy Bypass -File "${scriptPath}" -Title "${safeDesc}"`
|
|
1082
|
+
)
|
|
1083
|
+
selectedPath = stdout.trim()
|
|
1084
|
+
} else {
|
|
1085
|
+
// Fallback via inline script jika file tidak ditemukan
|
|
1086
|
+
const inlinePs = `Add-Type -AssemblyName System.Windows.Forms; $f = New-Object System.Windows.Forms.FolderBrowserDialog; $f.Description = '${safeDesc}'; if ($f.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { [Console]::Out.Write($f.SelectedPath) }`
|
|
1087
|
+
const enc = Buffer.from(inlinePs, 'utf16le').toString('base64')
|
|
1088
|
+
const { stdout } = await execPromise(
|
|
1089
|
+
`powershell.exe -NoProfile -STA -ExecutionPolicy Bypass -EncodedCommand ${enc}`
|
|
1090
|
+
)
|
|
1091
|
+
selectedPath = stdout.trim()
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
return {
|
|
1095
|
+
success: true,
|
|
1096
|
+
path: selectedPath || null,
|
|
1097
|
+
data: selectedPath || null
|
|
1098
|
+
}
|
|
1099
|
+
} catch (e) {
|
|
1100
|
+
return { success: false, error: e.message }
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
|
|
1105
|
+
'open-folder': {
|
|
1106
|
+
needsApproval: false,
|
|
1107
|
+
handler: async (args, config) => {
|
|
1108
|
+
try {
|
|
1109
|
+
let targetPath =
|
|
1110
|
+
typeof args === 'object' && args !== null ? args.path || '' : String(args || '').trim()
|
|
1111
|
+
const activeRoot =
|
|
1112
|
+
config?.workspaceRoot || path.join(os.homedir(), 'Documents', 'Mark Workspace')
|
|
1113
|
+
if (!targetPath) targetPath = activeRoot
|
|
1114
|
+
else if (!path.isAbsolute(targetPath)) targetPath = path.join(activeRoot, targetPath)
|
|
1115
|
+
targetPath = path.normalize(targetPath)
|
|
1116
|
+
if (!fs.existsSync(targetPath)) {
|
|
1117
|
+
fs.mkdirSync(targetPath, { recursive: true })
|
|
1118
|
+
}
|
|
1119
|
+
await execPromise(`explorer.exe "${targetPath}"`)
|
|
1120
|
+
return { success: true, message: `Folder dibuka: ${targetPath}` }
|
|
1121
|
+
} catch (e) {
|
|
1122
|
+
return { success: false, error: e.message }
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|