@stan-chen/simple-cli 0.2.1

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.
Files changed (87) hide show
  1. package/README.md +287 -0
  2. package/dist/cli.d.ts +6 -0
  3. package/dist/cli.js +259 -0
  4. package/dist/commands/add.d.ts +9 -0
  5. package/dist/commands/add.js +50 -0
  6. package/dist/commands/git/commit.d.ts +12 -0
  7. package/dist/commands/git/commit.js +97 -0
  8. package/dist/commands/git/status.d.ts +6 -0
  9. package/dist/commands/git/status.js +42 -0
  10. package/dist/commands/index.d.ts +16 -0
  11. package/dist/commands/index.js +376 -0
  12. package/dist/commands/mcp/status.d.ts +6 -0
  13. package/dist/commands/mcp/status.js +31 -0
  14. package/dist/commands/swarm.d.ts +36 -0
  15. package/dist/commands/swarm.js +236 -0
  16. package/dist/commands.d.ts +32 -0
  17. package/dist/commands.js +427 -0
  18. package/dist/context.d.ts +116 -0
  19. package/dist/context.js +327 -0
  20. package/dist/index.d.ts +6 -0
  21. package/dist/index.js +109 -0
  22. package/dist/lib/agent.d.ts +98 -0
  23. package/dist/lib/agent.js +281 -0
  24. package/dist/lib/editor.d.ts +74 -0
  25. package/dist/lib/editor.js +441 -0
  26. package/dist/lib/git.d.ts +164 -0
  27. package/dist/lib/git.js +351 -0
  28. package/dist/lib/ui.d.ts +159 -0
  29. package/dist/lib/ui.js +252 -0
  30. package/dist/mcp/client.d.ts +22 -0
  31. package/dist/mcp/client.js +81 -0
  32. package/dist/mcp/manager.d.ts +186 -0
  33. package/dist/mcp/manager.js +442 -0
  34. package/dist/prompts/provider.d.ts +22 -0
  35. package/dist/prompts/provider.js +78 -0
  36. package/dist/providers/index.d.ts +15 -0
  37. package/dist/providers/index.js +82 -0
  38. package/dist/providers/multi.d.ts +11 -0
  39. package/dist/providers/multi.js +28 -0
  40. package/dist/registry.d.ts +24 -0
  41. package/dist/registry.js +379 -0
  42. package/dist/repoMap.d.ts +5 -0
  43. package/dist/repoMap.js +79 -0
  44. package/dist/router.d.ts +41 -0
  45. package/dist/router.js +108 -0
  46. package/dist/skills.d.ts +25 -0
  47. package/dist/skills.js +288 -0
  48. package/dist/swarm/coordinator.d.ts +86 -0
  49. package/dist/swarm/coordinator.js +257 -0
  50. package/dist/swarm/index.d.ts +28 -0
  51. package/dist/swarm/index.js +29 -0
  52. package/dist/swarm/task.d.ts +104 -0
  53. package/dist/swarm/task.js +221 -0
  54. package/dist/swarm/types.d.ts +132 -0
  55. package/dist/swarm/types.js +37 -0
  56. package/dist/swarm/worker.d.ts +107 -0
  57. package/dist/swarm/worker.js +299 -0
  58. package/dist/tools/analyzeFile.d.ts +16 -0
  59. package/dist/tools/analyzeFile.js +43 -0
  60. package/dist/tools/git.d.ts +40 -0
  61. package/dist/tools/git.js +236 -0
  62. package/dist/tools/glob.d.ts +34 -0
  63. package/dist/tools/glob.js +165 -0
  64. package/dist/tools/grep.d.ts +53 -0
  65. package/dist/tools/grep.js +296 -0
  66. package/dist/tools/linter.d.ts +35 -0
  67. package/dist/tools/linter.js +349 -0
  68. package/dist/tools/listDir.d.ts +29 -0
  69. package/dist/tools/listDir.js +50 -0
  70. package/dist/tools/memory.d.ts +34 -0
  71. package/dist/tools/memory.js +215 -0
  72. package/dist/tools/readFiles.d.ts +25 -0
  73. package/dist/tools/readFiles.js +31 -0
  74. package/dist/tools/reloadTools.d.ts +11 -0
  75. package/dist/tools/reloadTools.js +22 -0
  76. package/dist/tools/runCommand.d.ts +32 -0
  77. package/dist/tools/runCommand.js +79 -0
  78. package/dist/tools/scraper.d.ts +31 -0
  79. package/dist/tools/scraper.js +211 -0
  80. package/dist/tools/writeFiles.d.ts +63 -0
  81. package/dist/tools/writeFiles.js +87 -0
  82. package/dist/ui/server.d.ts +5 -0
  83. package/dist/ui/server.js +74 -0
  84. package/dist/watcher.d.ts +35 -0
  85. package/dist/watcher.js +164 -0
  86. package/docs/assets/logo.jpeg +0 -0
  87. package/package.json +78 -0
@@ -0,0 +1,74 @@
1
+ import http from 'http';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { getContextManager } from '../context.js';
6
+ import { createProvider } from '../providers/index.js';
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ export async function startServer(options) {
9
+ const { port, host } = options;
10
+ // Initialize Simple-CLI core
11
+ const ctx = getContextManager();
12
+ await ctx.initialize();
13
+ const provider = createProvider();
14
+ const server = http.createServer(async (req, res) => {
15
+ // Basic router
16
+ if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
17
+ res.writeHead(200, { 'Content-Type': 'text/html' });
18
+ res.end(fs.readFileSync(path.join(__dirname, 'index.html')));
19
+ return;
20
+ }
21
+ if (req.method === 'POST' && req.url === '/api/chat') {
22
+ let body = '';
23
+ req.on('data', chunk => { body += chunk; });
24
+ req.on('end', async () => {
25
+ try {
26
+ const { message } = JSON.parse(body);
27
+ if (!message) {
28
+ res.writeHead(400);
29
+ res.end(JSON.stringify({ error: 'Message required' }));
30
+ return;
31
+ }
32
+ // Add message to context
33
+ ctx.addMessage('user', message);
34
+ // Get system prompt and history
35
+ const systemPrompt = await ctx.buildSystemPrompt();
36
+ const history = ctx.getHistory();
37
+ // Generate response
38
+ const response = await provider.generateResponse(systemPrompt, history.map(m => ({ role: m.role, content: m.content })));
39
+ // Parse response (minimalist version of cli.ts logic)
40
+ const thought = response.match(/<thought>([\s\S]*?)<\/thought>/)?.[1]?.trim();
41
+ const jsonMatch = response.match(/\{[\s\S]*"tool"[\s\S]*\}/);
42
+ let action = { tool: 'none', message: 'No action', args: {} };
43
+ if (jsonMatch) {
44
+ try {
45
+ action = JSON.parse(jsonMatch[0]);
46
+ }
47
+ catch { /* ignore */ }
48
+ }
49
+ const messageText = action.message || response.replace(/<thought>[\s\S]*?<\/thought>/, '').replace(/\{[\s\S]*"tool"[\s\S]*\}/, '').trim();
50
+ // Update context
51
+ ctx.addMessage('assistant', response);
52
+ res.writeHead(200, { 'Content-Type': 'application/json' });
53
+ res.end(JSON.stringify({
54
+ message: messageText,
55
+ thought,
56
+ action
57
+ }));
58
+ }
59
+ catch (error) {
60
+ res.writeHead(500);
61
+ res.end(JSON.stringify({ error: error.message }));
62
+ }
63
+ });
64
+ return;
65
+ }
66
+ // 404
67
+ res.writeHead(404);
68
+ res.end('Not Found');
69
+ });
70
+ server.listen(port, host, () => {
71
+ console.log(`\n🚀 Simple-CLI UI running at http://${host}:${port}`);
72
+ console.log(` Press Ctrl+C to stop\n`);
73
+ });
74
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * File Watcher - Watch files for changes and AI comments
3
+ * Uses chokidar for robust cross-platform watching
4
+ */
5
+ import { EventEmitter } from 'events';
6
+ export interface WatcherOptions {
7
+ root: string;
8
+ ignorePatterns?: RegExp[];
9
+ onFileChange?: (filePath: string, type: 'add' | 'change' | 'unlink') => void;
10
+ onAIComment?: (filePath: string, comments: AIComment[]) => void;
11
+ verbose?: boolean;
12
+ }
13
+ export interface AIComment {
14
+ line: number;
15
+ text: string;
16
+ action: 'request' | 'question' | 'note';
17
+ }
18
+ export declare class FileWatcher extends EventEmitter {
19
+ private options;
20
+ private watcher;
21
+ private watchedFiles;
22
+ private ignorePatterns;
23
+ private verbose;
24
+ constructor(options: WatcherOptions);
25
+ start(): void;
26
+ stop(): void;
27
+ private shouldIgnore;
28
+ private handleChange;
29
+ private handleUnlink;
30
+ private extractAIComments;
31
+ hasActionableComments(): boolean;
32
+ getActionableCommentsPrompt(): string;
33
+ private log;
34
+ }
35
+ export declare function createFileWatcher(options: WatcherOptions): FileWatcher;
@@ -0,0 +1,164 @@
1
+ /**
2
+ * File Watcher - Watch files for changes and AI comments
3
+ * Uses chokidar for robust cross-platform watching
4
+ */
5
+ import * as chokidar from 'chokidar';
6
+ import { readFile } from 'fs/promises';
7
+ import { relative } from 'path';
8
+ import { EventEmitter } from 'events';
9
+ // AI comment pattern (matches: # ai!, // ai?, -- ai, etc.)
10
+ const AI_COMMENT_PATTERN = /(?:#|\/\/|--|;+)\s*(ai\b.*|ai\b.*|.*\bai[?!]?)\s*$/i;
11
+ // Default ignore patterns
12
+ const IGNORE_PATTERNS = [
13
+ /node_modules/,
14
+ /\.git/,
15
+ /dist\//,
16
+ /build\//,
17
+ /\.next/,
18
+ /\.nuxt/,
19
+ /__pycache__/,
20
+ /\.pytest_cache/,
21
+ /\.venv/,
22
+ /venv/,
23
+ /\.env$/,
24
+ /\.DS_Store/,
25
+ /\.swp$/,
26
+ /\.swo$/,
27
+ /\.bak$/,
28
+ /~$/,
29
+ /\.tmp$/,
30
+ /\.log$/,
31
+ ];
32
+ const MAX_FILE_SIZE = 1024 * 1024; // 1MB
33
+ export class FileWatcher extends EventEmitter {
34
+ options;
35
+ watcher = null;
36
+ watchedFiles = new Map();
37
+ ignorePatterns;
38
+ verbose;
39
+ constructor(options) {
40
+ super();
41
+ this.options = options;
42
+ this.ignorePatterns = [...IGNORE_PATTERNS, ...(options.ignorePatterns || [])];
43
+ this.verbose = options.verbose || false;
44
+ if (options.onFileChange)
45
+ this.on('file-change', options.onFileChange);
46
+ if (options.onAIComment)
47
+ this.on('ai-comment', options.onAIComment);
48
+ }
49
+ start() {
50
+ if (this.watcher)
51
+ return;
52
+ this.log(`Starting chokidar watcher on ${this.options.root}`);
53
+ this.watcher = chokidar.watch(this.options.root, {
54
+ ignored: (path) => this.shouldIgnore(path),
55
+ persistent: true,
56
+ ignoreInitial: false,
57
+ awaitWriteFinish: {
58
+ stabilityThreshold: 300,
59
+ pollInterval: 100
60
+ }
61
+ });
62
+ this.watcher
63
+ .on('add', (path) => this.handleChange(path, 'add'))
64
+ .on('change', (path) => this.handleChange(path, 'change'))
65
+ .on('unlink', (path) => this.handleUnlink(path));
66
+ }
67
+ stop() {
68
+ if (this.watcher) {
69
+ this.watcher.close();
70
+ this.watcher = null;
71
+ this.log('Watcher stopped');
72
+ }
73
+ this.watchedFiles.clear();
74
+ }
75
+ shouldIgnore(path) {
76
+ const relPath = relative(this.options.root, path);
77
+ // Always include root
78
+ if (path === this.options.root)
79
+ return false;
80
+ return this.ignorePatterns.some(pattern => pattern.test(relPath));
81
+ }
82
+ async handleChange(path, type) {
83
+ const relPath = relative(this.options.root, path);
84
+ // Skip large files (chokidar might have already stat-ed, but we want to be safe before reading)
85
+ // Note: chokidar doesn't expose stat size in event easily without another stat call or 'add' stats arg
86
+ // For simplicity, we just read.
87
+ try {
88
+ // ... Check size logic could go here if critical
89
+ const comments = await this.extractAIComments(path);
90
+ const existing = this.watchedFiles.get(path);
91
+ const hasNewComments = comments.length > 0 && (!existing || JSON.stringify(comments) !== JSON.stringify(existing.aiComments));
92
+ this.watchedFiles.set(path, { path, aiComments: comments });
93
+ this.emit('file-change', relPath, type);
94
+ if (hasNewComments) {
95
+ this.emit('ai-comment', relPath, comments);
96
+ }
97
+ }
98
+ catch (e) {
99
+ this.log(`Error processing ${relPath}: ${e}`);
100
+ }
101
+ }
102
+ handleUnlink(path) {
103
+ const relPath = relative(this.options.root, path);
104
+ this.watchedFiles.delete(path);
105
+ this.emit('file-change', relPath, 'unlink');
106
+ }
107
+ async extractAIComments(filePath) {
108
+ try {
109
+ const content = await readFile(filePath, 'utf-8');
110
+ if (content.length > MAX_FILE_SIZE)
111
+ return [];
112
+ const lines = content.split('\n');
113
+ const comments = [];
114
+ for (let i = 0; i < lines.length; i++) {
115
+ const match = AI_COMMENT_PATTERN.exec(lines[i]);
116
+ if (match) {
117
+ const text = match[1].trim();
118
+ let action = 'note';
119
+ const lower = text.toLowerCase();
120
+ if (lower.endsWith('!') || lower.startsWith('ai!'))
121
+ action = 'request';
122
+ else if (lower.endsWith('?') || lower.startsWith('ai?'))
123
+ action = 'question';
124
+ comments.push({ line: i + 1, text, action });
125
+ }
126
+ }
127
+ return comments;
128
+ }
129
+ catch {
130
+ return [];
131
+ }
132
+ }
133
+ hasActionableComments() {
134
+ for (const [_, file] of this.watchedFiles) {
135
+ if (file.aiComments.some(c => c.action === 'request' || c.action === 'question')) {
136
+ return true;
137
+ }
138
+ }
139
+ return false;
140
+ }
141
+ getActionableCommentsPrompt() {
142
+ const parts = [];
143
+ for (const [path, file] of this.watchedFiles) {
144
+ const actionable = file.aiComments.filter(c => c.action === 'request' || c.action === 'question');
145
+ if (actionable.length > 0) {
146
+ const relPath = relative(this.options.root, path);
147
+ parts.push(`\n${relPath}:`);
148
+ for (const c of actionable) {
149
+ parts.push(` Line ${c.line} [${c.action === 'request' ? '!' : '?'}]: ${c.text}`);
150
+ }
151
+ }
152
+ }
153
+ return parts.length > 0 ? `The following AI comments were found in the codebase:${parts.join('\n')}` : '';
154
+ }
155
+ log(msg) {
156
+ if (this.verbose)
157
+ console.log(`[Watcher] ${msg}`);
158
+ }
159
+ }
160
+ export function createFileWatcher(options) {
161
+ const w = new FileWatcher(options);
162
+ w.start();
163
+ return w;
164
+ }
Binary file
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@stan-chen/simple-cli",
3
+ "version": "0.2.1",
4
+ "description": "A lean, lightweight coding agent. Packs a punch with token efficiency.",
5
+ "type": "module",
6
+ "main": "dist/cli.js",
7
+ "bin": {
8
+ "simple": "dist/cli.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node --loader ts-node/esm src/cli.ts",
12
+ "dev": "node --loader ts-node/esm --watch src/index.ts",
13
+ "build": "tsc",
14
+ "typecheck": "tsc --noEmit",
15
+ "test": "vitest run",
16
+ "test:watch": "vitest",
17
+ "test:coverage": "vitest run --coverage",
18
+ "lint": "eslint src/ tests/",
19
+ "format": "prettier --write src/ tests/"
20
+ },
21
+ "keywords": [
22
+ "cli",
23
+ "agent",
24
+ "coding-assistant",
25
+ "llm",
26
+ "mcp",
27
+ "ai",
28
+ "openai",
29
+ "anthropic",
30
+ "gemini",
31
+ "litellm",
32
+ "aider"
33
+ ],
34
+ "license": "MIT",
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ },
38
+ "dependencies": {
39
+ "@clack/prompts": "^0.7.0",
40
+ "@modelcontextprotocol/sdk": "^1.0.0",
41
+ "@oclif/core": "^4.0.0",
42
+ "chalk": "^5.3.0",
43
+ "chokidar": "^5.0.0",
44
+ "diff": "^5.2.0",
45
+ "dotenv": "^16.4.7",
46
+ "fast-glob": "^3.3.2",
47
+ "fast-levenshtein": "^3.0.0",
48
+ "glob": "^13.0.0",
49
+ "gpt-tokenizer": "^2.5.0",
50
+ "jsonrepair": "^3.13.2",
51
+ "openai": "^6.16.0",
52
+ "ora": "^8.0.0",
53
+ "picocolors": "^1.0.0",
54
+ "simple-git": "^3.22.0",
55
+ "ts-morph": "^22.0.0",
56
+ "turndown": "^7.2.0",
57
+ "yaml": "^2.8.2",
58
+ "zod": "^3.24.1"
59
+ },
60
+ "devDependencies": {
61
+ "@oclif/test": "^4.0.0",
62
+ "@types/chokidar": "^1.7.5",
63
+ "@types/diff": "^5.2.3",
64
+ "@types/fast-levenshtein": "^0.0.4",
65
+ "@types/node": "^22.19.7",
66
+ "@types/turndown": "^5.0.5",
67
+ "oclif": "^4.0.0",
68
+ "ts-node": "^10.9.2",
69
+ "tsx": "^4.19.2",
70
+ "typescript": "^5.7.2",
71
+ "vitest": "^2.1.8"
72
+ },
73
+ "files": [
74
+ "dist",
75
+ "README.md",
76
+ "docs/assets/logo.jpeg"
77
+ ]
78
+ }