@stan-chen/simple-cli 0.2.5 → 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,11 +1,49 @@
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';
4
- import { exec } from 'child_process';
3
+ import { join, resolve, relative, extname, isAbsolute } from 'path';
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);
12
+ const activeProcesses = [];
13
+ export const cleanupProcesses = () => {
14
+ for (const proc of activeProcesses) {
15
+ if (!proc.killed && proc.pid) {
16
+ if (process.platform === 'win32') {
17
+ try {
18
+ execSync(`taskkill /F /T /PID ${proc.pid}`);
19
+ }
20
+ catch {
21
+ proc.kill('SIGTERM');
22
+ }
23
+ }
24
+ else {
25
+ proc.kill('SIGTERM');
26
+ }
27
+ }
28
+ }
29
+ };
30
+ // Handle cleanup on exit
31
+ process.on('exit', cleanupProcesses);
32
+ process.on('SIGINT', () => {
33
+ cleanupProcesses();
34
+ process.exit();
35
+ });
36
+ process.on('SIGTERM', () => {
37
+ cleanupProcesses();
38
+ process.exit();
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
+ };
9
47
  export const readFiles = {
10
48
  name: 'read_files',
11
49
  description: 'Read contents of one or more files',
@@ -26,6 +64,10 @@ export const readFiles = {
26
64
  const results = [];
27
65
  for (const p of paths) {
28
66
  try {
67
+ if (!isPathAllowed(p)) {
68
+ results.push({ path: p, error: "Access denied: Path is outside the allowed workspace." });
69
+ continue;
70
+ }
29
71
  if (existsSync(p)) {
30
72
  const content = await readFile(p, 'utf-8');
31
73
  results.push({ path: p, content });
@@ -85,6 +127,10 @@ export const writeFiles = {
85
127
  results.push({ success: false, message: 'File path missing' });
86
128
  continue;
87
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
+ }
88
134
  const dir = resolve(f.path, '..');
89
135
  if (!existsSync(dir)) {
90
136
  await mkdir(dir, { recursive: true });
@@ -130,11 +176,29 @@ export const createTool = {
130
176
  scope: z.enum(['local', 'global']).default('local')
131
177
  }),
132
178
  execute: async ({ source_path, name, description, usage, scope }) => {
179
+ if (!isPathAllowed(source_path))
180
+ return `Access denied: Path is outside the allowed workspace.`;
133
181
  if (!existsSync(source_path))
134
182
  return `Source file not found: ${source_path}`;
135
- const content = await readFile(source_path, 'utf-8');
136
183
  const ext = extname(source_path);
137
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');
138
202
  let header = '';
139
203
  if (ext === '.js' || ext === '.ts') {
140
204
  header = `/**\n * ${name}\n * ${description}\n * Usage: ${usage}\n */\n\n`;
@@ -142,9 +206,6 @@ export const createTool = {
142
206
  else if (ext === '.py') {
143
207
  header = `"""\n${name}\n${description}\nUsage: ${usage}\n"""\n\n`;
144
208
  }
145
- const targetDir = scope === 'global'
146
- ? join(process.env.HOME || process.cwd(), '.agent', 'tools')
147
- : join(process.cwd(), '.agent', 'tools');
148
209
  await mkdir(targetDir, { recursive: true });
149
210
  const targetPath = join(targetDir, filename);
150
211
  await writeFile(targetPath, header + content);
@@ -210,6 +271,14 @@ export const listFiles = {
210
271
  maxResults: z.number().optional()
211
272
  }),
212
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
+ }
213
282
  const files = await glob(pattern, {
214
283
  cwd: path,
215
284
  ignore: ignore,
@@ -239,6 +308,9 @@ export const searchFiles = {
239
308
  filesOnly: z.boolean().default(false)
240
309
  }),
241
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
+ }
242
314
  let files = [];
243
315
  try {
244
316
  const stats = await stat(path);
@@ -325,15 +397,35 @@ export const listDir = {
325
397
  description: 'List contents of a directory',
326
398
  inputSchema: z.object({ path: z.string().default('.') }),
327
399
  execute: async ({ path }) => {
400
+ if (!isPathAllowed(path))
401
+ return "Access denied: Path is outside the allowed workspace.";
328
402
  const items = await readdir(path, { withFileTypes: true });
329
403
  return items.map(i => ({ name: i.name, isDir: i.isDirectory() }));
330
404
  }
331
405
  };
332
406
  export const runCommand = {
333
407
  name: 'run_command',
334
- description: 'Run a shell command',
335
- inputSchema: z.object({ command: z.string(), timeout: z.number().optional() }),
336
- execute: async ({ command, timeout }) => {
408
+ description: 'Run a shell command. Use background: true for servers or long-running tasks.',
409
+ inputSchema: z.object({
410
+ command: z.string(),
411
+ timeout: z.number().optional(),
412
+ background: z.boolean().default(false).describe('Run command in background and return immediately')
413
+ }),
414
+ execute: async ({ command, timeout, background }) => {
415
+ if (background) {
416
+ const child = spawn(command, {
417
+ shell: true,
418
+ detached: true,
419
+ stdio: 'ignore'
420
+ });
421
+ child.unref();
422
+ activeProcesses.push(child);
423
+ return {
424
+ message: `Started background process: ${command}`,
425
+ pid: child.pid,
426
+ success: true
427
+ };
428
+ }
337
429
  try {
338
430
  const { stdout, stderr } = await execAsync(command, { timeout });
339
431
  return { stdout, stderr, exitCode: 0, timedOut: false };
@@ -350,6 +442,31 @@ export const runCommand = {
350
442
  }
351
443
  }
352
444
  };
445
+ export const stopCommand = {
446
+ name: 'stop_command',
447
+ description: 'Stop a background process by its PID',
448
+ inputSchema: z.object({ pid: z.number() }),
449
+ execute: async ({ pid }) => {
450
+ if (process.platform === 'win32') {
451
+ try {
452
+ await execAsync(`taskkill /F /T /PID ${pid}`);
453
+ return `Successfully stopped process ${pid}`;
454
+ }
455
+ catch (e) {
456
+ return `Error stopping process ${pid}: ${e.message}`;
457
+ }
458
+ }
459
+ else {
460
+ try {
461
+ process.kill(pid, 'SIGTERM');
462
+ return `Sent SIGTERM to process ${pid}`;
463
+ }
464
+ catch (e) {
465
+ return `Error stopping process ${pid}: ${e.message}`;
466
+ }
467
+ }
468
+ }
469
+ };
353
470
  export const deleteFile = {
354
471
  name: 'delete_file',
355
472
  description: 'Delete a file',
@@ -358,6 +475,8 @@ export const deleteFile = {
358
475
  const path = args.path || args.file || args.filename;
359
476
  if (!path)
360
477
  return "Error: 'path' argument required";
478
+ if (!isPathAllowed(path))
479
+ return "Access denied: Path is outside the allowed workspace.";
361
480
  if (existsSync(path)) {
362
481
  await unlink(path);
363
482
  return `Deleted ${path}`;
@@ -375,6 +494,8 @@ export const gitTool = {
375
494
  message: z.string().optional()
376
495
  }),
377
496
  execute: async ({ operation, cwd, files, message }) => {
497
+ if (!isPathAllowed(cwd))
498
+ return { success: false, error: "Access denied: Path is outside the allowed workspace." };
378
499
  const run = async (cmd) => {
379
500
  try {
380
501
  const { stdout } = await execAsync(cmd, { cwd });
@@ -414,6 +535,8 @@ export const linter = {
414
535
  description: 'Lint a file',
415
536
  inputSchema: z.object({ path: z.string() }),
416
537
  execute: async ({ path }) => {
538
+ if (!isPathAllowed(path))
539
+ return { passed: false, errors: [{ message: 'Access denied: Path is outside the allowed workspace.' }] };
417
540
  if (!existsSync(path))
418
541
  return { passed: false, errors: [{ message: 'File not found' }] };
419
542
  const ext = extname(path);
@@ -478,4 +601,258 @@ export const getTrackedFiles = async (cwd) => {
478
601
  return [];
479
602
  }
480
603
  };
481
- export const allBuiltins = [readFiles, writeFiles, createTool, scrapeUrl, listFiles, searchFiles, listDir, runCommand, 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
+ }