@stan-chen/simple-cli 0.2.6 → 0.2.7

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/dist/builtins.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import { readFile, writeFile, readdir, unlink, mkdir, stat } from 'fs/promises';
2
2
  import { existsSync } from 'fs';
3
- import { join, resolve, relative, extname } from 'path';
3
+ import { join, resolve, relative, extname, isAbsolute } from 'path';
4
4
  import { exec, spawn, execSync } from 'child_process';
5
5
  import { promisify } from 'util';
6
6
  import { z } from 'zod';
7
7
  import glob from 'fast-glob';
8
+ import { Scheduler } from './scheduler.js';
9
+ import { AsyncTaskManager } from './async_tasks.js';
10
+ import { loadConfig } from './config.js';
8
11
  const execAsync = promisify(exec);
9
12
  const activeProcesses = [];
10
13
  export const cleanupProcesses = () => {
@@ -34,6 +37,13 @@ process.on('SIGTERM', () => {
34
37
  cleanupProcesses();
35
38
  process.exit();
36
39
  });
40
+ // Helper function to validate path is within allowed workspace
41
+ const isPathAllowed = (p) => {
42
+ const resolvedPath = resolve(p);
43
+ const workspaceRoot = resolve(process.cwd());
44
+ const relativePath = relative(workspaceRoot, resolvedPath);
45
+ return !relativePath.startsWith('..') && !isAbsolute(relativePath);
46
+ };
37
47
  export const readFiles = {
38
48
  name: 'read_files',
39
49
  description: 'Read contents of one or more files',
@@ -54,6 +64,10 @@ export const readFiles = {
54
64
  const results = [];
55
65
  for (const p of paths) {
56
66
  try {
67
+ if (!isPathAllowed(p)) {
68
+ results.push({ path: p, error: "Access denied: Path is outside the allowed workspace." });
69
+ continue;
70
+ }
57
71
  if (existsSync(p)) {
58
72
  const content = await readFile(p, 'utf-8');
59
73
  results.push({ path: p, content });
@@ -113,6 +127,10 @@ export const writeFiles = {
113
127
  results.push({ success: false, message: 'File path missing' });
114
128
  continue;
115
129
  }
130
+ if (!isPathAllowed(f.path)) {
131
+ results.push({ path: f.path, success: false, message: "Access denied: Path is outside the allowed workspace." });
132
+ continue;
133
+ }
116
134
  const dir = resolve(f.path, '..');
117
135
  if (!existsSync(dir)) {
118
136
  await mkdir(dir, { recursive: true });
@@ -158,11 +176,29 @@ export const createTool = {
158
176
  scope: z.enum(['local', 'global']).default('local')
159
177
  }),
160
178
  execute: async ({ source_path, name, description, usage, scope }) => {
179
+ if (!isPathAllowed(source_path))
180
+ return `Access denied: Path is outside the allowed workspace.`;
161
181
  if (!existsSync(source_path))
162
182
  return `Source file not found: ${source_path}`;
163
- const content = await readFile(source_path, 'utf-8');
164
183
  const ext = extname(source_path);
165
184
  const filename = `${name}${ext}`;
185
+ const targetDir = scope === 'global'
186
+ ? join(process.env.HOME || process.cwd(), '.agent', 'tools')
187
+ : join(process.cwd(), '.agent', 'tools');
188
+ // Check for existing tools
189
+ if (existsSync(join(targetDir, filename))) {
190
+ return `Error: Tool '${name}' already exists at ${join(targetDir, filename)}. Please use a different name or delete the existing tool.`;
191
+ }
192
+ // Check for similar names (simple containment)
193
+ if (existsSync(targetDir)) {
194
+ const existing = await readdir(targetDir);
195
+ const similar = existing.find(f => f.startsWith(name) || name.startsWith(f.split('.')[0]));
196
+ if (similar) {
197
+ // Warning only, allow creation but notify
198
+ console.warn(`Warning: A similar tool '${similar}' already exists.`);
199
+ }
200
+ }
201
+ const content = await readFile(source_path, 'utf-8');
166
202
  let header = '';
167
203
  if (ext === '.js' || ext === '.ts') {
168
204
  header = `/**\n * ${name}\n * ${description}\n * Usage: ${usage}\n */\n\n`;
@@ -170,9 +206,6 @@ export const createTool = {
170
206
  else if (ext === '.py') {
171
207
  header = `"""\n${name}\n${description}\nUsage: ${usage}\n"""\n\n`;
172
208
  }
173
- const targetDir = scope === 'global'
174
- ? join(process.env.HOME || process.cwd(), '.agent', 'tools')
175
- : join(process.cwd(), '.agent', 'tools');
176
209
  await mkdir(targetDir, { recursive: true });
177
210
  const targetPath = join(targetDir, filename);
178
211
  await writeFile(targetPath, header + content);
@@ -238,6 +271,14 @@ export const listFiles = {
238
271
  maxResults: z.number().optional()
239
272
  }),
240
273
  execute: async ({ pattern, path, ignore, includeDirectories, maxResults }) => {
274
+ if (!isPathAllowed(path)) {
275
+ return {
276
+ matches: [],
277
+ count: 0,
278
+ truncated: false,
279
+ error: "Access denied: Path is outside the allowed workspace."
280
+ };
281
+ }
241
282
  const files = await glob(pattern, {
242
283
  cwd: path,
243
284
  ignore: ignore,
@@ -267,6 +308,9 @@ export const searchFiles = {
267
308
  filesOnly: z.boolean().default(false)
268
309
  }),
269
310
  execute: async ({ pattern, path, glob: globPattern, ignoreCase, contextLines, maxResults, filesOnly }) => {
311
+ if (!isPathAllowed(path)) {
312
+ return { matches: [], count: 0, truncated: false, error: "Access denied: Path is outside the allowed workspace." };
313
+ }
270
314
  let files = [];
271
315
  try {
272
316
  const stats = await stat(path);
@@ -353,6 +397,8 @@ export const listDir = {
353
397
  description: 'List contents of a directory',
354
398
  inputSchema: z.object({ path: z.string().default('.') }),
355
399
  execute: async ({ path }) => {
400
+ if (!isPathAllowed(path))
401
+ return "Access denied: Path is outside the allowed workspace.";
356
402
  const items = await readdir(path, { withFileTypes: true });
357
403
  return items.map(i => ({ name: i.name, isDir: i.isDirectory() }));
358
404
  }
@@ -429,6 +475,8 @@ export const deleteFile = {
429
475
  const path = args.path || args.file || args.filename;
430
476
  if (!path)
431
477
  return "Error: 'path' argument required";
478
+ if (!isPathAllowed(path))
479
+ return "Access denied: Path is outside the allowed workspace.";
432
480
  if (existsSync(path)) {
433
481
  await unlink(path);
434
482
  return `Deleted ${path}`;
@@ -446,6 +494,8 @@ export const gitTool = {
446
494
  message: z.string().optional()
447
495
  }),
448
496
  execute: async ({ operation, cwd, files, message }) => {
497
+ if (!isPathAllowed(cwd))
498
+ return { success: false, error: "Access denied: Path is outside the allowed workspace." };
449
499
  const run = async (cmd) => {
450
500
  try {
451
501
  const { stdout } = await execAsync(cmd, { cwd });
@@ -485,6 +535,8 @@ export const linter = {
485
535
  description: 'Lint a file',
486
536
  inputSchema: z.object({ path: z.string() }),
487
537
  execute: async ({ path }) => {
538
+ if (!isPathAllowed(path))
539
+ return { passed: false, errors: [{ message: 'Access denied: Path is outside the allowed workspace.' }] };
488
540
  if (!existsSync(path))
489
541
  return { passed: false, errors: [{ message: 'File not found' }] };
490
542
  const ext = extname(path);
@@ -549,4 +601,258 @@ export const getTrackedFiles = async (cwd) => {
549
601
  return [];
550
602
  }
551
603
  };
552
- export const allBuiltins = [readFiles, writeFiles, createTool, scrapeUrl, listFiles, searchFiles, listDir, runCommand, stopCommand, deleteFile, gitTool, linter];
604
+ // --- Meta-Orchestrator Tools ---
605
+ export const delegate_cli = {
606
+ name: 'delegate_cli',
607
+ description: 'Delegate a complex task to a specialized external CLI agent (Codex, Gemini, Claude).',
608
+ inputSchema: z.object({
609
+ cli: z.string(),
610
+ task: z.string(),
611
+ context_files: z.array(z.string()).optional(),
612
+ async: z.boolean().default(false).describe("Run in background mode. Returns a Task ID to monitor.")
613
+ }),
614
+ execute: async ({ cli, task, context_files, async }) => {
615
+ try {
616
+ console.log(`[delegate_cli] Spawning external process for ${cli}...`);
617
+ const config = await loadConfig();
618
+ // Default to mock if no config for this agent
619
+ if (!config.agents || !config.agents[cli]) {
620
+ console.warn(`[delegate_cli] No configuration found for '${cli}'. Falling back to mock.`);
621
+ const cmd = `npx tsx tests/manual_scripts/mock_cli.ts "${task}"`;
622
+ const { stdout, stderr } = await execAsync(cmd);
623
+ if (stderr)
624
+ console.warn(`[delegate_cli] Stderr: ${stderr}`);
625
+ return `[${cli} CLI (Mock)]:\n${stdout.trim()}`;
626
+ }
627
+ const agent = config.agents[cli];
628
+ const cmdArgs = [...(agent.args || []), task];
629
+ // Handle file arguments for agents that don't support stdin or use --file flags
630
+ if (!agent.supports_stdin && context_files && context_files.length > 0) {
631
+ for (const file of context_files) {
632
+ if (!isPathAllowed(file)) {
633
+ console.warn(`[delegate_cli] Skipped restricted file: ${file}`);
634
+ continue;
635
+ }
636
+ const flag = agent.context_flag !== undefined ? agent.context_flag : '--file';
637
+ if (flag) {
638
+ cmdArgs.push(flag, file);
639
+ }
640
+ else {
641
+ cmdArgs.push(file);
642
+ }
643
+ }
644
+ }
645
+ const child = spawn(agent.command, cmdArgs, {
646
+ env: { ...process.env, ...agent.env },
647
+ shell: false // Use false for safer arg handling, unless command relies on shell features
648
+ });
649
+ // Async Mode Handling
650
+ if (async) {
651
+ // Determine command to run for AsyncTaskManager
652
+ // We need to reconstruct the full command string for AsyncTaskManager or pass args
653
+ // Actually AsyncTaskManager takes command and args.
654
+ // But we already spawned 'child' here?
655
+ // Wait, if async, we shouldn't spawn here and wait.
656
+ // We should delegate the spawning to AsyncTaskManager.
657
+ // Let's refactor:
658
+ child.kill(); // Kill the one we just started (oops, inefficient but safe if we didn't write stdin yet)
659
+ // Actually, let's just use AsyncTaskManager INSTEAD of spawning manually.
660
+ const taskManager = AsyncTaskManager.getInstance();
661
+ // Prepare context file content to pass as environment variable or temp file
662
+ // if the agent expects stdin.
663
+ // AsyncTaskManager runs detached, so we can't easily pipe stdin unless we wrap it.
664
+ // For simplicity, if async is requested, we might only support 'context_files' passed as args.
665
+ // If agent requires stdin, we can write a temporary input file and pipe it:
666
+ // cmd: "cat input.txt | agent ..."
667
+ // This is getting complex for 'generic' agents.
668
+ // Strategy: Just run the agent command. If it needs context files, they are in cmdArgs.
669
+ // If it needs stdin, we warn or skip for now in async mode unless we implement 'input_file' support.
670
+ if (agent.supports_stdin && context_files && context_files.length > 0) {
671
+ return `[delegate_cli] Warning: Async mode with Stdin context is not fully supported yet. Please use 'async: false' or ensure agent accepts files via arguments.`;
672
+ }
673
+ const id = await taskManager.startTask(agent.command, cmdArgs, agent.env);
674
+ return `[delegate_cli] Async Task Started.\nID: ${id}\nMonitor status using 'check_task_status'.`;
675
+ }
676
+ // Sync Mode (Existing Logic)
677
+ // Handle Stdin Context Injection
678
+ if (agent.supports_stdin && context_files && context_files.length > 0) {
679
+ // Read files and pipe to stdin
680
+ // Format: --- filename ---\n content \n ---
681
+ let context = "";
682
+ for (const file of context_files) {
683
+ if (!isPathAllowed(file)) {
684
+ console.warn(`[delegate_cli] Skipped restricted file: ${file}`);
685
+ continue;
686
+ }
687
+ if (existsSync(file)) {
688
+ const content = await readFile(file, 'utf-8');
689
+ context += `--- ${file} ---\n${content}\n\n`;
690
+ }
691
+ }
692
+ child.stdin.write(context);
693
+ child.stdin.end();
694
+ }
695
+ else {
696
+ child.stdin.end();
697
+ }
698
+ let stdout = '';
699
+ let stderr = '';
700
+ child.stdout.on('data', d => stdout += d.toString());
701
+ child.stderr.on('data', d => stderr += d.toString());
702
+ return new Promise((resolve, reject) => {
703
+ child.on('close', code => {
704
+ if (stderr)
705
+ console.warn(`[delegate_cli] Stderr: ${stderr}`);
706
+ if (code === 0) {
707
+ resolve(`[${cli} CLI]:\n${stdout.trim()}`);
708
+ }
709
+ else {
710
+ resolve(`[${cli} CLI] Process exited with code ${code}.\nOutput: ${stdout}\nError: ${stderr}`);
711
+ }
712
+ });
713
+ child.on('error', (err) => {
714
+ resolve(`[${cli} CLI] Failed to start process: ${err.message}`);
715
+ });
716
+ });
717
+ }
718
+ catch (e) {
719
+ return `[${cli} CLI]: Error executing external process: ${e.message}`;
720
+ }
721
+ }
722
+ };
723
+ export const schedule_task = {
724
+ name: 'schedule_task',
725
+ description: 'Register a recurring task to be executed by the agent autonomously.',
726
+ inputSchema: z.object({
727
+ cron: z.string().describe('Standard cron expression (e.g. "0 9 * * *")'),
728
+ prompt: z.string().describe('The instruction to execute'),
729
+ description: z.string().describe('Human-readable description')
730
+ }),
731
+ execute: async ({ cron, prompt, description }) => {
732
+ try {
733
+ const scheduler = Scheduler.getInstance();
734
+ const id = await scheduler.scheduleTask(cron, prompt, description);
735
+ return `Task scheduled successfully with ID: ${id}`;
736
+ }
737
+ catch (e) {
738
+ return `Failed to schedule task: ${e.message}`;
739
+ }
740
+ }
741
+ };
742
+ export const pr_list = {
743
+ name: 'pr_list',
744
+ description: 'List open pull requests',
745
+ inputSchema: z.object({ limit: z.number().default(10) }),
746
+ execute: async ({ limit }) => {
747
+ try {
748
+ const { stdout } = await execAsync(`gh pr list --limit ${limit}`);
749
+ return stdout.trim() || 'No open PRs found.';
750
+ }
751
+ catch (e) {
752
+ return `Error listing PRs: ${e.message}`;
753
+ }
754
+ }
755
+ };
756
+ export const pr_review = {
757
+ name: 'pr_review',
758
+ description: 'Get the diff/details of a PR',
759
+ inputSchema: z.object({ pr_number: z.number() }),
760
+ execute: async ({ pr_number }) => {
761
+ try {
762
+ const { stdout } = await execAsync(`gh pr diff ${pr_number}`);
763
+ return stdout.trim();
764
+ }
765
+ catch (e) {
766
+ return `Error reviewing PR ${pr_number}: ${e.message}`;
767
+ }
768
+ }
769
+ };
770
+ export const pr_comment = {
771
+ name: 'pr_comment',
772
+ description: 'Add a comment to a PR (e.g., to instruct Jules to fix something).',
773
+ inputSchema: z.object({
774
+ pr_number: z.number(),
775
+ body: z.string()
776
+ }),
777
+ execute: async ({ pr_number, body }) => {
778
+ try {
779
+ await execAsync(`gh pr comment ${pr_number} --body "${body}"`);
780
+ return `Commented on PR ${pr_number}: "${body}"`;
781
+ }
782
+ catch (e) {
783
+ return `Error commenting on PR ${pr_number}: ${e.message}`;
784
+ }
785
+ }
786
+ };
787
+ export const pr_ready = {
788
+ name: 'pr_ready',
789
+ description: 'Mark a Draft PR as Ready for Review',
790
+ inputSchema: z.object({ pr_number: z.number() }),
791
+ execute: async ({ pr_number }) => {
792
+ try {
793
+ await execAsync(`gh pr ready ${pr_number}`);
794
+ return `PR ${pr_number} marked as Ready for Review`;
795
+ }
796
+ catch (e) {
797
+ return `Error marking PR ${pr_number} as ready: ${e.message}`;
798
+ }
799
+ }
800
+ };
801
+ export const pr_merge = {
802
+ name: 'pr_merge',
803
+ description: 'Merge a pull request',
804
+ inputSchema: z.object({
805
+ pr_number: z.number(),
806
+ method: z.enum(['merge', 'squash', 'rebase']).default('merge')
807
+ }),
808
+ execute: async ({ pr_number, method }) => {
809
+ try {
810
+ await execAsync(`gh pr merge ${pr_number} --${method} --delete-branch`);
811
+ return `Successfully merged PR ${pr_number}`;
812
+ }
813
+ catch (e) {
814
+ return `Error merging PR ${pr_number}: ${e.message}`;
815
+ }
816
+ }
817
+ };
818
+ export const check_task_status = {
819
+ name: 'check_task_status',
820
+ description: 'Check the status and logs of a background task.',
821
+ inputSchema: z.object({
822
+ id: z.string().describe('The Task ID returned by delegate_cli')
823
+ }),
824
+ execute: async ({ id }) => {
825
+ try {
826
+ const manager = AsyncTaskManager.getInstance();
827
+ const task = await manager.getTaskStatus(id);
828
+ const logs = await manager.getTaskLogs(id, 5); // Last 5 lines
829
+ return `Task ID: ${task.id}
830
+ Status: ${task.status}
831
+ PID: ${task.pid}
832
+ Last Logs:
833
+ ${logs}
834
+ `;
835
+ }
836
+ catch (e) {
837
+ return `Error checking task ${id}: ${e.message}`;
838
+ }
839
+ }
840
+ };
841
+ export const list_bg_tasks = {
842
+ name: 'list_bg_tasks',
843
+ description: 'List all background tasks.',
844
+ inputSchema: z.object({}),
845
+ execute: async () => {
846
+ try {
847
+ const manager = AsyncTaskManager.getInstance();
848
+ const tasks = await manager.listTasks();
849
+ if (tasks.length === 0)
850
+ return "No background tasks found.";
851
+ return tasks.map(t => `- [${t.status.toUpperCase()}] ${t.id}: ${t.command} (Started: ${new Date(t.startTime).toISOString()})`).join('\n');
852
+ }
853
+ catch (e) {
854
+ return `Error listing tasks: ${e.message}`;
855
+ }
856
+ }
857
+ };
858
+ export const allBuiltins = [readFiles, writeFiles, createTool, scrapeUrl, listFiles, searchFiles, listDir, runCommand, stopCommand, deleteFile, gitTool, linter, delegate_cli, schedule_task, check_task_status, list_bg_tasks, pr_list, pr_review, pr_comment, pr_ready, pr_merge];
@@ -0,0 +1,5 @@
1
+ /**
2
+ * [Simple-CLI AI-Created]
3
+ * Recreated file based on task description.
4
+ */
5
+ export declare function buildMemoryContext(reflectionsDir: string, files: string[]): Promise<string>;
@@ -0,0 +1,14 @@
1
+ import { readFile } from 'fs/promises';
2
+ import * as path from 'path';
3
+ /**
4
+ * [Simple-CLI AI-Created]
5
+ * Recreated file based on task description.
6
+ */
7
+ export async function buildMemoryContext(reflectionsDir, files) {
8
+ // Optimized: Concurrent non-blocking reads using Promise.all
9
+ const contents = await Promise.all(files.map(async (f) => {
10
+ const content = await readFile(path.join(reflectionsDir, f), 'utf-8');
11
+ return `\n--- Previous Reflection (${f}) ---\n${content}\n`;
12
+ }));
13
+ return contents.join('');
14
+ }
package/dist/cli.js CHANGED
@@ -7,17 +7,25 @@ import { createLLM } from './llm.js';
7
7
  import { MCP } from './mcp.js';
8
8
  import { getActiveSkill } from './skills.js';
9
9
  import { showBanner } from './tui.js';
10
+ import { Scheduler } from './scheduler.js';
11
+ import { log, outro } from '@clack/prompts';
10
12
  async function main() {
11
13
  const args = process.argv.slice(2);
12
14
  // Handle optional directory argument
13
15
  let cwd = process.cwd();
14
16
  let interactive = true;
17
+ let daemon = false;
15
18
  const remainingArgs = [];
16
19
  for (const arg of args) {
17
20
  if (arg === '--non-interactive') {
18
21
  interactive = false;
19
22
  continue;
20
23
  }
24
+ if (arg === '--daemon') {
25
+ daemon = true;
26
+ interactive = false;
27
+ continue;
28
+ }
21
29
  if (!arg.startsWith('-')) {
22
30
  try {
23
31
  if (statSync(arg).isDirectory()) {
@@ -37,9 +45,52 @@ async function main() {
37
45
  const mcp = new MCP();
38
46
  const provider = createLLM();
39
47
  const engine = new Engine(provider, registry, mcp);
40
- const skill = await getActiveSkill(cwd);
41
- const ctx = new Context(cwd, skill);
42
- showBanner();
43
- await engine.run(ctx, prompt || undefined, { interactive });
48
+ // --- Scheduler Integration ---
49
+ const scheduler = Scheduler.getInstance(cwd);
50
+ const processDueTasks = async () => {
51
+ const dueTasks = await scheduler.getDueTasks();
52
+ if (dueTasks.length > 0) {
53
+ log.info(`Found ${dueTasks.length} scheduled tasks due.`);
54
+ for (const task of dueTasks) {
55
+ log.step(`Running task: ${task.description} (${task.cron})`);
56
+ // Use a fresh context for each task
57
+ const taskCtx = new Context(cwd, await getActiveSkill(cwd));
58
+ try {
59
+ // Run non-interactively
60
+ await engine.run(taskCtx, task.prompt, { interactive: false });
61
+ await scheduler.markTaskRun(task.id, true);
62
+ log.success(`Task ${task.id} completed.`);
63
+ }
64
+ catch (e) {
65
+ log.error(`Task ${task.id} failed: ${e.message}`);
66
+ await scheduler.markTaskRun(task.id, false);
67
+ }
68
+ }
69
+ log.info('All scheduled tasks processed.');
70
+ }
71
+ };
72
+ if (daemon) {
73
+ showBanner();
74
+ log.info('Running in daemon mode. Checking for tasks every 60s...');
75
+ while (true) {
76
+ try {
77
+ await processDueTasks();
78
+ }
79
+ catch (e) {
80
+ log.error(`Error in daemon loop: ${e.message}`);
81
+ }
82
+ // Sleep for 60 seconds
83
+ await new Promise(resolve => setTimeout(resolve, 60000));
84
+ }
85
+ }
86
+ else {
87
+ showBanner();
88
+ // Standard run: check tasks once, then interactive
89
+ await processDueTasks();
90
+ const skill = await getActiveSkill(cwd);
91
+ const ctx = new Context(cwd, skill);
92
+ await engine.run(ctx, prompt || undefined, { interactive });
93
+ outro('Session finished.');
94
+ }
44
95
  }
45
96
  main().catch(console.error);
@@ -0,0 +1,27 @@
1
+ export interface AgentConfig {
2
+ command: string;
3
+ args: string[];
4
+ description: string;
5
+ supports_stdin?: boolean;
6
+ env?: Record<string, string>;
7
+ context_flag?: string;
8
+ }
9
+ export interface TaskConfig {
10
+ id: string;
11
+ cron: string;
12
+ command: string;
13
+ description: string;
14
+ enabled?: boolean;
15
+ }
16
+ export interface SchedulerConfig {
17
+ enabled: boolean;
18
+ tasks: TaskConfig[];
19
+ }
20
+ export interface Config {
21
+ mcpServers?: Record<string, any>;
22
+ agents?: Record<string, AgentConfig>;
23
+ scheduler?: SchedulerConfig;
24
+ yoloMode?: boolean;
25
+ autoDecisionTimeout?: number;
26
+ }
27
+ export declare function loadConfig(cwd?: string): Promise<Config>;
package/dist/config.js ADDED
@@ -0,0 +1,21 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ export async function loadConfig(cwd = process.cwd()) {
5
+ const locations = [
6
+ join(cwd, 'mcp.json'),
7
+ join(cwd, '.agent', 'config.json')
8
+ ];
9
+ for (const loc of locations) {
10
+ if (existsSync(loc)) {
11
+ try {
12
+ const content = await readFile(loc, 'utf-8');
13
+ return JSON.parse(content);
14
+ }
15
+ catch (e) {
16
+ console.error(`Failed to parse config at ${loc}:`, e);
17
+ }
18
+ }
19
+ }
20
+ return {};
21
+ }