@aigencydev/cli 0.4.0 → 0.5.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/README.md CHANGED
@@ -3,8 +3,6 @@
3
3
  Terminalden yapay zeka destekli kod üretimi, dosya yönetimi ve proje otomasyonu.
4
4
  AIGENCY hesabınla bağlanır, projende çalışır, kodu senin onayınla yazar.
5
5
 
6
- > **Önemli:** AIGENCY CLI mevcut web ve mobil AIGENCY hizmetlerini etkilemez. Ayrı bir API namespace (`/api/cli/*`) kullanır, mevcut `/api/v1`, `/api/v2` ve `/api/mobile` endpoint'leri değişmez.
7
-
8
6
  ---
9
7
 
10
8
  ## Kurulum
@@ -183,8 +181,6 @@ Paket yönetimi: [aigency.dev/developer/cli](https://aigency.dev/developer/cli)
183
181
  AI-powered code generation, file management and project automation from your terminal.
184
182
  Connect with your AIGENCY account, work in your project, code is written with your approval.
185
183
 
186
- > **Important:** AIGENCY CLI does not affect existing web and mobile AIGENCY services. It uses a separate API namespace (`/api/cli/*`), existing `/api/v1`, `/api/v2` and `/api/mobile` endpoints remain unchanged.
187
-
188
184
  ---
189
185
 
190
186
  ## Installation
@@ -18,9 +18,22 @@ export function prepareChunk(prompt, history, options = {}) {
18
18
  warning: `Prompt tek başına ${promptTokens} token — context limiti (${available}) aşılıyor. Komutunuzu kısaltın.`,
19
19
  };
20
20
  }
21
- const reversed = [...history].reverse();
22
- const selected = [];
21
+ const pinned = [];
22
+ let remaining = [...history];
23
23
  let total = promptTokens;
24
+ if (remaining.length > 0) {
25
+ const first = remaining[0];
26
+ if (first.role === "user") {
27
+ const firstTokens = first.approxTokens ?? approxTokens(first.content || "");
28
+ if (total + firstTokens <= available) {
29
+ pinned.push(first);
30
+ total += firstTokens;
31
+ remaining = remaining.slice(1);
32
+ }
33
+ }
34
+ }
35
+ const reversed = [...remaining].reverse();
36
+ const selected = [];
24
37
  for (const msg of reversed) {
25
38
  const msgTokens = msg.approxTokens ?? approxTokens(msg.content || "");
26
39
  if (total + msgTokens > available)
@@ -28,12 +41,13 @@ export function prepareChunk(prompt, history, options = {}) {
28
41
  total += msgTokens;
29
42
  selected.unshift(msg);
30
43
  }
31
- const droppedCount = history.length - selected.length;
44
+ const finalHistory = [...pinned, ...selected];
45
+ const droppedCount = history.length - finalHistory.length;
32
46
  const warning = droppedCount > 0
33
47
  ? `Geçmiş kırpıldı: ${droppedCount} eski mesaj atlandı (bağlam limiti)`
34
48
  : null;
35
49
  return {
36
- history: selected,
50
+ history: finalHistory,
37
51
  inputTokens: total,
38
52
  droppedCount,
39
53
  warning,
package/dist/cli.js CHANGED
@@ -82,11 +82,15 @@ export async function runCli(argv) {
82
82
  }
83
83
  const modeFromFlag = parseModeFlag(parsed.flags.mode);
84
84
  const modelFromFlag = parseModelFlag(parsed.flags.model);
85
+ const continueFlag = parsed.flags.continue === true;
86
+ const resumeId = typeof parsed.flags.resume === "string" ? parsed.flags.resume : undefined;
85
87
  try {
86
88
  if (!parsed.command && !parsed.firstPositional) {
87
89
  await runChatCommand({
88
90
  mode: modeFromFlag,
89
91
  model: modelFromFlag,
92
+ continueLastSession: continueFlag,
93
+ resumeSessionId: resumeId,
90
94
  });
91
95
  return 0;
92
96
  }
@@ -98,6 +102,8 @@ export async function runCli(argv) {
98
102
  prompt,
99
103
  mode: modeFromFlag,
100
104
  model: modelFromFlag,
105
+ continueLastSession: continueFlag,
106
+ resumeSessionId: resumeId,
101
107
  });
102
108
  return 0;
103
109
  }
@@ -122,6 +128,9 @@ export async function runCli(argv) {
122
128
  case "init":
123
129
  await runInitCommand({ force: parsed.flags.force === true });
124
130
  return 0;
131
+ case "mcp":
132
+ await (await import("./commands/mcp-server.js")).runMcpServerCommand();
133
+ return 0;
125
134
  case "chat": {
126
135
  const prompt = parsed.positional.join(" ") || undefined;
127
136
  await runChatCommand({
@@ -10,6 +10,7 @@ import { ensureProjectDataDirs } from "../config/bootstrap.js";
10
10
  import { fetchMe } from "../api-client/auth.js";
11
11
  import { App } from "../ui/App.js";
12
12
  import { log } from "../utils/logger.js";
13
+ import { findLatestSession, loadChatHistoryFromSession, } from "../agent/session-store.js";
13
14
  async function findProjectRoot(cwd) {
14
15
  const markers = [
15
16
  ".git",
@@ -73,15 +74,71 @@ export async function runChatCommand(options = {}) {
73
74
  const priorInsights = [instructions.combinedText, memory.summary]
74
75
  .filter((s) => s && s.trim())
75
76
  .join("\n\n---\n\n");
77
+ let restoredHistory = [];
78
+ let restoredSessionId;
79
+ let continuePrompt;
80
+ if (options.continueLastSession || options.resumeSessionId) {
81
+ try {
82
+ let targetSessionId;
83
+ if (options.resumeSessionId) {
84
+ targetSessionId = options.resumeSessionId;
85
+ log.info(`Session restore: ${targetSessionId}`);
86
+ }
87
+ else {
88
+ const latest = await findLatestSession(projectRoot);
89
+ if (latest) {
90
+ targetSessionId = latest.sessionId;
91
+ log.info(`Son session bulundu: ${targetSessionId}`);
92
+ }
93
+ }
94
+ if (targetSessionId) {
95
+ const history = await loadChatHistoryFromSession(targetSessionId, projectRoot);
96
+ if (history.length > 0) {
97
+ restoredHistory = history;
98
+ restoredSessionId = targetSessionId;
99
+ if (!options.prompt) {
100
+ continuePrompt =
101
+ "Kaldığım yerden devam ediyorum. Önce read_tasks ile mevcut plana bak, " +
102
+ "pending ve in_progress task'leri sıraya göre tamamla. " +
103
+ "Tüm task'lar completed olana kadar durma.";
104
+ }
105
+ log.info(`${history.length} mesaj restore edildi`);
106
+ }
107
+ else {
108
+ log.warn("Session bulundu ama boş — yeni oturum başlatılıyor");
109
+ }
110
+ }
111
+ else {
112
+ log.warn("Devam edilecek session bulunamadı — yeni oturum başlatılıyor");
113
+ }
114
+ }
115
+ catch (err) {
116
+ log.warn(`Session restore hatası: ${err.message}`);
117
+ }
118
+ }
119
+ let finalInsights = priorInsights;
120
+ if (restoredHistory.length > 0) {
121
+ const historyBlock = restoredHistory
122
+ .map((m) => `[${m.role === "user" ? "Kullanıcı" : "Asistan"}]: ${m.content.slice(0, 500)}`)
123
+ .slice(-20)
124
+ .join("\n\n");
125
+ finalInsights = [
126
+ priorInsights,
127
+ `## ÖNCEKİ OTURUM (${restoredSessionId || "bilinmiyor"})\nAşağıda önceki oturumdan devam edilen konuşma özeti var:\n\n${historyBlock}`,
128
+ ]
129
+ .filter((s) => s && s.trim())
130
+ .join("\n\n---\n\n");
131
+ log.info(`Session history priorInsights'a enjekte edildi (${restoredHistory.length} mesaj)`);
132
+ }
76
133
  const { waitUntilExit } = render(React.createElement(App, {
77
134
  session,
78
135
  initialMode: mode,
79
136
  initialModel: model,
80
137
  cwd,
81
138
  projectRoot,
82
- initialPrompt: options.prompt,
139
+ initialPrompt: continuePrompt || options.prompt,
83
140
  settings,
84
- memorySummary: priorInsights,
141
+ memorySummary: finalInsights,
85
142
  }), {
86
143
  exitOnCtrlC: false,
87
144
  });
@@ -0,0 +1,4 @@
1
+ import { startMcpServer } from "../mcp/server.js";
2
+ export async function runMcpServerCommand() {
3
+ await startMcpServer();
4
+ }
@@ -39,5 +39,19 @@ export const SettingsSchema = z.object({
39
39
  theme: z.enum(["dark", "auto"]).default("auto"),
40
40
  })
41
41
  .default({ theme: "auto" }),
42
+ mcp: z
43
+ .object({
44
+ servers: z
45
+ .array(z.object({
46
+ name: z.string().regex(/^[a-z0-9_-]+$/),
47
+ command: z.string(),
48
+ args: z.array(z.string()).default([]),
49
+ env: z.record(z.string()).default({}),
50
+ autoStart: z.boolean().default(true),
51
+ enabled: z.boolean().default(true),
52
+ }))
53
+ .default([]),
54
+ })
55
+ .default({ servers: [] }),
42
56
  });
43
57
  export const DEFAULT_SETTINGS = SettingsSchema.parse({});
@@ -0,0 +1,156 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import { log } from "../utils/logger.js";
4
+ export class McpClientManager {
5
+ servers = new Map();
6
+ async startAll(configs) {
7
+ const eligible = configs.filter((c) => c.enabled && c.autoStart);
8
+ if (eligible.length === 0)
9
+ return;
10
+ log.info(`MCP: ${eligible.length} server başlatılıyor...`);
11
+ const results = await Promise.allSettled(eligible.map((c) => this.startServer(c)));
12
+ let ready = 0;
13
+ for (const r of results) {
14
+ if (r.status === "fulfilled")
15
+ ready++;
16
+ }
17
+ log.info(`MCP: ${ready}/${eligible.length} server hazır`);
18
+ }
19
+ async startServer(config) {
20
+ if (this.servers.has(config.name)) {
21
+ log.warn(`MCP: ${config.name} zaten çalışıyor, atlanıyor`);
22
+ return;
23
+ }
24
+ const instance = {
25
+ name: config.name,
26
+ config,
27
+ client: null,
28
+ transport: null,
29
+ tools: [],
30
+ status: "starting",
31
+ };
32
+ this.servers.set(config.name, instance);
33
+ try {
34
+ const transport = new StdioClientTransport({
35
+ command: config.command,
36
+ args: config.args,
37
+ env: { ...process.env, ...config.env },
38
+ });
39
+ const client = new Client({ name: "aigency-cli", version: "1.0.0" }, { capabilities: {} });
40
+ instance.transport = transport;
41
+ instance.client = client;
42
+ await client.connect(transport);
43
+ const toolsResult = await client.listTools();
44
+ instance.tools = (toolsResult.tools || []).map((t) => ({
45
+ qualifiedName: `mcp_${config.name}_${t.name}`,
46
+ originalName: t.name,
47
+ serverName: config.name,
48
+ description: t.description || "",
49
+ inputSchema: t.inputSchema || { type: "object", properties: {} },
50
+ }));
51
+ instance.status = "ready";
52
+ log.info(`MCP: ${config.name} hazır (${instance.tools.length} tool)`);
53
+ }
54
+ catch (err) {
55
+ instance.status = "error";
56
+ instance.error = err.message;
57
+ log.error(`MCP: ${config.name} başlatılamadı: ${instance.error}`);
58
+ }
59
+ }
60
+ async stopAll() {
61
+ const names = [...this.servers.keys()];
62
+ await Promise.allSettled(names.map((n) => this.stopServer(n)));
63
+ this.servers.clear();
64
+ }
65
+ async stopServer(name) {
66
+ const instance = this.servers.get(name);
67
+ if (!instance)
68
+ return;
69
+ try {
70
+ await instance.client?.close();
71
+ }
72
+ catch {
73
+ }
74
+ instance.status = "stopped";
75
+ this.servers.delete(name);
76
+ log.debug(`MCP: ${name} durduruldu`);
77
+ }
78
+ getAllTools() {
79
+ const tools = [];
80
+ for (const instance of this.servers.values()) {
81
+ if (instance.status === "ready") {
82
+ tools.push(...instance.tools);
83
+ }
84
+ }
85
+ return tools;
86
+ }
87
+ toLlmTools() {
88
+ return this.getAllTools().map((t) => ({
89
+ type: "function",
90
+ function: {
91
+ name: t.qualifiedName,
92
+ description: `[MCP: ${t.serverName}] ${t.description}`,
93
+ parameters: t.inputSchema,
94
+ },
95
+ }));
96
+ }
97
+ async callTool(qualifiedName, params) {
98
+ const start = Date.now();
99
+ const match = qualifiedName.match(/^mcp_([^_]+)_(.+)$/);
100
+ if (!match) {
101
+ return {
102
+ success: false,
103
+ output: "",
104
+ error: `Geçersiz MCP tool adı: ${qualifiedName}`,
105
+ duration: Date.now() - start,
106
+ };
107
+ }
108
+ const serverName = match[1];
109
+ const toolName = match[2];
110
+ const instance = this.servers.get(serverName);
111
+ if (!instance || instance.status !== "ready") {
112
+ return {
113
+ success: false,
114
+ output: "",
115
+ error: `MCP server '${serverName}' hazır değil`,
116
+ duration: Date.now() - start,
117
+ };
118
+ }
119
+ try {
120
+ const result = await instance.client.callTool({
121
+ name: toolName,
122
+ arguments: params,
123
+ });
124
+ const textContent = result.content
125
+ ?.filter((c) => c.type === "text")
126
+ ?.map((c) => c.text)
127
+ ?.join("\n") || "";
128
+ return {
129
+ success: !result.isError,
130
+ output: textContent,
131
+ error: result.isError ? textContent.slice(0, 200) : undefined,
132
+ metadata: { mcpServer: serverName, mcpTool: toolName },
133
+ duration: Date.now() - start,
134
+ };
135
+ }
136
+ catch (err) {
137
+ return {
138
+ success: false,
139
+ output: "",
140
+ error: `MCP çağrı hatası: ${err.message.slice(0, 150)}`,
141
+ duration: Date.now() - start,
142
+ };
143
+ }
144
+ }
145
+ getStatuses() {
146
+ return [...this.servers.values()].map((s) => ({
147
+ name: s.name,
148
+ status: s.status,
149
+ toolCount: s.tools.length,
150
+ error: s.error,
151
+ }));
152
+ }
153
+ hasTools() {
154
+ return this.getAllTools().length > 0;
155
+ }
156
+ }
@@ -0,0 +1,46 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
+ import { dispatchLocalTool } from "../tools/registry.js";
5
+ import { EXPOSED_TOOL_SCHEMAS } from "../tools/metadata.js";
6
+ const CLI_VERSION = "0.4.0";
7
+ export async function startMcpServer() {
8
+ const server = new Server({ name: "aigency-cli", version: CLI_VERSION }, { capabilities: { tools: {} } });
9
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
10
+ tools: EXPOSED_TOOL_SCHEMAS.map((t) => ({
11
+ name: t.name,
12
+ description: t.description,
13
+ inputSchema: t.parameters,
14
+ })),
15
+ }));
16
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
17
+ const { name, arguments: args } = request.params;
18
+ const isExposed = EXPOSED_TOOL_SCHEMAS.some((t) => t.name === name);
19
+ if (!isExposed) {
20
+ return {
21
+ content: [{ type: "text", text: `Tool '${name}' MCP üzerinden erişilemez` }],
22
+ isError: true,
23
+ };
24
+ }
25
+ const ctx = {
26
+ cwd: process.cwd(),
27
+ projectRoot: process.cwd(),
28
+ mode: "accept_edits",
29
+ };
30
+ try {
31
+ const result = await dispatchLocalTool(name, (args || {}), ctx);
32
+ return {
33
+ content: [{ type: "text", text: result.output || result.error || "(boş)" }],
34
+ isError: !result.success,
35
+ };
36
+ }
37
+ catch (err) {
38
+ return {
39
+ content: [{ type: "text", text: `Hata: ${err.message.slice(0, 200)}` }],
40
+ isError: true,
41
+ };
42
+ }
43
+ });
44
+ const transport = new StdioServerTransport();
45
+ await server.connect(transport);
46
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,149 @@
1
+ import { execSync } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ const SYSTEM_DEPS = [
6
+ { name: "node", command: "node", required: true, versionFlag: "--version" },
7
+ { name: "npm", command: "npm", required: true, versionFlag: "--version" },
8
+ { name: "git", command: "git", required: false, versionFlag: "--version" },
9
+ { name: "python3", command: os.platform() === "win32" ? "python" : "python3", required: false, versionFlag: "--version" },
10
+ { name: "docker", command: "docker", required: false, versionFlag: "--version" },
11
+ ];
12
+ function getInstallHint(name, platform) {
13
+ const hints = {
14
+ node: {
15
+ win32: "choco install nodejs-lts veya https://nodejs.org",
16
+ darwin: "brew install node",
17
+ linux: "sudo apt install nodejs npm veya https://nodejs.org",
18
+ },
19
+ npm: {
20
+ win32: "Node.js ile birlikte gelir — node kur",
21
+ darwin: "Node.js ile birlikte gelir — brew install node",
22
+ linux: "Node.js ile birlikte gelir — sudo apt install nodejs npm",
23
+ },
24
+ git: {
25
+ win32: "choco install git veya https://git-scm.com",
26
+ darwin: "xcode-select --install veya brew install git",
27
+ linux: "sudo apt install git",
28
+ },
29
+ python3: {
30
+ win32: "choco install python veya https://python.org",
31
+ darwin: "brew install python3",
32
+ linux: "sudo apt install python3",
33
+ },
34
+ docker: {
35
+ win32: "https://docs.docker.com/desktop/install/windows-install/",
36
+ darwin: "brew install --cask docker",
37
+ linux: "sudo apt install docker.io docker-compose",
38
+ },
39
+ };
40
+ return hints[name]?.[platform] || `${name} kurulumu için dökümantasyona bakın`;
41
+ }
42
+ function checkCommand(command, versionFlag) {
43
+ try {
44
+ const result = execSync(`${command} ${versionFlag}`, {
45
+ timeout: 5000,
46
+ encoding: "utf8",
47
+ stdio: ["pipe", "pipe", "pipe"],
48
+ });
49
+ return result.trim().split("\n")[0] || "kurulu";
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ export const checkDependenciesHandler = async (params, ctx) => {
56
+ const start = Date.now();
57
+ const projectPath = (params.path || params.projectPath || ".");
58
+ const resolved = path.resolve(ctx.cwd, projectPath);
59
+ const platform = os.platform();
60
+ const results = [];
61
+ const missing = [];
62
+ const warnings = [];
63
+ results.push("## Sistem Bağımlılıkları\n");
64
+ results.push(`Platform: ${platform} (${os.arch()})`);
65
+ results.push(`Node sürümü gereksinimi: >= 18.18.0\n`);
66
+ for (const dep of SYSTEM_DEPS) {
67
+ const version = checkCommand(dep.command, dep.versionFlag);
68
+ if (version) {
69
+ results.push(`✓ ${dep.name}: ${version}`);
70
+ if (dep.name === "node") {
71
+ const match = version.match(/v?(\d+)\.(\d+)/);
72
+ if (match) {
73
+ const major = parseInt(match[1], 10);
74
+ const minor = parseInt(match[2], 10);
75
+ if (major < 18 || (major === 18 && minor < 18)) {
76
+ warnings.push(`Node.js ${version} çok eski — 18.18+ gerekli`);
77
+ }
78
+ }
79
+ }
80
+ }
81
+ else {
82
+ const status = dep.required ? "✗ (ZORUNLU)" : "✗ (opsiyonel)";
83
+ results.push(`${status} ${dep.name}: kurulu değil`);
84
+ results.push(` → ${getInstallHint(dep.name, platform)}`);
85
+ if (dep.required)
86
+ missing.push(dep.name);
87
+ }
88
+ }
89
+ results.push("\n## Proje Bağımlılıkları\n");
90
+ const pkgPath = path.join(resolved, "package.json");
91
+ try {
92
+ const pkgContent = await fs.readFile(pkgPath, "utf8");
93
+ const pkg = JSON.parse(pkgContent);
94
+ results.push(`Proje: ${pkg.name || "(isimsiz)"} v${pkg.version || "0.0.0"}`);
95
+ const requiredScripts = ["dev", "build"];
96
+ const scripts = pkg.scripts || {};
97
+ for (const s of requiredScripts) {
98
+ if (scripts[s]) {
99
+ results.push(`✓ scripts.${s}: ${scripts[s]}`);
100
+ }
101
+ else {
102
+ warnings.push(`scripts.${s} eksik`);
103
+ results.push(`✗ scripts.${s}: tanımlı değil`);
104
+ }
105
+ }
106
+ try {
107
+ await fs.access(path.join(resolved, "node_modules"));
108
+ results.push("✓ node_modules: mevcut");
109
+ }
110
+ catch {
111
+ missing.push("node_modules");
112
+ results.push("✗ node_modules: eksik → npm install çalıştır");
113
+ }
114
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
115
+ const keyDeps = ["next", "react", "typescript", "tailwindcss"];
116
+ for (const dep of keyDeps) {
117
+ if (allDeps[dep]) {
118
+ results.push(`✓ ${dep}: ${allDeps[dep]}`);
119
+ }
120
+ }
121
+ }
122
+ catch {
123
+ results.push("package.json bulunamadı — henüz proje oluşturulmamış olabilir");
124
+ }
125
+ results.push("\n## Özet\n");
126
+ if (missing.length === 0 && warnings.length === 0) {
127
+ results.push("✓ Tüm bağımlılıklar hazır — projeye başlanabilir.");
128
+ }
129
+ else {
130
+ if (missing.length > 0) {
131
+ results.push(`✗ Eksik (zorunlu): ${missing.join(", ")}`);
132
+ }
133
+ if (warnings.length > 0) {
134
+ results.push(`⚠ Uyarılar: ${warnings.join("; ")}`);
135
+ }
136
+ }
137
+ return {
138
+ success: missing.length === 0,
139
+ output: results.join("\n"),
140
+ metadata: {
141
+ platform,
142
+ arch: os.arch(),
143
+ missing,
144
+ warnings,
145
+ hasMissing: missing.length > 0,
146
+ },
147
+ duration: Date.now() - start,
148
+ };
149
+ };
@@ -0,0 +1,76 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ const ALL_SHADCN_COMPONENTS = [
4
+ "accordion", "alert", "alert-dialog", "aspect-ratio", "avatar",
5
+ "badge", "breadcrumb", "button", "calendar", "card", "carousel",
6
+ "chart", "checkbox", "collapsible", "combobox", "command",
7
+ "context-menu", "data-table", "date-picker", "dialog", "drawer",
8
+ "dropdown-menu", "form", "hover-card", "input", "input-otp",
9
+ "label", "menubar", "navigation-menu", "pagination", "popover",
10
+ "progress", "radio-group", "resizable", "scroll-area", "select",
11
+ "separator", "sheet", "sidebar", "skeleton", "slider", "sonner",
12
+ "switch", "table", "tabs", "textarea", "toast", "toggle",
13
+ "toggle-group", "tooltip",
14
+ ];
15
+ async function scanComponents(projectRoot) {
16
+ const componentsDir = path.join(projectRoot, "components", "ui");
17
+ const configPath = path.join(projectRoot, "components.json");
18
+ let hasConfig = false;
19
+ try {
20
+ await fs.access(configPath);
21
+ hasConfig = true;
22
+ }
23
+ catch {
24
+ }
25
+ const installed = [];
26
+ try {
27
+ const files = await fs.readdir(componentsDir);
28
+ for (const file of files) {
29
+ const name = file.replace(/\.(tsx|ts|jsx|js)$/, "");
30
+ if (name && !name.startsWith(".")) {
31
+ installed.push(name);
32
+ }
33
+ }
34
+ }
35
+ catch {
36
+ }
37
+ const installedSet = new Set(installed);
38
+ const available = ALL_SHADCN_COMPONENTS.filter((c) => !installedSet.has(c));
39
+ return { installed, available, hasConfig, componentsDir };
40
+ }
41
+ export const listComponentsHandler = async (params, ctx) => {
42
+ const start = Date.now();
43
+ const projectPath = (params.path || params.projectPath || ".");
44
+ const resolved = path.resolve(ctx.cwd, projectPath);
45
+ const result = await scanComponents(resolved);
46
+ const lines = [];
47
+ lines.push("## shadcn/ui Component Durumu\n");
48
+ if (!result.hasConfig) {
49
+ lines.push("⚠ components.json bulunamadı — shadcn/ui henüz init edilmemiş olabilir.");
50
+ lines.push("Kurulum: `npx --yes shadcn@latest init --yes --defaults`\n");
51
+ }
52
+ if (result.installed.length > 0) {
53
+ lines.push(`### Kurulu (${result.installed.length})`);
54
+ lines.push(result.installed.map((c) => `✓ ${c}`).join("\n"));
55
+ lines.push("");
56
+ }
57
+ else {
58
+ lines.push("### Kurulu: (yok)\n");
59
+ }
60
+ if (result.available.length > 0) {
61
+ lines.push(`### Kurulabilir (${result.available.length})`);
62
+ lines.push(result.available.map((c) => ` ${c}`).join("\n"));
63
+ lines.push("");
64
+ lines.push("Kurulum: `npx --yes shadcn@latest add <component> -y`");
65
+ }
66
+ return {
67
+ success: true,
68
+ output: lines.join("\n"),
69
+ metadata: {
70
+ installed: result.installed,
71
+ available: result.available,
72
+ hasConfig: result.hasConfig,
73
+ },
74
+ duration: Date.now() - start,
75
+ };
76
+ };
@@ -0,0 +1,125 @@
1
+ import { log } from "../utils/logger.js";
2
+ const DOCS_URLS = {
3
+ "next.js": "https://nextjs.org/docs",
4
+ nextjs: "https://nextjs.org/docs",
5
+ next: "https://nextjs.org/docs",
6
+ react: "https://react.dev/reference",
7
+ tailwind: "https://tailwindcss.com/docs",
8
+ tailwindcss: "https://tailwindcss.com/docs",
9
+ prisma: "https://www.prisma.io/docs",
10
+ shadcn: "https://ui.shadcn.com/docs",
11
+ "shadcn/ui": "https://ui.shadcn.com/docs",
12
+ typescript: "https://www.typescriptlang.org/docs/handbook",
13
+ ts: "https://www.typescriptlang.org/docs/handbook",
14
+ node: "https://nodejs.org/docs/latest/api",
15
+ nodejs: "https://nodejs.org/docs/latest/api",
16
+ vite: "https://vitejs.dev/guide",
17
+ zod: "https://zod.dev",
18
+ "framer-motion": "https://www.framer.com/motion",
19
+ framer: "https://www.framer.com/motion",
20
+ express: "https://expressjs.com/en/5x/api.html",
21
+ "ink": "https://github.com/vadimdemedes/ink",
22
+ };
23
+ export function listKnownFrameworks() {
24
+ return [...new Set(Object.values(DOCS_URLS))].map((url) => {
25
+ const match = Object.entries(DOCS_URLS).find(([, v]) => v === url);
26
+ return match?.[0] || url;
27
+ });
28
+ }
29
+ export function buildDocsUrl(framework, topic) {
30
+ const key = framework.toLowerCase().trim();
31
+ const base = DOCS_URLS[key];
32
+ if (!base)
33
+ return null;
34
+ if (!topic)
35
+ return base;
36
+ const slug = topic
37
+ .toLowerCase()
38
+ .trim()
39
+ .replace(/\s+/g, "-")
40
+ .replace(/[^a-z0-9\-/]/g, "");
41
+ return `${base}/${slug}`;
42
+ }
43
+ function extractText(html) {
44
+ return html
45
+ .replace(/<script[\s\S]*?<\/script>/gi, "")
46
+ .replace(/<style[\s\S]*?<\/style>/gi, "")
47
+ .replace(/<nav[\s\S]*?<\/nav>/gi, "")
48
+ .replace(/<footer[\s\S]*?<\/footer>/gi, "")
49
+ .replace(/<[^>]+>/g, " ")
50
+ .replace(/&amp;/g, "&")
51
+ .replace(/&lt;/g, "<")
52
+ .replace(/&gt;/g, ">")
53
+ .replace(/&quot;/g, '"')
54
+ .replace(/&#39;/g, "'")
55
+ .replace(/&nbsp;/g, " ")
56
+ .replace(/\s+/g, " ")
57
+ .trim();
58
+ }
59
+ async function fetchDocsContent(url, maxChars = 15_000) {
60
+ const controller = new AbortController();
61
+ const timeout = setTimeout(() => controller.abort(), 15_000);
62
+ try {
63
+ const res = await fetch(url, {
64
+ headers: {
65
+ "User-Agent": "AIGENCY-CLI/1.0 (docs-lookup)",
66
+ Accept: "text/html,application/xhtml+xml",
67
+ },
68
+ signal: controller.signal,
69
+ });
70
+ if (!res.ok) {
71
+ return `HTTP ${res.status} — ${url} erişilemedi`;
72
+ }
73
+ const html = await res.text();
74
+ const text = extractText(html);
75
+ return text.slice(0, maxChars);
76
+ }
77
+ catch (err) {
78
+ if (err.name === "AbortError") {
79
+ return `Zaman aşımı — ${url} 15 saniyede yanıt vermedi`;
80
+ }
81
+ return `Hata: ${err.message.slice(0, 100)}`;
82
+ }
83
+ finally {
84
+ clearTimeout(timeout);
85
+ }
86
+ }
87
+ export const docsLookupHandler = async (params) => {
88
+ const start = Date.now();
89
+ const framework = String(params.framework || "").trim();
90
+ const topic = params.topic ? String(params.topic).trim() : undefined;
91
+ const directUrl = params.url ? String(params.url).trim() : undefined;
92
+ if (!framework && !directUrl) {
93
+ return {
94
+ success: false,
95
+ output: "",
96
+ error: "framework veya url parametresi gerekli",
97
+ duration: Date.now() - start,
98
+ };
99
+ }
100
+ let url;
101
+ if (directUrl) {
102
+ url = directUrl;
103
+ }
104
+ else {
105
+ const built = buildDocsUrl(framework, topic);
106
+ if (!built) {
107
+ const known = Object.keys(DOCS_URLS).slice(0, 10).join(", ");
108
+ return {
109
+ success: false,
110
+ output: "",
111
+ error: `'${framework}' için docs URL bulunamadı. Bilinenler: ${known}`,
112
+ duration: Date.now() - start,
113
+ };
114
+ }
115
+ url = built;
116
+ }
117
+ log.debug(`docs_lookup: ${url}`);
118
+ const content = await fetchDocsContent(url);
119
+ return {
120
+ success: true,
121
+ output: `## Dokümantasyon: ${framework || url}\nKaynak: ${url}\n\n${content}`,
122
+ metadata: { url, framework, topic },
123
+ duration: Date.now() - start,
124
+ };
125
+ };
@@ -0,0 +1,103 @@
1
+ export const EXPOSED_TOOL_SCHEMAS = [
2
+ {
3
+ name: "read_file",
4
+ description: "Read a file's contents with optional line range",
5
+ parameters: {
6
+ type: "object",
7
+ properties: {
8
+ path: { type: "string", description: "File path to read" },
9
+ startLine: { type: "number", description: "Start line (1-based, optional)" },
10
+ endLine: { type: "number", description: "End line (optional)" },
11
+ },
12
+ required: ["path"],
13
+ },
14
+ },
15
+ {
16
+ name: "list_files",
17
+ description: "List files and directories in a path",
18
+ parameters: {
19
+ type: "object",
20
+ properties: {
21
+ path: { type: "string", description: "Directory path" },
22
+ recursive: { type: "boolean", description: "Recurse into subdirectories" },
23
+ maxDepth: { type: "number", description: "Max recursion depth" },
24
+ },
25
+ required: ["path"],
26
+ },
27
+ },
28
+ {
29
+ name: "search_files",
30
+ description: "Search file contents with regex or literal pattern",
31
+ parameters: {
32
+ type: "object",
33
+ properties: {
34
+ pattern: { type: "string", description: "Search pattern" },
35
+ path: { type: "string", description: "Directory to search in" },
36
+ isRegex: { type: "boolean", description: "Treat pattern as regex" },
37
+ caseSensitive: { type: "boolean", description: "Case sensitive search" },
38
+ maxResults: { type: "number", description: "Max results to return" },
39
+ },
40
+ required: ["pattern"],
41
+ },
42
+ },
43
+ {
44
+ name: "write_file",
45
+ description: "Create a new file or overwrite an existing file",
46
+ parameters: {
47
+ type: "object",
48
+ properties: {
49
+ path: { type: "string", description: "File path" },
50
+ content: { type: "string", description: "File content to write" },
51
+ },
52
+ required: ["path", "content"],
53
+ },
54
+ },
55
+ {
56
+ name: "edit_file",
57
+ description: "Replace a text block in an existing file",
58
+ parameters: {
59
+ type: "object",
60
+ properties: {
61
+ path: { type: "string", description: "File path" },
62
+ old_string: { type: "string", description: "Text to find" },
63
+ new_string: { type: "string", description: "Replacement text" },
64
+ },
65
+ required: ["path", "old_string", "new_string"],
66
+ },
67
+ },
68
+ {
69
+ name: "bash_execute",
70
+ description: "Execute a shell command (non-interactive, 5 min timeout)",
71
+ parameters: {
72
+ type: "object",
73
+ properties: {
74
+ command: { type: "string", description: "Shell command to run" },
75
+ working_directory: { type: "string", description: "Working directory (optional)" },
76
+ timeout_ms: { type: "number", description: "Timeout in milliseconds" },
77
+ },
78
+ required: ["command"],
79
+ },
80
+ },
81
+ {
82
+ name: "verify_project",
83
+ description: "Health check for Node.js/Next.js/React/Vite projects",
84
+ parameters: {
85
+ type: "object",
86
+ properties: {
87
+ path: { type: "string", description: "Project directory path" },
88
+ type: { type: "string", enum: ["auto", "next", "react", "vite", "node"], description: "Project type" },
89
+ },
90
+ required: ["path"],
91
+ },
92
+ },
93
+ {
94
+ name: "check_dependencies",
95
+ description: "Check system and project dependencies (Node.js, npm, git, etc.)",
96
+ parameters: {
97
+ type: "object",
98
+ properties: {
99
+ path: { type: "string", description: "Project directory (optional)" },
100
+ },
101
+ },
102
+ },
103
+ ];
@@ -9,6 +9,9 @@ import { saveMemoryToolHandler, readMemoryToolHandler, deleteMemoryToolHandler,
9
9
  import { writeTasksHandler, readTasksHandler } from "./task-tools.js";
10
10
  import { verifyProjectHandler } from "./verify-project.js";
11
11
  import { assertDoneHandler } from "./assert-done.js";
12
+ import { checkDependenciesHandler } from "./check-dependencies.js";
13
+ import { docsLookupHandler } from "./docs-lookup.js";
14
+ import { listComponentsHandler } from "./components.js";
12
15
  const HANDLERS = {
13
16
  read_file: readFileTool,
14
17
  list_files: listFilesTool,
@@ -24,6 +27,9 @@ const HANDLERS = {
24
27
  read_tasks: readTasksHandler,
25
28
  verify_project: verifyProjectHandler,
26
29
  assert_done: assertDoneHandler,
30
+ check_dependencies: checkDependenciesHandler,
31
+ docs_lookup: docsLookupHandler,
32
+ list_components: listComponentsHandler,
27
33
  };
28
34
  export function hasLocalTool(name) {
29
35
  return name in HANDLERS;
@@ -56,3 +62,6 @@ export async function dispatchLocalTool(toolName, params, ctx) {
56
62
  export function listLocalTools() {
57
63
  return Object.keys(HANDLERS);
58
64
  }
65
+ export function isMcpTool(name) {
66
+ return name.startsWith("mcp_");
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigencydev/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "AIGENCY CLI — terminalden yapay zeka destekli kod üretimi, dosya yönetimi ve proje otomasyonu.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,6 +26,7 @@
26
26
  "node": ">=18.18.0"
27
27
  },
28
28
  "dependencies": {
29
+ "@modelcontextprotocol/sdk": "^1.29.0",
29
30
  "commander": "^12.1.0",
30
31
  "ink": "^5.0.1",
31
32
  "ink-spinner": "^5.0.0",