natureco-cli 5.1.2 → 5.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "natureco-cli",
3
- "version": "5.1.2",
3
+ "version": "5.2.0",
4
4
  "description": "OpenClaw'dan daha güvenli, daha hızlı, daha ucuz AI agent CLI. Multi-agent, self-evolving skills, audit log, maliyet optimizasyonu ve NatureCo platform-native.",
5
5
  "bin": {
6
6
  "natureco": "bin/natureco.js"
@@ -78,11 +78,41 @@ async function createCron({ name, schedule, command, description = "" }) {
78
78
  }
79
79
  } catch {}
80
80
 
81
+ // v5.2.0: Gercek macOS crontab'a ekle (kullanici crontab, sudo gerekmez)
82
+ const marker = `# natureco:${name}`;
83
+ let crontabUpdated = false;
84
+ let crontabError = null;
85
+
86
+ try {
87
+ const { execSync } = require("child_process");
88
+ // Mevcut crontab'i oku
89
+ let existing = "";
90
+ try { existing = execSync("crontab -l 2>/dev/null", { encoding: "utf8" }); }
91
+ catch { existing = ""; }
92
+
93
+ // Ayni isimde var mi kontrol
94
+ if (existing.includes(marker)) {
95
+ crontabUpdated = false;
96
+ } else {
97
+ const newLine = `${resolvedSchedule} ${command} ${marker}`;
98
+ const updated = existing + (existing.endsWith("\n") || existing === "" ? "" : "\n") + newLine + "\n";
99
+ // Yeni crontab yukle (heredoc yerine - ile stdin)
100
+ execSync("crontab -", { input: updated, encoding: "utf8" });
101
+ crontabUpdated = true;
102
+ }
103
+ } catch (e) {
104
+ crontabError = e.message;
105
+ }
106
+
81
107
  return {
82
108
  success: true,
83
109
  cron: newCron,
84
- message: `Cron olusturuldu: ${name} (${resolvedSchedule})`,
85
- hint: systemCrontabInstalled ? undefined : `Sistem crontab'a eklemek icin: crontab -e ve ekleyin: ${resolvedSchedule} ${command}`,
110
+ message: crontabUpdated
111
+ ? `Cron olusturuldu VE macOS crontab'a eklendi: ${name} (${resolvedSchedule})`
112
+ : `Cron olusturuldu: ${name} (${resolvedSchedule})${crontabError ? ` - crontab eklenemedi: ${crontabError}` : ""}`,
113
+ crontabUpdated,
114
+ crontabError,
115
+ schedule: resolvedSchedule,
86
116
  };
87
117
  }
88
118
 
@@ -9,9 +9,31 @@ const { spawn } = require("child_process");
9
9
  const path = require("path");
10
10
  const os = require("os");
11
11
 
12
+ // v5.2.0: Agent alias mapping (Parton'un testinden — "review" diye bir agent yok)
13
+ const AGENT_ALIASES = {
14
+ "review": "general", // eskiden review diye bir vardi, simdi general
15
+ "analyze": "explore",
16
+ "scan": "security",
17
+ "audit": "security",
18
+ "check": "general",
19
+ };
20
+
21
+ const VALID_AGENTS = ["explore", "general", "seo", "content", "security", "debugger", "translator"];
22
+
12
23
  async function delegate({ task, agent = "general", timeoutMs = 60000 }) {
13
24
  if (!task) return { success: false, error: "task gerekli" };
14
25
 
26
+ // Alias cozumu
27
+ if (AGENT_ALIASES[agent]) {
28
+ agent = AGENT_ALIASES[agent];
29
+ }
30
+ if (!VALID_AGENTS.includes(agent)) {
31
+ return {
32
+ success: false,
33
+ error: `Gecersiz agent: ${agent}. Var olan agent tipleri: ${VALID_AGENTS.join(", ")}. Alias'lar: ${Object.keys(AGENT_ALIASES).join(", ")}`,
34
+ };
35
+ }
36
+
15
37
  // Bu basitlestirilmis versiyon: kendi agent sistemi yerine
16
38
  // natureco'nun REPL'ini sub-process olarak baslatip gorev verir
17
39
  return new Promise((resolve) => {
@@ -1,4 +1,5 @@
1
1
  const fs = require('fs');
2
+ const { expandPath } = require('../utils/paths');
2
3
  const path = require('path');
3
4
  const { execSync } = require('child_process');
4
5
 
@@ -16,7 +17,7 @@ module.exports = {
16
17
 
17
18
  async execute(params) {
18
19
  try {
19
- const filePath = path.resolve(params.path.replace(/^~/, require('os').homedir()));
20
+ const filePath = path.resolve(expandPath(params.path));
20
21
  const maxChars = params.maxChars || 50000;
21
22
 
22
23
  if (!fs.existsSync(filePath)) {
@@ -1,7 +1,7 @@
1
- const listDir = require('./list_dir');
2
-
3
- module.exports = {
4
- ...listDir,
5
- name: 'filesystem',
6
- description: 'List files and directories',
7
- };
1
+ const listDir = require('./list_dir');
2
+
3
+ module.exports = {
4
+ ...listDir,
5
+ name: 'filesystem',
6
+ description: 'List files and directories',
7
+ };
@@ -1,127 +1,127 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const os = require('os');
4
-
5
- /**
6
- * List Directory Tool
7
- * Lists files and directories in a given path
8
- */
9
-
10
- module.exports = {
11
- name: 'list_dir',
12
- description: 'List files and directories in a given path. Returns file names, sizes, and types.',
13
- inputSchema: {
14
- type: 'object',
15
- properties: {
16
- path: {
17
- type: 'string',
18
- description: 'Directory path to list (relative or absolute). Use "." for current directory.'
19
- }
20
- },
21
- required: ['path']
22
- },
23
-
24
- async execute(params) {
25
- try {
26
- // Expand ~ to home directory
27
- let dirPath = params.path || '.';
28
- dirPath = dirPath.replace(/^~/, os.homedir());
29
-
30
- // Fix /home path - replace with actual home directory (Unix only)
31
- // Handles: /home, home, /home/Documents, /home/anything
32
- if (process.platform !== 'win32') {
33
- if (dirPath === '/home' || dirPath === 'home' || dirPath.startsWith('/home/')) {
34
- dirPath = dirPath.replace(/^\/home/, os.homedir());
35
- }
36
- }
37
-
38
- const absolutePath = path.resolve(dirPath);
39
-
40
- // Check if directory exists
41
- if (!fs.existsSync(absolutePath)) {
42
- return {
43
- success: false,
44
- error: `Directory not found: ${params.path}`
45
- };
46
- }
47
-
48
- // Check if it's a directory
49
- const stats = fs.statSync(absolutePath);
50
- if (!stats.isDirectory()) {
51
- return {
52
- success: false,
53
- error: `Not a directory: ${params.path}`
54
- };
55
- }
56
-
57
- // Read directory
58
- const entries = fs.readdirSync(absolutePath, { withFileTypes: true });
59
-
60
- // Format entries
61
- const items = entries.map(entry => {
62
- const itemPath = path.join(absolutePath, entry.name);
63
- let size = 0;
64
- let type = 'unknown';
65
-
66
- try {
67
- const itemStats = fs.statSync(itemPath);
68
- size = itemStats.size;
69
-
70
- if (entry.isDirectory()) {
71
- type = 'directory';
72
- } else if (entry.isFile()) {
73
- type = 'file';
74
- } else if (entry.isSymbolicLink()) {
75
- type = 'symlink';
76
- }
77
- } catch (err) {
78
- // Ignore stat errors
79
- }
80
-
81
- return {
82
- name: entry.name,
83
- type: type,
84
- size: size
85
- };
86
- });
87
-
88
- // Sort: directories first, then files
89
- items.sort((a, b) => {
90
- if (a.type === 'directory' && b.type !== 'directory') return -1;
91
- if (a.type !== 'directory' && b.type === 'directory') return 1;
92
- return a.name.localeCompare(b.name);
93
- });
94
-
95
- // Format output
96
- let output = `Directory: ${absolutePath}\n`;
97
- output += `Total items: ${items.length}\n\n`;
98
-
99
- items.forEach(item => {
100
- const typeIcon = item.type === 'directory' ? '📁' : '📄';
101
- const sizeStr = item.type === 'file' ? ` (${formatSize(item.size)})` : '';
102
- output += `${typeIcon} ${item.name}${sizeStr}\n`;
103
- });
104
-
105
- return {
106
- success: true,
107
- output: output,
108
- items: items,
109
- path: absolutePath
110
- };
111
-
112
- } catch (error) {
113
- return {
114
- success: false,
115
- error: error.message
116
- };
117
- }
118
- }
119
- };
120
-
121
- function formatSize(bytes) {
122
- if (bytes === 0) return '0 B';
123
- const k = 1024;
124
- const sizes = ['B', 'KB', 'MB', 'GB'];
125
- const i = Math.floor(Math.log(bytes) / Math.log(k));
126
- return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
127
- }
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ /**
6
+ * List Directory Tool
7
+ * Lists files and directories in a given path
8
+ */
9
+
10
+ module.exports = {
11
+ name: 'list_dir',
12
+ description: 'List files and directories in a given path. Returns file names, sizes, and types.',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ path: {
17
+ type: 'string',
18
+ description: 'Directory path to list (relative or absolute). Use "." for current directory.'
19
+ }
20
+ },
21
+ required: ['path']
22
+ },
23
+
24
+ async execute(params) {
25
+ try {
26
+ // Expand ~ to home directory
27
+ let dirPath = params.path || '.';
28
+ dirPath = dirPath.replace(/^~/, os.homedir());
29
+
30
+ // Fix /home path - replace with actual home directory (Unix only)
31
+ // Handles: /home, home, /home/Documents, /home/anything
32
+ if (process.platform !== 'win32') {
33
+ if (dirPath === '/home' || dirPath === 'home' || dirPath.startsWith('/home/')) {
34
+ dirPath = dirPath.replace(/^\/home/, os.homedir());
35
+ }
36
+ }
37
+
38
+ const absolutePath = path.resolve(dirPath);
39
+
40
+ // Check if directory exists
41
+ if (!fs.existsSync(absolutePath)) {
42
+ return {
43
+ success: false,
44
+ error: `Directory not found: ${params.path}`
45
+ };
46
+ }
47
+
48
+ // Check if it's a directory
49
+ const stats = fs.statSync(absolutePath);
50
+ if (!stats.isDirectory()) {
51
+ return {
52
+ success: false,
53
+ error: `Not a directory: ${params.path}`
54
+ };
55
+ }
56
+
57
+ // Read directory
58
+ const entries = fs.readdirSync(absolutePath, { withFileTypes: true });
59
+
60
+ // Format entries
61
+ const items = entries.map(entry => {
62
+ const itemPath = path.join(absolutePath, entry.name);
63
+ let size = 0;
64
+ let type = 'unknown';
65
+
66
+ try {
67
+ const itemStats = fs.statSync(itemPath);
68
+ size = itemStats.size;
69
+
70
+ if (entry.isDirectory()) {
71
+ type = 'directory';
72
+ } else if (entry.isFile()) {
73
+ type = 'file';
74
+ } else if (entry.isSymbolicLink()) {
75
+ type = 'symlink';
76
+ }
77
+ } catch (err) {
78
+ // Ignore stat errors
79
+ }
80
+
81
+ return {
82
+ name: entry.name,
83
+ type: type,
84
+ size: size
85
+ };
86
+ });
87
+
88
+ // Sort: directories first, then files
89
+ items.sort((a, b) => {
90
+ if (a.type === 'directory' && b.type !== 'directory') return -1;
91
+ if (a.type !== 'directory' && b.type === 'directory') return 1;
92
+ return a.name.localeCompare(b.name);
93
+ });
94
+
95
+ // Format output
96
+ let output = `Directory: ${absolutePath}\n`;
97
+ output += `Total items: ${items.length}\n\n`;
98
+
99
+ items.forEach(item => {
100
+ const typeIcon = item.type === 'directory' ? '📁' : '📄';
101
+ const sizeStr = item.type === 'file' ? ` (${formatSize(item.size)})` : '';
102
+ output += `${typeIcon} ${item.name}${sizeStr}\n`;
103
+ });
104
+
105
+ return {
106
+ success: true,
107
+ output: output,
108
+ items: items,
109
+ path: absolutePath
110
+ };
111
+
112
+ } catch (error) {
113
+ return {
114
+ success: false,
115
+ error: error.message
116
+ };
117
+ }
118
+ }
119
+ };
120
+
121
+ function formatSize(bytes) {
122
+ if (bytes === 0) return '0 B';
123
+ const k = 1024;
124
+ const sizes = ['B', 'KB', 'MB', 'GB'];
125
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
126
+ return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
127
+ }
@@ -90,7 +90,20 @@ async function setAlarm({ time, label = "Alarm", calendarName = "Calendar" }) {
90
90
 
91
91
  // AppleScript ile macOS Calendar'a all-day etkinlik olarak alarm ekle
92
92
  // (Reminder ile ayni sonuc, daha guvenilir cunku Calendar otomasyon izni Reminders'dan once verilir)
93
- // 2 adim: Reminders'a alarm ekle (ana mekanizma) + Calendar'a event (gorunurluk icin)
93
+ // Dual: Calendar event (gorunurluk) + Reminders (bildirim + alarm)
94
+ // Once Calendar'a basit etkinlik (alarm'siz, ama gorunur), sonra Reminders alarm
95
+ const calendarScript = `
96
+ tell application "Calendar"
97
+ set targetCal to first calendar whose writable is true
98
+ set startDate to date "${parsed.formattedDate} ${parsed.formattedTime}"
99
+ set endDate to startDate + (1 * minutes)
100
+ set newEvent to make new event at end of events of targetCal with properties {summary:"⏰ ${label.replace(/"/g, "'")} - NatureCo", start date:startDate, end date:endDate, allday event:false}
101
+ save
102
+ return id of newEvent
103
+ end tell
104
+ `;
105
+
106
+ // Reminders'a bildirim + sesli alarm
94
107
  const reminderScript = `
95
108
  tell application "Reminders"
96
109
  set targetList to default list
@@ -102,11 +115,13 @@ async function setAlarm({ time, label = "Alarm", calendarName = "Calendar" }) {
102
115
  `;
103
116
 
104
117
  try {
105
- const eventId = await runAppleScript(reminderScript);
118
+ const calId = await runAppleScript(calendarScript);
119
+ const remId = await runAppleScript(reminderScript);
106
120
  return {
107
121
  success: true,
108
- eventId,
109
- message: `⏰ Alarm kuruldu: ${parsed.humanReadable} — "${label}"`,
122
+ eventId: remId,
123
+ calendarEventId: calId,
124
+ message: `⏰ Alarm kuruldu: ${parsed.humanReadable} — "${label}" (Calendar + Reminders)`,
110
125
  calendar: calendarName,
111
126
  targetTime: parsed.humanReadable,
112
127
  targetTimestamp: parsed.timestamp,
@@ -0,0 +1,87 @@
1
+ /**
2
+ * macos_screenshot - macOS native screenshot (v5.2.0)
3
+ *
4
+ * Playwright'a gerek kalmadan screencapture komutu ile
5
+ * ekran goruntusu alir. Sonra base64 olarak doner.
6
+ */
7
+
8
+ const { spawn } = require("child_process");
9
+ const os = require("os");
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+
13
+ const IS_MAC = os.platform() === "darwin";
14
+
15
+ async function captureScreen({ outputPath = null, region = "full" } = {}) {
16
+ if (!IS_MAC) return { success: false, error: "Sadece macOS" };
17
+
18
+ const tmpFile = outputPath || path.join(os.tmpdir(), `screenshot-${Date.now()}.png`);
19
+ return new Promise((resolve) => {
20
+ // screencapture -x: ses yok, -t png: png format
21
+ const proc = spawn("screencapture", ["-x", "-t", "png", tmpFile]);
22
+ proc.on("close", (code) => {
23
+ if (code === 0 && fs.existsSync(tmpFile)) {
24
+ const buffer = fs.readFileSync(tmpFile);
25
+ const stats = fs.statSync(tmpFile);
26
+ resolve({
27
+ success: true,
28
+ path: tmpFile,
29
+ size: stats.size,
30
+ mime: "image/png",
31
+ base64: buffer.toString("base64"),
32
+ message: `Screenshot kaydedildi: ${tmpFile}`,
33
+ });
34
+ } else {
35
+ resolve({ success: false, error: `screencapture exit ${code}` });
36
+ }
37
+ });
38
+ proc.on("error", (e) => resolve({ success: false, error: e.message }));
39
+ });
40
+ }
41
+
42
+ async function captureWindow({ windowTitle = null, outputPath = null } = {}) {
43
+ if (!IS_MAC) return { success: false, error: "Sadece macOS" };
44
+
45
+ const tmpFile = outputPath || path.join(os.tmpdir(), `screenshot-${Date.now()}.png`);
46
+ return new Promise((resolve) => {
47
+ // screencapture -l <windowId> veya -w (interaktif, fare ile pencere seç)
48
+ const args = ["-x", "-t", "png", "-o"]; // -o: shadow yok
49
+ if (windowTitle) args.push("-l", String(windowTitle));
50
+ args.push(tmpFile);
51
+
52
+ const proc = spawn("screencapture", args);
53
+ proc.on("close", (code) => {
54
+ if (code === 0 && fs.existsSync(tmpFile)) {
55
+ const buffer = fs.readFileSync(tmpFile);
56
+ const stats = fs.statSync(tmpFile);
57
+ resolve({
58
+ success: true,
59
+ path: tmpFile,
60
+ size: stats.size,
61
+ mime: "image/png",
62
+ base64: buffer.toString("base64"),
63
+ });
64
+ } else {
65
+ resolve({ success: false, error: `screencapture exit ${code}` });
66
+ }
67
+ });
68
+ proc.on("error", (e) => resolve({ success: false, error: e.message }));
69
+ });
70
+ }
71
+
72
+ module.exports = {
73
+ name: "macos_screenshot",
74
+ description: "macOS native ekran goruntusu al (Playwright gerekmez). screencapture komutunu kullanir.",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {
78
+ outputPath: { type: "string", description: "Kayit yolu (default: /tmp/screenshot-<ts>.png)" },
79
+ region: { type: "string", description: "full/window (default: full)", enum: ["full", "window"] },
80
+ windowTitle: { type: "string", description: "Pencere basligi (sadece region=window)" },
81
+ },
82
+ },
83
+ async execute(params) {
84
+ if (params.region === "window") return captureWindow(params);
85
+ return captureScreen(params);
86
+ },
87
+ };
@@ -1,75 +1,75 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const os = require('os');
4
-
5
- module.exports = {
6
- name: 'read_file',
7
- description: 'Read content from a file',
8
- inputSchema: {
9
- type: 'object',
10
- properties: {
11
- path: {
12
- type: 'string',
13
- description: 'File path to read'
14
- }
15
- },
16
- required: ['path']
17
- },
18
-
19
- async execute(params) {
20
- try {
21
- // Expand ~ to home directory
22
- let filePath = params.path.replace(/^~/, os.homedir());
23
- filePath = path.resolve(filePath);
24
-
25
- if (!fs.existsSync(filePath)) {
26
- return {
27
- success: false,
28
- error: 'File does not exist'
29
- };
30
- }
31
-
32
- const stats = fs.statSync(filePath);
33
-
34
- if (!stats.isFile()) {
35
- return {
36
- success: false,
37
- error: 'Path is not a file'
38
- };
39
- }
40
-
41
- // Check file size - if > 1MB, read only first 50KB
42
- if (stats.size > 1024 * 1024) {
43
- const fd = fs.openSync(filePath, 'r');
44
- const buf = Buffer.alloc(50000);
45
- fs.readSync(fd, buf, 0, 50000, 0);
46
- fs.closeSync(fd);
47
-
48
- const content = '[Büyük dosya - ilk ~50KB gösteriliyor]\n' + buf.toString('utf8');
49
-
50
- return {
51
- success: true,
52
- path: filePath,
53
- content,
54
- size: stats.size,
55
- truncated: true
56
- };
57
- }
58
-
59
- const content = fs.readFileSync(filePath, 'utf-8');
60
-
61
- return {
62
- success: true,
63
- path: filePath,
64
- content,
65
- size: stats.size,
66
- truncated: false
67
- };
68
- } catch (error) {
69
- return {
70
- success: false,
71
- error: error.message
72
- };
73
- }
74
- }
75
- };
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ module.exports = {
6
+ name: 'read_file',
7
+ description: 'Read content from a file',
8
+ inputSchema: {
9
+ type: 'object',
10
+ properties: {
11
+ path: {
12
+ type: 'string',
13
+ description: 'File path to read'
14
+ }
15
+ },
16
+ required: ['path']
17
+ },
18
+
19
+ async execute(params) {
20
+ try {
21
+ // v5.2.0: Robust path expansion (~, ~/, relative)
22
+ const { expandPath } = require('../utils/paths');
23
+ const filePath = expandPath(params.path);
24
+
25
+ if (!fs.existsSync(filePath)) {
26
+ return {
27
+ success: false,
28
+ error: 'File does not exist'
29
+ };
30
+ }
31
+
32
+ const stats = fs.statSync(filePath);
33
+
34
+ if (!stats.isFile()) {
35
+ return {
36
+ success: false,
37
+ error: 'Path is not a file'
38
+ };
39
+ }
40
+
41
+ // Check file size - if > 1MB, read only first 50KB
42
+ if (stats.size > 1024 * 1024) {
43
+ const fd = fs.openSync(filePath, 'r');
44
+ const buf = Buffer.alloc(50000);
45
+ fs.readSync(fd, buf, 0, 50000, 0);
46
+ fs.closeSync(fd);
47
+
48
+ const content = '[Büyük dosya - ilk ~50KB gösteriliyor]\n' + buf.toString('utf8');
49
+
50
+ return {
51
+ success: true,
52
+ path: filePath,
53
+ content,
54
+ size: stats.size,
55
+ truncated: true
56
+ };
57
+ }
58
+
59
+ const content = fs.readFileSync(filePath, 'utf-8');
60
+
61
+ return {
62
+ success: true,
63
+ path: filePath,
64
+ content,
65
+ size: stats.size,
66
+ truncated: false
67
+ };
68
+ } catch (error) {
69
+ return {
70
+ success: false,
71
+ error: error.message
72
+ };
73
+ }
74
+ }
75
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * paths.js — Path helper utilities (v5.2.0)
3
+ *
4
+ * Tum tool'larda ~/Desktop/test.txt gibi path'leri dogru handle etmek icin.
5
+ * Parton'un gercek testinde "File does not exist" bug'i duzeltildi.
6
+ */
7
+
8
+ const os = require("os");
9
+ const path = require("path");
10
+
11
+ /**
12
+ * Path'i normalize et ve ~/ expansion yap.
13
+ * Mac/Windows/Linux'ta calisir.
14
+ *
15
+ * Ornek:
16
+ * expandPath("~/Desktop/test.txt") -> "/Users/gencay/Desktop/test.txt"
17
+ * expandPath("~") -> "/Users/gencay"
18
+ * expandPath("/tmp/x") -> "/tmp/x"
19
+ * expandPath("Downloads/x.txt") -> "<cwd>/Downloads/x.txt"
20
+ */
21
+ function expandPath(inputPath) {
22
+ if (!inputPath) return inputPath;
23
+ let p = String(inputPath);
24
+
25
+ // ~ veya ~/foo -> home + foo
26
+ if (p === "~") return os.homedir();
27
+ if (p.startsWith("~/")) {
28
+ return path.join(os.homedir(), p.slice(2));
29
+ }
30
+ if (p.startsWith("~")) {
31
+ return path.join(os.homedir(), p.slice(1));
32
+ }
33
+
34
+ // Zaten absolut ise dokunma
35
+ if (path.isAbsolute(p)) return p;
36
+
37
+ // Relative ise cwd'ye gore resolve
38
+ return path.resolve(process.cwd(), p);
39
+ }
40
+
41
+ function isValidPath(p) {
42
+ if (!p) return false;
43
+ if (typeof p !== "string") return false;
44
+ if (p.includes("\0")) return false; // null byte
45
+ return true;
46
+ }
47
+
48
+ /**
49
+ * Path icin shell-safe quote (Parton'un "Downloads/adsiz klasor" gibi yollari icin)
50
+ * shlex.quote kullaniyor — universal
51
+ */
52
+ function quotePath(p) {
53
+ if (!p) return p;
54
+ const { execSync } = require("child_process");
55
+ // shlex.quote Python'dan cagiriliyor cunku Node'da yok
56
+ // Veya: bash -c 'echo $1' ile quote edilmis halini al
57
+ try {
58
+ // macOS/Unix: shlex.quote'a benzer sekilde single-quote wrap
59
+ if (/^[\w\-\.\/=]+$/.test(p)) return p; // guvenli karakterler
60
+ return `'${p.replace(/'/g, "'\\''")}'`;
61
+ } catch {
62
+ return p;
63
+ }
64
+ }
65
+
66
+ module.exports = { expandPath, isValidPath, quotePath };