tuticli 0.1.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.
Files changed (3) hide show
  1. package/README.md +63 -0
  2. package/bin/cli.js +2001 -0
  3. package/package.json +49 -0
package/bin/cli.js ADDED
@@ -0,0 +1,2001 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/ui/chat.ts
7
+ import { createInterface } from "readline";
8
+ import { stdin as input, stdout as output } from "process";
9
+ import chalk3 from "chalk";
10
+
11
+ // src/providers/claudev3.ts
12
+ import axios from "axios";
13
+
14
+ // src/providers/base.ts
15
+ var BaseProvider = class {
16
+ validateConfig(config2) {
17
+ if (!config2.apiKey) {
18
+ throw new Error("API key n\xE3o configurada. Use: tuticli config set api-key SUA_API_KEY");
19
+ }
20
+ if (!config2.endpoint) {
21
+ throw new Error("Endpoint n\xE3o configurado");
22
+ }
23
+ }
24
+ };
25
+
26
+ // src/utils/logger.ts
27
+ import chalk from "chalk";
28
+ var d = chalk.dim;
29
+ var b = chalk.bold;
30
+ function timestamp() {
31
+ return d((/* @__PURE__ */ new Date()).toLocaleTimeString());
32
+ }
33
+ var logger = {
34
+ info(msg, ...args) {
35
+ console.log(`${timestamp()} ${b("\u2139")} ${msg}`, ...args);
36
+ },
37
+ success(msg, ...args) {
38
+ console.log(`${timestamp()} ${b("\u2714")} ${msg}`, ...args);
39
+ },
40
+ warn(msg, ...args) {
41
+ console.log(`${timestamp()} ${b("\u26A0")} ${msg}`, ...args);
42
+ },
43
+ error(msg, ...args) {
44
+ console.error(`${timestamp()} ${b("\u2716")} ${msg}`, ...args);
45
+ },
46
+ debug(msg, ...args) {
47
+ if (process.env.DEBUG) {
48
+ console.log(`${timestamp()} ${b("\u25CF")} ${d(msg)}`, ...args);
49
+ }
50
+ },
51
+ raw(msg) {
52
+ console.log(msg);
53
+ }
54
+ };
55
+
56
+ // src/providers/claudev3.ts
57
+ var ClaudeV3Provider = class extends BaseProvider {
58
+ name = "claude-v3";
59
+ buildMessages(messages) {
60
+ const result = [];
61
+ for (const m of messages) {
62
+ if (m.role !== "user" && m.role !== "assistant") continue;
63
+ const content = typeof m.content === "string" ? [{ type: "text", text: m.content }] : m.content;
64
+ result.push({
65
+ role: m.role,
66
+ content
67
+ });
68
+ }
69
+ return result;
70
+ }
71
+ buildTools(tools) {
72
+ if (!tools || tools.length === 0) return void 0;
73
+ return tools.map((t) => ({
74
+ name: t.name,
75
+ description: t.description,
76
+ input_schema: t.input_schema
77
+ }));
78
+ }
79
+ async chat(messages, config2, tools) {
80
+ this.validateConfig(config2);
81
+ const request = {
82
+ model: config2.model,
83
+ max_tokens: config2.maxTokens,
84
+ messages: this.buildMessages(messages),
85
+ stream: false
86
+ };
87
+ const apiTools = this.buildTools(tools);
88
+ if (apiTools) request.tools = apiTools;
89
+ try {
90
+ const response = await axios.post(config2.endpoint, request, {
91
+ headers: {
92
+ "x-api-key": config2.apiKey,
93
+ "Content-Type": "application/json"
94
+ },
95
+ timeout: 3e5
96
+ });
97
+ const data = response.data;
98
+ let text = "";
99
+ const toolCalls = [];
100
+ for (const block of data.content || []) {
101
+ if (block.type === "text") {
102
+ text += block.text || "";
103
+ } else if (block.type === "tool_use") {
104
+ toolCalls.push({
105
+ id: block.id || "",
106
+ name: block.name || "",
107
+ input: JSON.stringify(block.input || {})
108
+ });
109
+ }
110
+ }
111
+ return { text, toolCalls };
112
+ } catch (err) {
113
+ const axiosErr = err;
114
+ if (axiosErr.response) {
115
+ const status = axiosErr.response.status;
116
+ const msg = axiosErr.response.data?.error?.message || axiosErr.message;
117
+ throw new Error(`API error ${status}: ${msg}`);
118
+ }
119
+ throw new Error(`Falha na requisi\xE7\xE3o: ${axiosErr.message}`);
120
+ }
121
+ }
122
+ async *chatStream(messages, config2, tools) {
123
+ this.validateConfig(config2);
124
+ const request = {
125
+ model: config2.model,
126
+ max_tokens: config2.maxTokens,
127
+ messages: this.buildMessages(messages),
128
+ stream: true
129
+ };
130
+ const apiTools = this.buildTools(tools);
131
+ if (apiTools) request.tools = apiTools;
132
+ try {
133
+ const response = await axios.post(config2.endpoint, request, {
134
+ headers: {
135
+ "x-api-key": config2.apiKey,
136
+ "Content-Type": "application/json",
137
+ Accept: "text/event-stream"
138
+ },
139
+ responseType: "stream",
140
+ timeout: 3e5
141
+ });
142
+ const stream = response.data;
143
+ yield* this.parseSSE(stream);
144
+ } catch (err) {
145
+ const axiosErr = err;
146
+ if (axiosErr.response) {
147
+ const status = axiosErr.response.status;
148
+ let errorMsg = `API error ${status}`;
149
+ try {
150
+ const data = axiosErr.response.data;
151
+ const body = typeof data === "string" ? data : JSON.stringify(data);
152
+ const parsed = JSON.parse(body);
153
+ if (parsed.error?.message) {
154
+ errorMsg += `: ${parsed.error.message}`;
155
+ }
156
+ } catch {
157
+ errorMsg += `: ${axiosErr.message}`;
158
+ }
159
+ yield { type: "error", error: errorMsg };
160
+ } else {
161
+ yield { type: "error", error: `Falha na requisi\xE7\xE3o: ${axiosErr.message}` };
162
+ }
163
+ }
164
+ }
165
+ async *parseSSE(stream) {
166
+ let buffer = "";
167
+ const toolInputBuffers = {};
168
+ const toolCalls = {};
169
+ let textAccumulator = "";
170
+ for await (const chunk of stream) {
171
+ buffer += chunk.toString();
172
+ const lines = buffer.split("\n");
173
+ buffer = lines.pop() || "";
174
+ for (const line of lines) {
175
+ const trimmed = line.trim();
176
+ if (!trimmed.startsWith("data: ")) continue;
177
+ const dataStr = trimmed.slice(6).trim();
178
+ if (!dataStr || dataStr === "[DONE]") {
179
+ continue;
180
+ }
181
+ try {
182
+ const data = JSON.parse(dataStr);
183
+ if (data.type === "content_block_start") {
184
+ const idx = data.index;
185
+ const block = data.content_block;
186
+ if (block.type === "tool_use") {
187
+ toolCalls[idx] = { id: block.id, name: block.name };
188
+ toolInputBuffers[idx] = "";
189
+ } else if (block.type === "text") {
190
+ textAccumulator = "";
191
+ }
192
+ } else if (data.type === "content_block_delta") {
193
+ const idx = data.index;
194
+ const delta = data.delta;
195
+ if (!delta) continue;
196
+ if (delta.type === "text_delta") {
197
+ textAccumulator += delta.text || "";
198
+ } else if (delta.type === "input_json_delta") {
199
+ toolInputBuffers[idx] = (toolInputBuffers[idx] || "") + (delta.partial_json || "");
200
+ }
201
+ } else if (data.type === "content_block_stop") {
202
+ const idx = data.index;
203
+ if (toolCalls[idx] && toolInputBuffers[idx] !== void 0) {
204
+ const tc = toolCalls[idx];
205
+ const fullInput = toolInputBuffers[idx];
206
+ yield {
207
+ type: "tool_use",
208
+ tool_call: {
209
+ id: tc.id,
210
+ name: tc.name,
211
+ input: fullInput || "{}"
212
+ }
213
+ };
214
+ }
215
+ if (!toolCalls[idx] && textAccumulator) {
216
+ if (textAccumulator.trim()) {
217
+ yield { type: "content", content: textAccumulator };
218
+ }
219
+ textAccumulator = "";
220
+ }
221
+ } else if (data.type === "message_stop" || data.type === "done") {
222
+ if (textAccumulator.trim()) {
223
+ yield { type: "content", content: textAccumulator };
224
+ }
225
+ yield { type: "done" };
226
+ return;
227
+ } else if (data.type === "message_delta") {
228
+ if (data.delta?.stop_reason === "end_turn" || data.delta?.stop_reason === "stop") {
229
+ if (textAccumulator.trim()) {
230
+ yield { type: "content", content: textAccumulator };
231
+ }
232
+ textAccumulator = "";
233
+ yield { type: "done" };
234
+ return;
235
+ }
236
+ } else if (data.type === "error") {
237
+ yield { type: "error", error: data.error?.message || "Erro desconhecido" };
238
+ }
239
+ } catch {
240
+ if (dataStr !== "[DONE]") {
241
+ logger.debug(`SSE parse error for: ${dataStr}`);
242
+ }
243
+ }
244
+ }
245
+ }
246
+ yield { type: "done" };
247
+ }
248
+ async listModels(config2) {
249
+ this.validateConfig(config2);
250
+ try {
251
+ const response = await axios.get(config2.endpoint.replace("/messages", "/models"), {
252
+ headers: {
253
+ "x-api-key": config2.apiKey,
254
+ "Content-Type": "application/json"
255
+ },
256
+ timeout: 3e4
257
+ });
258
+ const data = response.data;
259
+ if (Array.isArray(data.models)) {
260
+ return data.models.map((m) => m.id || m.name || String(m));
261
+ }
262
+ if (Array.isArray(data.data)) {
263
+ return data.data.map((m) => m.id || String(m));
264
+ }
265
+ return [config2.model, "claude-opus-4-7", "claude-sonnet-4-7", "claude-haiku-4-7"];
266
+ } catch {
267
+ return [
268
+ config2.model,
269
+ "claude-opus-4-7",
270
+ "claude-sonnet-4-7",
271
+ "claude-haiku-4-7",
272
+ "claude-3-opus-latest",
273
+ "claude-3-sonnet-latest",
274
+ "claude-3-haiku-latest"
275
+ ];
276
+ }
277
+ }
278
+ };
279
+
280
+ // src/providers/index.ts
281
+ var providers = /* @__PURE__ */ new Map();
282
+ function registerProvider(name, provider) {
283
+ providers.set(name, provider);
284
+ }
285
+ function getProvider(name) {
286
+ const provider = providers.get(name || "claude-v3");
287
+ if (!provider) {
288
+ throw new Error(`Provider "${name}" n\xE3o encontrado. Dispon\xEDveis: ${listProviders().join(", ")}`);
289
+ }
290
+ return provider;
291
+ }
292
+ function listProviders() {
293
+ return Array.from(providers.keys());
294
+ }
295
+ registerProvider("claude-v3", new ClaudeV3Provider());
296
+
297
+ // src/config/manager.ts
298
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
299
+ import { join } from "path";
300
+ import { homedir } from "os";
301
+
302
+ // src/utils/validation.ts
303
+ function isValidApiKey(key) {
304
+ return typeof key === "string" && key.length > 0 && !key.includes(" ");
305
+ }
306
+ function isValidEndpoint(url) {
307
+ try {
308
+ const parsed = new URL(url);
309
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
310
+ } catch {
311
+ return false;
312
+ }
313
+ }
314
+ function isValidMaxTokens(n) {
315
+ return Number.isInteger(n) && n > 0 && n <= 2e5;
316
+ }
317
+
318
+ // src/config/manager.ts
319
+ var CONFIG_DIR = join(homedir(), ".tuticli");
320
+ var CONFIG_PATH = join(CONFIG_DIR, "config.json");
321
+ var DEFAULT_CONFIG = {
322
+ apiKey: "",
323
+ model: "claude-opus-4-7",
324
+ endpoint: "https://claudev3.hopto.org/v1/messages",
325
+ maxTokens: 4096
326
+ };
327
+ var VALID_KEYS = {
328
+ "api-key": {
329
+ validate: (v) => isValidApiKey(v),
330
+ normalize: (v) => v.trim()
331
+ },
332
+ "apiKey": {
333
+ validate: (v) => isValidApiKey(v),
334
+ normalize: (v) => v.trim()
335
+ },
336
+ model: {
337
+ validate: (v) => v.length > 0,
338
+ normalize: (v) => v.trim()
339
+ },
340
+ endpoint: {
341
+ validate: (v) => isValidEndpoint(v),
342
+ normalize: (v) => v.trim()
343
+ },
344
+ "max-tokens": {
345
+ validate: (v) => isValidMaxTokens(Number(v)),
346
+ normalize: (v) => Number(v)
347
+ },
348
+ "maxTokens": {
349
+ validate: (v) => isValidMaxTokens(Number(v)),
350
+ normalize: (v) => Number(v)
351
+ }
352
+ };
353
+ var ConfigManager = class {
354
+ config;
355
+ constructor() {
356
+ this.ensureDir();
357
+ this.config = this.load();
358
+ }
359
+ ensureDir() {
360
+ if (!existsSync(CONFIG_DIR)) {
361
+ mkdirSync(CONFIG_DIR, { recursive: true });
362
+ }
363
+ }
364
+ load() {
365
+ try {
366
+ if (existsSync(CONFIG_PATH)) {
367
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
368
+ const parsed = JSON.parse(raw);
369
+ return { ...DEFAULT_CONFIG, ...parsed };
370
+ }
371
+ } catch (err) {
372
+ logger.warn(`Falha ao ler config, usando padr\xE3o: ${err.message}`);
373
+ }
374
+ return { ...DEFAULT_CONFIG };
375
+ }
376
+ save() {
377
+ try {
378
+ writeFileSync(CONFIG_PATH, JSON.stringify(this.config, null, 2), "utf-8");
379
+ } catch (err) {
380
+ logger.error(`Falha ao salvar config: ${err.message}`);
381
+ }
382
+ }
383
+ get(key) {
384
+ return this.config[key];
385
+ }
386
+ getAll() {
387
+ return { ...this.config };
388
+ }
389
+ set(key, value) {
390
+ const entry = VALID_KEYS[key];
391
+ if (!entry) {
392
+ logger.error(`Chave inv\xE1lida: ${key}. Use: api-key, model, endpoint, max-tokens`);
393
+ return false;
394
+ }
395
+ if (!entry.validate(value)) {
396
+ logger.error(`Valor inv\xE1lido para "${key}": ${value}`);
397
+ return false;
398
+ }
399
+ const normalized = entry.normalize(value);
400
+ const configKey = this.mapToConfigKey(key);
401
+ this.config[configKey] = normalized;
402
+ this.save();
403
+ logger.success(`Config atualizada: ${key} = ${normalized}`);
404
+ return true;
405
+ }
406
+ mapToConfigKey(key) {
407
+ const map = {
408
+ "api-key": "apiKey",
409
+ "apiKey": "apiKey",
410
+ model: "model",
411
+ endpoint: "endpoint",
412
+ "max-tokens": "maxTokens",
413
+ "maxTokens": "maxTokens"
414
+ };
415
+ return map[key] || key;
416
+ }
417
+ show() {
418
+ const { apiKey, ...rest } = this.config;
419
+ const masked = apiKey ? apiKey.slice(0, 4) + "\u2026" + apiKey.slice(-4) : "(n\xE3o definida)";
420
+ console.log("\nConfigura\xE7\xE3o atual:");
421
+ console.log(` API Key: ${masked}`);
422
+ console.log(` Model: ${rest.model}`);
423
+ console.log(` Endpoint: ${rest.endpoint}`);
424
+ console.log(` Max Tokens: ${rest.maxTokens}`);
425
+ console.log(` Config file: ${CONFIG_PATH}
426
+ `);
427
+ }
428
+ reset() {
429
+ this.config = { ...DEFAULT_CONFIG };
430
+ this.save();
431
+ logger.success("Configura\xE7\xE3o resetada para valores padr\xE3o");
432
+ }
433
+ };
434
+
435
+ // src/storage/sqlite.ts
436
+ import { join as join2 } from "path";
437
+ import { homedir as homedir2 } from "os";
438
+ import { mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
439
+ import Database from "better-sqlite3";
440
+ var DB_DIR = join2(homedir2(), ".tuticli");
441
+ var DB_PATH = join2(DB_DIR, "conversations.db");
442
+ var Storage = class {
443
+ db;
444
+ constructor() {
445
+ if (!existsSync2(DB_DIR)) {
446
+ mkdirSync2(DB_DIR, { recursive: true });
447
+ }
448
+ this.db = new Database(DB_PATH);
449
+ this.db.pragma("journal_mode = WAL");
450
+ this.init();
451
+ }
452
+ init() {
453
+ this.db.exec(`
454
+ CREATE TABLE IF NOT EXISTS sessions (
455
+ id TEXT PRIMARY KEY,
456
+ name TEXT NOT NULL DEFAULT 'Nova conversa',
457
+ model TEXT NOT NULL DEFAULT 'claude-opus-4-7',
458
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
459
+ updated_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime'))
460
+ );
461
+
462
+ CREATE TABLE IF NOT EXISTS messages (
463
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
464
+ session_id TEXT NOT NULL,
465
+ role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system')),
466
+ content TEXT NOT NULL,
467
+ created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')),
468
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
469
+ );
470
+
471
+ CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
472
+ `);
473
+ }
474
+ createSession(name, model) {
475
+ const id = crypto.randomUUID();
476
+ const stmt = this.db.prepare(
477
+ "INSERT INTO sessions (id, name, model) VALUES (?, ?, ?)"
478
+ );
479
+ stmt.run(id, name || "Nova conversa", model || "claude-opus-4-7");
480
+ return this.getSession(id);
481
+ }
482
+ getSession(id) {
483
+ const row = this.db.prepare("SELECT * FROM sessions WHERE id = ?").get(id);
484
+ if (!row) return void 0;
485
+ return {
486
+ id: row.id,
487
+ name: row.name,
488
+ model: row.model,
489
+ createdAt: row.created_at,
490
+ updatedAt: row.updated_at
491
+ };
492
+ }
493
+ listSessions() {
494
+ const rows = this.db.prepare("SELECT * FROM sessions ORDER BY updated_at DESC").all();
495
+ return rows.map((row) => ({
496
+ id: row.id,
497
+ name: row.name,
498
+ model: row.model,
499
+ createdAt: row.created_at,
500
+ updatedAt: row.updated_at
501
+ }));
502
+ }
503
+ updateSessionName(id, name) {
504
+ this.db.prepare("UPDATE sessions SET name = ?, updated_at = datetime('now', 'localtime') WHERE id = ?").run(name, id);
505
+ }
506
+ deleteSession(id) {
507
+ this.db.prepare("DELETE FROM messages WHERE session_id = ?").run(id);
508
+ this.db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
509
+ }
510
+ clearAll() {
511
+ this.db.exec("DELETE FROM messages");
512
+ this.db.exec("DELETE FROM sessions");
513
+ }
514
+ addMessage(sessionId, role, content) {
515
+ const stmt = this.db.prepare(
516
+ "INSERT INTO messages (session_id, role, content) VALUES (?, ?, ?)"
517
+ );
518
+ const result = stmt.run(sessionId, role, content);
519
+ this.db.prepare("UPDATE sessions SET updated_at = datetime('now', 'localtime') WHERE id = ?").run(sessionId);
520
+ return {
521
+ id: result.lastInsertRowid,
522
+ sessionId,
523
+ role,
524
+ content
525
+ };
526
+ }
527
+ getMessages(sessionId) {
528
+ const rows = this.db.prepare("SELECT * FROM messages WHERE session_id = ? ORDER BY id ASC").all(sessionId);
529
+ return rows.map((row) => ({
530
+ id: row.id,
531
+ sessionId: row.session_id,
532
+ role: row.role,
533
+ content: row.content,
534
+ createdAt: row.created_at
535
+ }));
536
+ }
537
+ getMessageCount(sessionId) {
538
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM messages WHERE session_id = ?").get(sessionId);
539
+ return row.count;
540
+ }
541
+ close() {
542
+ this.db.close();
543
+ }
544
+ };
545
+
546
+ // src/tools/base.ts
547
+ var Tool = class {
548
+ needsConfirmation(args) {
549
+ return { needed: false };
550
+ }
551
+ };
552
+
553
+ // src/tools/read-file.ts
554
+ import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
555
+ import { resolve } from "path";
556
+ var ReadFileTool = class extends Tool {
557
+ definition = {
558
+ name: "read_file",
559
+ description: "Read the contents of a file. Use this to inspect source code, configs, or any text file in the project.",
560
+ input_schema: {
561
+ type: "object",
562
+ properties: {
563
+ path: {
564
+ type: "string",
565
+ description: "Relative or absolute path to the file"
566
+ },
567
+ limit: {
568
+ type: "number",
569
+ description: "Maximum number of lines to read (optional)"
570
+ },
571
+ offset: {
572
+ type: "number",
573
+ description: "Starting line number (1-indexed, optional)"
574
+ }
575
+ },
576
+ required: ["path"]
577
+ }
578
+ };
579
+ async execute(args) {
580
+ const filePath = resolve(String(args.path));
581
+ const limit = args.limit ? Number(args.limit) : void 0;
582
+ const offset = args.offset ? Number(args.offset) : void 0;
583
+ if (!existsSync3(filePath)) {
584
+ return { success: false, data: "", error: `Arquivo n\xE3o encontrado: ${filePath}` };
585
+ }
586
+ try {
587
+ const content = readFileSync2(filePath, "utf-8");
588
+ const lines = content.split("\n");
589
+ if (offset || limit) {
590
+ const start = offset ? Math.max(0, offset - 1) : 0;
591
+ const end = limit ? start + limit : void 0;
592
+ const sliced = lines.slice(start, end);
593
+ const total = lines.length;
594
+ const result = sliced.join("\n");
595
+ const meta = `
596
+ [Linhas ${start + 1}-${Math.min(end || total, total)} de ${total}]`;
597
+ return { success: true, data: result + meta };
598
+ }
599
+ return {
600
+ success: true,
601
+ data: content
602
+ };
603
+ } catch (err) {
604
+ return { success: false, data: "", error: `Erro ao ler arquivo: ${err.message}` };
605
+ }
606
+ }
607
+ };
608
+
609
+ // src/tools/write-file.ts
610
+ import { writeFileSync as writeFileSync2, existsSync as existsSync4 } from "fs";
611
+ import { resolve as resolve2, dirname } from "path";
612
+ import { mkdirSync as mkdirSync3 } from "fs";
613
+ var WriteFileTool = class extends Tool {
614
+ definition = {
615
+ name: "write_file",
616
+ description: "Write content to a file. Creates the file if it does not exist, or overwrites if it does. Use for creating new files or complete rewrites.",
617
+ input_schema: {
618
+ type: "object",
619
+ properties: {
620
+ path: {
621
+ type: "string",
622
+ description: "Relative or absolute path to the file"
623
+ },
624
+ content: {
625
+ type: "string",
626
+ description: "Full content to write to the file"
627
+ }
628
+ },
629
+ required: ["path", "content"]
630
+ }
631
+ };
632
+ needsConfirmation() {
633
+ return { needed: false };
634
+ }
635
+ async execute(args) {
636
+ const filePath = resolve2(String(args.path));
637
+ const content = String(args.content ?? "");
638
+ try {
639
+ const dir = dirname(filePath);
640
+ if (!existsSync4(dir)) {
641
+ mkdirSync3(dir, { recursive: true });
642
+ }
643
+ const isNew = !existsSync4(filePath);
644
+ writeFileSync2(filePath, content, "utf-8");
645
+ return {
646
+ success: true,
647
+ data: isNew ? `Arquivo criado: ${filePath} (${content.length} bytes)` : `Arquivo atualizado: ${filePath} (${content.length} bytes)`
648
+ };
649
+ } catch (err) {
650
+ return { success: false, data: "", error: `Erro ao escrever arquivo: ${err.message}` };
651
+ }
652
+ }
653
+ };
654
+
655
+ // src/tools/edit-file.ts
656
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
657
+ import { resolve as resolve3 } from "path";
658
+ var EditFileTool = class extends Tool {
659
+ definition = {
660
+ name: "edit_file",
661
+ description: "Make targeted edits to a file by finding and replacing specific text. Use for surgical changes instead of rewriting the entire file. The old_string must match exactly including whitespace.",
662
+ input_schema: {
663
+ type: "object",
664
+ properties: {
665
+ path: {
666
+ type: "string",
667
+ description: "Relative or absolute path to the file to edit"
668
+ },
669
+ old_string: {
670
+ type: "string",
671
+ description: "The exact text to find and replace (must match precisely)"
672
+ },
673
+ new_string: {
674
+ type: "string",
675
+ description: "The new text to replace with"
676
+ }
677
+ },
678
+ required: ["path", "old_string", "new_string"]
679
+ }
680
+ };
681
+ needsConfirmation() {
682
+ return { needed: false };
683
+ }
684
+ async execute(args) {
685
+ const filePath = resolve3(String(args.path));
686
+ const oldString = String(args.old_string ?? "");
687
+ const newString = String(args.new_string ?? "");
688
+ if (!existsSync5(filePath)) {
689
+ return { success: false, data: "", error: `Arquivo n\xE3o encontrado: ${filePath}` };
690
+ }
691
+ try {
692
+ const content = readFileSync4(filePath, "utf-8");
693
+ if (!content.includes(oldString)) {
694
+ return {
695
+ success: false,
696
+ data: "",
697
+ error: `Texto n\xE3o encontrado no arquivo. Verifique se h\xE1 diferen\xE7as de espa\xE7amento ou quebra de linha.`
698
+ };
699
+ }
700
+ const count = content.split(oldString).length - 1;
701
+ const newContent = content.replaceAll(oldString, newString);
702
+ writeFileSync3(filePath, newContent, "utf-8");
703
+ return {
704
+ success: true,
705
+ data: `Arquivo editado: ${filePath} (${count} ocorr\xEAncia${count > 1 ? "s" : ""} alterada${count > 1 ? "s" : ""})`
706
+ };
707
+ } catch (err) {
708
+ return { success: false, data: "", error: `Erro ao editar arquivo: ${err.message}` };
709
+ }
710
+ }
711
+ };
712
+
713
+ // src/tools/list-files.ts
714
+ import { readdirSync, statSync, existsSync as existsSync6 } from "fs";
715
+ import { resolve as resolve4, join as join3, relative } from "path";
716
+ var IGNORED = /* @__PURE__ */ new Set([
717
+ "node_modules",
718
+ ".git",
719
+ ".svn",
720
+ ".hg",
721
+ "dist",
722
+ "build",
723
+ ".next",
724
+ ".cache",
725
+ "__pycache__",
726
+ ".DS_Store",
727
+ "*.pyc",
728
+ ".gitignore"
729
+ ]);
730
+ function shouldIgnore(name) {
731
+ if (IGNORED.has(name)) return true;
732
+ if (name.startsWith(".") && name !== ".env" && name !== ".env.example") return true;
733
+ return false;
734
+ }
735
+ function scanDir(dirPath, maxDepth, depth = 0, relativePath = "") {
736
+ if (depth > maxDepth) return [];
737
+ const entries = [];
738
+ try {
739
+ const files = readdirSync(dirPath);
740
+ for (const file of files) {
741
+ if (shouldIgnore(file)) continue;
742
+ const fullPath = join3(dirPath, file);
743
+ try {
744
+ const stat = statSync(fullPath);
745
+ if (stat.isDirectory()) {
746
+ entries.push({ name: file + "/", type: "dir" });
747
+ if (depth < maxDepth) {
748
+ const sub = scanDir(fullPath, maxDepth, depth + 1, join3(relativePath, file));
749
+ entries.push(...sub);
750
+ }
751
+ } else if (stat.isFile()) {
752
+ entries.push({ name: file, type: "file", size: stat.size });
753
+ }
754
+ } catch {
755
+ }
756
+ }
757
+ } catch {
758
+ }
759
+ return entries;
760
+ }
761
+ function formatSize(bytes) {
762
+ if (bytes < 1024) return `${bytes}B`;
763
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
764
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
765
+ }
766
+ function formatTree(entries, indent = "") {
767
+ let result = "";
768
+ for (const entry of entries) {
769
+ if (entry.type === "dir") {
770
+ result += `${indent}\u{1F4C1} ${entry.name}
771
+ `;
772
+ } else {
773
+ result += `${indent}\u{1F4C4} ${entry.name}${entry.size !== void 0 ? ` (${formatSize(entry.size)})` : ""}
774
+ `;
775
+ }
776
+ }
777
+ return result;
778
+ }
779
+ var ListFilesTool = class extends Tool {
780
+ definition = {
781
+ name: "list_files",
782
+ description: "List files and directories in the project. Shows a tree view with file sizes. Use to understand project structure.",
783
+ input_schema: {
784
+ type: "object",
785
+ properties: {
786
+ path: {
787
+ type: "string",
788
+ description: 'Directory path relative to project root (default: ".")'
789
+ },
790
+ depth: {
791
+ type: "number",
792
+ description: "Maximum directory depth to traverse (default: 3, max: 10)"
793
+ }
794
+ },
795
+ required: []
796
+ }
797
+ };
798
+ async execute(args) {
799
+ const dirPath = resolve4(String(args.path || "."));
800
+ const depth = Math.min(Number(args.depth || 3), 10);
801
+ if (!existsSync6(dirPath)) {
802
+ return { success: false, data: "", error: `Diret\xF3rio n\xE3o encontrado: ${dirPath}` };
803
+ }
804
+ try {
805
+ const entries = scanDir(dirPath, depth);
806
+ const cwd = process.cwd();
807
+ const relPath = relative(cwd, dirPath) || ".";
808
+ let output2 = `\u{1F4C1} ${relPath}/
809
+ `;
810
+ output2 += formatTree(entries, "");
811
+ const fileCount = entries.filter((e) => e.type === "file").length;
812
+ const dirCount = entries.filter((e) => e.type === "dir").length;
813
+ output2 += `
814
+ ${dirCount} pastas, ${fileCount} arquivos`;
815
+ return { success: true, data: output2 };
816
+ } catch (err) {
817
+ return { success: false, data: "", error: `Erro ao listar diret\xF3rio: ${err.message}` };
818
+ }
819
+ }
820
+ };
821
+
822
+ // src/tools/glob-files.ts
823
+ import { resolve as resolve5 } from "path";
824
+ var IGNORE_PATTERNS = [
825
+ "node_modules/**",
826
+ ".git/**",
827
+ "dist/**",
828
+ "build/**",
829
+ ".next/**",
830
+ ".cache/**",
831
+ "__pycache__/**",
832
+ "*.pyc"
833
+ ];
834
+ var GlobFilesTool = class extends Tool {
835
+ definition = {
836
+ name: "glob",
837
+ description: "Find files matching a glob pattern. Use to search for files by name or extension pattern.",
838
+ input_schema: {
839
+ type: "object",
840
+ properties: {
841
+ pattern: {
842
+ type: "string",
843
+ description: 'Glob pattern to match (e.g., "src/**/*.ts", "*.json", "**/*.test.ts")'
844
+ },
845
+ path: {
846
+ type: "string",
847
+ description: 'Directory to search in (default: ".")'
848
+ }
849
+ },
850
+ required: ["pattern"]
851
+ }
852
+ };
853
+ async execute(args) {
854
+ const pattern = String(args.pattern);
855
+ const searchPath = String(args.path || ".");
856
+ try {
857
+ const { globSync } = await import("glob");
858
+ const results = globSync(pattern, {
859
+ cwd: resolve5(searchPath),
860
+ ignore: IGNORE_PATTERNS,
861
+ dot: false,
862
+ nodir: true
863
+ });
864
+ if (results.length === 0) {
865
+ return { success: true, data: "Nenhum arquivo encontrado para o padr\xE3o: " + pattern };
866
+ }
867
+ const sorted = results.sort();
868
+ const list = sorted.map((f) => ` ${f}`).join("\n");
869
+ return {
870
+ success: true,
871
+ data: `Arquivos encontrados (${results.length}):
872
+ ${list}`
873
+ };
874
+ } catch (err) {
875
+ return { success: false, data: "", error: `Erro na busca glob: ${err.message}` };
876
+ }
877
+ }
878
+ };
879
+
880
+ // src/tools/grep-files.ts
881
+ import { readFileSync as readFileSync5, statSync as statSync2 } from "fs";
882
+ import { resolve as resolve6 } from "path";
883
+ var MAX_FILE_SIZE = 1024 * 1024;
884
+ function isText(buf) {
885
+ for (let i = 0; i < Math.min(buf.length, 4096); i++) {
886
+ const c = buf[i];
887
+ if (c === 0) return false;
888
+ }
889
+ return true;
890
+ }
891
+ var GrepFilesTool = class extends Tool {
892
+ definition = {
893
+ name: "grep",
894
+ description: "Search for a regex pattern across files in the project. Use to find where functions are defined, variables are used, or any text pattern.",
895
+ input_schema: {
896
+ type: "object",
897
+ properties: {
898
+ pattern: {
899
+ type: "string",
900
+ description: "Regex pattern to search for"
901
+ },
902
+ path: {
903
+ type: "string",
904
+ description: 'Directory to search in (default: ".")'
905
+ },
906
+ include: {
907
+ type: "string",
908
+ description: 'Only search files matching this glob pattern (e.g., "*.ts", "*.{ts,js}")'
909
+ },
910
+ maxResults: {
911
+ type: "number",
912
+ description: "Maximum number of results to return (default: 50)"
913
+ }
914
+ },
915
+ required: ["pattern"]
916
+ }
917
+ };
918
+ async execute(args) {
919
+ const rawPattern = String(args.pattern);
920
+ const searchPath = resolve6(String(args.path || "."));
921
+ const maxResults = Math.min(Number(args.maxResults || 50), 200);
922
+ const includePattern = args.include ? String(args.include) : void 0;
923
+ let regex;
924
+ try {
925
+ regex = new RegExp(rawPattern, "gi");
926
+ } catch {
927
+ return { success: false, data: "", error: `Padr\xE3o regex inv\xE1lido: ${rawPattern}` };
928
+ }
929
+ try {
930
+ const { globSync } = await import("glob");
931
+ const searchGlob = includePattern || "**/*";
932
+ const files = globSync(searchGlob, {
933
+ cwd: searchPath,
934
+ ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
935
+ nodir: true,
936
+ dot: false
937
+ });
938
+ const results = [];
939
+ for (const file of files.slice(0, 500)) {
940
+ if (results.length >= maxResults) break;
941
+ const fullPath = resolve6(searchPath, file);
942
+ try {
943
+ const stat = statSync2(fullPath);
944
+ if (stat.size > MAX_FILE_SIZE) continue;
945
+ const buf = readFileSync5(fullPath);
946
+ if (!isText(buf)) continue;
947
+ const content = buf.toString("utf-8");
948
+ const lines = content.split("\n");
949
+ for (let i = 0; i < lines.length; i++) {
950
+ if (results.length >= maxResults) break;
951
+ if (regex.test(lines[i])) {
952
+ const lineNum = i + 1;
953
+ const trimmed = lines[i].trim();
954
+ results.push(`${file}:${lineNum}: ${trimmed}`);
955
+ }
956
+ }
957
+ } catch {
958
+ }
959
+ }
960
+ if (results.length === 0) {
961
+ return { success: true, data: `Nenhum resultado para: ${rawPattern}` };
962
+ }
963
+ return {
964
+ success: true,
965
+ data: `Resultados para "${rawPattern}" (${results.length}):
966
+ ${results.join("\n")}`
967
+ };
968
+ } catch (err) {
969
+ return { success: false, data: "", error: `Erro na busca: ${err.message}` };
970
+ }
971
+ }
972
+ };
973
+
974
+ // src/tools/project-info.ts
975
+ import { readFileSync as readFileSync6, readdirSync as readdirSync2, statSync as statSync3, existsSync as existsSync7 } from "fs";
976
+ import { join as join4 } from "path";
977
+ var IGNORE_DIRS = /* @__PURE__ */ new Set([
978
+ "node_modules",
979
+ ".git",
980
+ "dist",
981
+ "build",
982
+ ".next",
983
+ ".cache",
984
+ "__pycache__",
985
+ ".svn",
986
+ ".hg",
987
+ "coverage"
988
+ ]);
989
+ function scanFiles(dir, depth = 0, maxDepth = 4) {
990
+ if (depth > maxDepth) return [];
991
+ const files = [];
992
+ try {
993
+ const entries = readdirSync2(dir);
994
+ for (const entry of entries) {
995
+ if (IGNORE_DIRS.has(entry)) continue;
996
+ if (entry.startsWith(".") && entry !== ".env" && entry !== ".env.example") continue;
997
+ const fullPath = join4(dir, entry);
998
+ try {
999
+ const stat = statSync3(fullPath);
1000
+ if (stat.isDirectory()) {
1001
+ files.push(...scanFiles(fullPath, depth + 1, maxDepth));
1002
+ } else if (stat.isFile()) {
1003
+ const ext = entry.includes(".") ? entry.split(".").pop() : "";
1004
+ files.push({ path: fullPath, size: stat.size, ext });
1005
+ }
1006
+ } catch {
1007
+ }
1008
+ }
1009
+ } catch {
1010
+ }
1011
+ return files;
1012
+ }
1013
+ function readConfigIfExists(filePath) {
1014
+ try {
1015
+ if (existsSync7(filePath)) {
1016
+ return JSON.parse(readFileSync6(filePath, "utf-8"));
1017
+ }
1018
+ } catch {
1019
+ }
1020
+ return null;
1021
+ }
1022
+ var ProjectInfoTool = class extends Tool {
1023
+ definition = {
1024
+ name: "project_info",
1025
+ description: "Get an overview of the project structure, including file counts by type, project config files, and dependency information. Use this first to understand what kind of project this is.",
1026
+ input_schema: {
1027
+ type: "object",
1028
+ properties: {},
1029
+ required: []
1030
+ }
1031
+ };
1032
+ async execute(_args) {
1033
+ const cwd = process.cwd();
1034
+ try {
1035
+ const files = scanFiles(cwd);
1036
+ const totalSize = files.reduce((acc, f) => acc + f.size, 0);
1037
+ const byExt = {};
1038
+ for (const f of files) {
1039
+ byExt[f.ext] = (byExt[f.ext] || 0) + 1;
1040
+ }
1041
+ const extSummary = Object.entries(byExt).sort((a, b2) => b2[1] - a[1]).map(([ext, count]) => ` .${ext}: ${count} arquivos`).join("\n");
1042
+ const topFiles = files.sort((a, b2) => b2.size - a.size).slice(0, 15).map((f) => {
1043
+ const rel = f.path.replace(cwd + "/", "");
1044
+ const sizeLabel = f.size > 1024 * 1024 ? `${(f.size / (1024 * 1024)).toFixed(1)}MB` : f.size > 1024 ? `${(f.size / 1024).toFixed(1)}KB` : `${f.size}B`;
1045
+ return ` ${rel} (${sizeLabel})`;
1046
+ }).join("\n");
1047
+ let configInfo = "";
1048
+ const pkg = readConfigIfExists(join4(cwd, "package.json"));
1049
+ if (pkg) {
1050
+ const name = pkg.name || "(sem nome)";
1051
+ const deps = Object.keys(pkg.dependencies || {}).length;
1052
+ const devDeps = Object.keys(pkg.devDependencies || {}).length;
1053
+ const scripts = Object.keys(pkg.scripts || {}).length;
1054
+ configInfo += ` Node.js: ${name}
1055
+ Depend\xEAncias: ${deps} | Dev: ${devDeps} | Scripts: ${scripts}
1056
+ `;
1057
+ }
1058
+ const tsconfig = readConfigIfExists(join4(cwd, "tsconfig.json"));
1059
+ if (tsconfig) {
1060
+ configInfo += ` TypeScript: ${tsconfig.compilerOptions?.target || "presente"}
1061
+ `;
1062
+ }
1063
+ try {
1064
+ const gitHead = readFileSync6(join4(cwd, ".git", "HEAD"), "utf-8").trim();
1065
+ configInfo += ` Git: ${gitHead.replace("ref: refs/heads/", "branch ")}
1066
+ `;
1067
+ } catch {
1068
+ }
1069
+ const output2 = [
1070
+ `\u{1F4C1} Projeto: ${cwd}`,
1071
+ ``,
1072
+ `\u{1F4CA} Vis\xE3o geral:`,
1073
+ ` Total: ${files.length} arquivos (${(totalSize / 1024 / 1024).toFixed(2)}MB)`,
1074
+ ``,
1075
+ `\u{1F4E6} Configura\xE7\xE3o:`,
1076
+ configInfo || " (sem configura\xE7\xE3o detectada)",
1077
+ ``,
1078
+ `\u{1F4C4} Tipos de arquivo:`,
1079
+ extSummary || " (sem arquivos)",
1080
+ ``,
1081
+ `\u{1F4CF} Maiores arquivos:`,
1082
+ topFiles || " (sem arquivos)"
1083
+ ].join("\n");
1084
+ return { success: true, data: output2 };
1085
+ } catch (err) {
1086
+ return { success: false, data: "", error: `Erro ao analisar projeto: ${err.message}` };
1087
+ }
1088
+ }
1089
+ };
1090
+
1091
+ // src/tools/run-command.ts
1092
+ import { execSync } from "child_process";
1093
+ import { resolve as resolve8 } from "path";
1094
+ var BLOCKED_COMMANDS = [
1095
+ "rm -rf /",
1096
+ "rm -rf ~",
1097
+ "rm -rf .",
1098
+ "mkfs",
1099
+ "dd if=",
1100
+ "> /dev/sd",
1101
+ ":(){ :|:& };:",
1102
+ "chmod 777",
1103
+ "sudo ",
1104
+ "su ",
1105
+ "passwd"
1106
+ ];
1107
+ function isBlocked(command) {
1108
+ const lower = command.toLowerCase();
1109
+ return BLOCKED_COMMANDS.some((b2) => lower.includes(b2));
1110
+ }
1111
+ var MAX_OUTPUT = 1e4;
1112
+ var RunCommandTool = class extends Tool {
1113
+ definition = {
1114
+ name: "run_command",
1115
+ description: "Execute a shell command in the project directory. Use for running build commands, tests, linters, or any terminal operation. THE USER WILL BE ASKED TO CONFIRM BEFORE EXECUTION.",
1116
+ input_schema: {
1117
+ type: "object",
1118
+ properties: {
1119
+ command: {
1120
+ type: "string",
1121
+ description: "The shell command to execute"
1122
+ },
1123
+ workdir: {
1124
+ type: "string",
1125
+ description: "Working directory (default: current project root)"
1126
+ },
1127
+ description: {
1128
+ type: "string",
1129
+ description: "Explain what this command does and why it is needed"
1130
+ }
1131
+ },
1132
+ required: ["command", "description"]
1133
+ }
1134
+ };
1135
+ needsConfirmation(args) {
1136
+ const cmd = String(args.command || "");
1137
+ const desc = String(args.description || "");
1138
+ return {
1139
+ needed: true,
1140
+ message: `Executar comando?
1141
+ Descri\xE7\xE3o: ${desc}
1142
+ Comando: ${cmd}`
1143
+ };
1144
+ }
1145
+ async execute(args) {
1146
+ const command = String(args.command || "");
1147
+ const workdir = args.workdir ? resolve8(String(args.workdir)) : process.cwd();
1148
+ const description = String(args.description || "");
1149
+ if (!command) {
1150
+ return { success: false, data: "", error: "Nenhum comando fornecido" };
1151
+ }
1152
+ if (isBlocked(command)) {
1153
+ return { success: false, data: "", error: "Comando bloqueado por raz\xF5es de seguran\xE7a" };
1154
+ }
1155
+ try {
1156
+ const output2 = execSync(command, {
1157
+ cwd: workdir,
1158
+ encoding: "utf-8",
1159
+ timeout: 12e4,
1160
+ maxBuffer: MAX_OUTPUT,
1161
+ env: { ...process.env }
1162
+ });
1163
+ const truncated = output2.length > MAX_OUTPUT ? output2.slice(0, MAX_OUTPUT) + `
1164
+ ... (sa\xEDda truncada, ${output2.length - MAX_OUTPUT} bytes omitidos)` : output2;
1165
+ return {
1166
+ success: true,
1167
+ data: truncated || "(comando executado sem sa\xEDda)"
1168
+ };
1169
+ } catch (err) {
1170
+ const stderr = err.stderr || "";
1171
+ const stdout = err.stdout || "";
1172
+ const message = err.message;
1173
+ return {
1174
+ success: false,
1175
+ data: "",
1176
+ error: `Comando falhou: ${message}
1177
+ ${stderr || stdout || ""}`.trim()
1178
+ };
1179
+ }
1180
+ }
1181
+ };
1182
+
1183
+ // src/tools/index.ts
1184
+ var toolRegistry = /* @__PURE__ */ new Map();
1185
+ function register(tool) {
1186
+ toolRegistry.set(tool.definition.name, tool);
1187
+ }
1188
+ function getAllTools() {
1189
+ return Array.from(toolRegistry.values());
1190
+ }
1191
+ function getTool(name) {
1192
+ return toolRegistry.get(name);
1193
+ }
1194
+ register(new ReadFileTool());
1195
+ register(new WriteFileTool());
1196
+ register(new EditFileTool());
1197
+ register(new ListFilesTool());
1198
+ register(new GlobFilesTool());
1199
+ register(new GrepFilesTool());
1200
+ register(new ProjectInfoTool());
1201
+ register(new RunCommandTool());
1202
+
1203
+ // src/ui/components.ts
1204
+ import chalk2 from "chalk";
1205
+ import boxen from "boxen";
1206
+ var w = chalk2.bold;
1207
+ var d2 = chalk2.dim;
1208
+ var gold = chalk2.hex("#E8BF6A");
1209
+ function renderWelcome() {
1210
+ const art = chalk2.bold(`
1211
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
1212
+ \u2551 \u2551
1213
+ \u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2551
1214
+ \u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2551
1215
+ \u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2551
1216
+ \u2551 \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2551
1217
+ \u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2551
1218
+ \u2551 \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u2551
1219
+ \u2551 \u2551
1220
+ \u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557\u2551
1221
+ \u2551 \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2551
1222
+ \u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2551
1223
+ \u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2551
1224
+ \u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2551
1225
+ \u2551 \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u2551
1226
+ \u2551 \u2551
1227
+ \u2551 CLI interativo para Claude AI \u2551
1228
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
1229
+ `);
1230
+ logger.raw(art);
1231
+ logger.raw(d2(" Comandos: /help, /clear, /sessions, /exit, /new\n"));
1232
+ }
1233
+ function renderDirConfirm(dir) {
1234
+ console.log(
1235
+ boxen(
1236
+ ` ${d2("\u{1F4C1} Diret\xF3rio atual")}
1237
+ ${w(dir)}
1238
+
1239
+ ${d2("O tuticli pode acessar arquivos desta pasta.")}
1240
+ ${d2("Deseja continuar?")}`,
1241
+ {
1242
+ padding: 1,
1243
+ margin: { top: 1, bottom: 0 },
1244
+ borderStyle: "round",
1245
+ borderColor: "white",
1246
+ dimBorder: true
1247
+ }
1248
+ )
1249
+ );
1250
+ }
1251
+ function renderHelp() {
1252
+ console.log(boxen(
1253
+ `
1254
+ ${w("Comandos dispon\xEDveis")}
1255
+
1256
+ ${w("/help")} Mostra esta mensagem
1257
+ ${w("/clear")} Limpa o hist\xF3rico da sess\xE3o atual
1258
+ ${w("/sessions")} Lista todas as sess\xF5es
1259
+ ${w("/new")} Inicia uma nova sess\xE3o
1260
+ ${w("/exit")} Sai do chat
1261
+
1262
+ ${d2("Dica:")} Pressione ${w("Ctrl+C")} para sair a qualquer momento.
1263
+ `,
1264
+ {
1265
+ padding: 1,
1266
+ margin: 0,
1267
+ borderStyle: "round",
1268
+ borderColor: "white",
1269
+ dimBorder: true
1270
+ }
1271
+ ));
1272
+ }
1273
+ function renderFormattedContent(content) {
1274
+ const blocks = content.split(/(```[\w-]*\n[\s\S]*?```)/);
1275
+ return blocks.map((block) => {
1276
+ if (!block.startsWith("```")) {
1277
+ return block;
1278
+ }
1279
+ const firstLineEnd = block.indexOf("\n");
1280
+ const langLine = block.slice(3, firstLineEnd).trim();
1281
+ const code = block.slice(firstLineEnd + 1, -3);
1282
+ const lines = code.split("\n");
1283
+ const lineNumWidth = String(lines.length).length;
1284
+ const langTag = langLine ? chalk2.bold(` ${langLine} `) : d2(" code ");
1285
+ const codeLines = lines.map((line, i) => {
1286
+ const num = d2(String(i + 1).padStart(lineNumWidth, " ") + " \u2502");
1287
+ return `${num} ${gold(line)}`;
1288
+ }).join("\n");
1289
+ return "\n" + boxen(codeLines, {
1290
+ padding: { top: 0, bottom: 0, left: 0, right: 0 },
1291
+ margin: { top: 0, bottom: 0 },
1292
+ borderStyle: "round",
1293
+ borderColor: "white",
1294
+ dimBorder: true,
1295
+ title: langTag,
1296
+ titleAlignment: "left"
1297
+ }) + "\n";
1298
+ }).join("");
1299
+ }
1300
+ function renderMessage(msg) {
1301
+ const prefix = msg.role === "user" ? w("Voc\xEA") : w("Claude");
1302
+ if (msg.role === "assistant" && msg.content.includes("```")) {
1303
+ const formatted = renderFormattedContent(msg.content);
1304
+ console.log(
1305
+ boxen(formatted, {
1306
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
1307
+ margin: { top: 1, bottom: 0 },
1308
+ borderStyle: "round",
1309
+ borderColor: "white",
1310
+ dimBorder: true,
1311
+ title: prefix,
1312
+ titleAlignment: "left"
1313
+ })
1314
+ );
1315
+ } else {
1316
+ console.log(
1317
+ boxen(msg.content, {
1318
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
1319
+ margin: { top: 1, bottom: 0 },
1320
+ borderStyle: "round",
1321
+ borderColor: "white",
1322
+ title: prefix,
1323
+ titleAlignment: "left"
1324
+ })
1325
+ );
1326
+ }
1327
+ }
1328
+ function renderStreamStart() {
1329
+ process.stdout.write(w("\n Claude ") + d2("(streaming...) \n"));
1330
+ }
1331
+ var TYPING_SPEED = 20;
1332
+ function delay(ms) {
1333
+ return new Promise((resolve9) => setTimeout(resolve9, ms));
1334
+ }
1335
+ async function renderStreamChunk(text) {
1336
+ for (const char of text) {
1337
+ process.stdout.write(char);
1338
+ if (char !== " " && char !== "\n") {
1339
+ await delay(TYPING_SPEED);
1340
+ } else if (char === "\n") {
1341
+ await delay(TYPING_SPEED);
1342
+ }
1343
+ }
1344
+ }
1345
+ function renderStreamEnd() {
1346
+ process.stdout.write("\n\n");
1347
+ }
1348
+ function renderError(msg) {
1349
+ console.log(
1350
+ boxen(msg, {
1351
+ padding: 1,
1352
+ borderStyle: "round",
1353
+ borderColor: "white",
1354
+ title: w(" Erro "),
1355
+ titleAlignment: "left"
1356
+ })
1357
+ );
1358
+ }
1359
+ function renderSessionsTable(sessions) {
1360
+ if (sessions.length === 0) {
1361
+ logger.info("Nenhuma sess\xE3o encontrada.");
1362
+ return;
1363
+ }
1364
+ console.log(w("\n Sess\xF5es:\n"));
1365
+ sessions.forEach((s, i) => {
1366
+ const shortId = s.id.slice(0, 8);
1367
+ const name = s.name.length > 30 ? s.name.slice(0, 30) + "\u2026" : s.name;
1368
+ console.log(
1369
+ ` ${d2(String(i + 1).padStart(2, " ") + ".")} ${w(name)} ${d2(shortId)} ${d2(s.model)} ${d2(s.updatedAt)}`
1370
+ );
1371
+ });
1372
+ console.log();
1373
+ }
1374
+
1375
+ // src/ui/chat.ts
1376
+ var config = new ConfigManager();
1377
+ var storage = new Storage();
1378
+ var STATUS = [
1379
+ "Estou processando sua solicita\xE7\xE3o\u2026",
1380
+ "Analisando sua pergunta\u2026",
1381
+ "Consultando o modelo de IA\u2026",
1382
+ "Preparando resposta\u2026",
1383
+ "Processando\u2026",
1384
+ "Aguarde um momento\u2026",
1385
+ "Estou pensando\u2026",
1386
+ "Buscando informa\xE7\xF5es\u2026",
1387
+ "Analisando o contexto\u2026",
1388
+ "Compilando conhecimento\u2026",
1389
+ "Estruturando resposta\u2026",
1390
+ "Consultando base de conhecimento\u2026",
1391
+ "Processando dados\u2026",
1392
+ "Gerando resposta\u2026",
1393
+ "Aplicando racioc\xEDnio\u2026",
1394
+ "Verificando informa\xE7\xF5es\u2026",
1395
+ "Organizando pensamento\u2026",
1396
+ "Analisando c\xF3digo\u2026",
1397
+ "Buscando padr\xF5es\u2026",
1398
+ "Interpretando requisi\xE7\xE3o\u2026",
1399
+ "Conectando ao modelo\u2026",
1400
+ "Carregando contexto\u2026",
1401
+ "Sintetizando informa\xE7\xF5es\u2026",
1402
+ "Avaliando melhor abordagem\u2026",
1403
+ "Calculando resposta\u2026",
1404
+ "Construindo solu\xE7\xE3o\u2026",
1405
+ "Revisando\u2026",
1406
+ "Aplicando l\xF3gica\u2026",
1407
+ "Examinando detalhes\u2026",
1408
+ "Preparando an\xE1lise\u2026",
1409
+ "Coletando informa\xE7\xF5es\u2026",
1410
+ "Analisando arquivos\u2026",
1411
+ "Quase l\xE1\u2026",
1412
+ "Finalizando\u2026",
1413
+ "Validando resposta\u2026",
1414
+ "Processando com carinho\u2026",
1415
+ "Consultando intelig\xEAncia\u2026",
1416
+ "Montando resultado\u2026",
1417
+ "Reunindo dados\u2026",
1418
+ "Elaborando resposta\u2026",
1419
+ "Pensando profundamente\u2026",
1420
+ "Analisando requisitos\u2026",
1421
+ "Buscando solu\xE7\xE3o ideal\u2026"
1422
+ ];
1423
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1424
+ function createSpinner() {
1425
+ let interval = null;
1426
+ let current = "";
1427
+ let idx = 0;
1428
+ function clearLine() {
1429
+ if (current) {
1430
+ process.stderr.write(`\r${" ".repeat(current.length + 2)}\r`);
1431
+ }
1432
+ }
1433
+ return {
1434
+ start(msg) {
1435
+ current = msg;
1436
+ idx = 0;
1437
+ clearLine();
1438
+ if (!interval) {
1439
+ interval = setInterval(() => {
1440
+ process.stderr.write(`\r${FRAMES[idx]} ${current}`);
1441
+ idx = (idx + 1) % FRAMES.length;
1442
+ }, 80);
1443
+ }
1444
+ },
1445
+ stop() {
1446
+ if (interval) {
1447
+ clearInterval(interval);
1448
+ interval = null;
1449
+ }
1450
+ clearLine();
1451
+ current = "";
1452
+ },
1453
+ message(msg) {
1454
+ current = msg;
1455
+ }
1456
+ };
1457
+ }
1458
+ function askConfirm(msg) {
1459
+ return new Promise((resolve9) => {
1460
+ const rl = createInterface({ input, output });
1461
+ rl.question(chalk3.bold(`
1462
+ ${msg} (s/N) \u203A `), (answer) => {
1463
+ rl.close();
1464
+ try {
1465
+ input.setRawMode(true);
1466
+ } catch {
1467
+ }
1468
+ resolve9(answer.trim().toLowerCase() === "s");
1469
+ });
1470
+ });
1471
+ }
1472
+ function spinnerWithRotation(s, ms = 1800) {
1473
+ let i = 0;
1474
+ s.start(STATUS[i]);
1475
+ const id = setInterval(() => {
1476
+ i = (i + 1) % STATUS.length;
1477
+ s.message(STATUS[i]);
1478
+ }, ms);
1479
+ return () => {
1480
+ clearInterval(id);
1481
+ s.stop();
1482
+ };
1483
+ }
1484
+ function toolMessage(name, input2) {
1485
+ const path = String(input2.path || input2.pattern || input2.command || "");
1486
+ const shortPath = path.length > 50 ? path.slice(0, 50) + "\u2026" : path;
1487
+ const icons = {
1488
+ read_file: "\u{1F4D6} Lendo",
1489
+ write_file: "\u270F\uFE0F Escrevendo",
1490
+ edit_file: "\u{1F527} Editando",
1491
+ list_files: "\u{1F4C2} Listando",
1492
+ glob: "\u{1F4CE} Procurando",
1493
+ grep: "\u{1F50D} Buscando",
1494
+ project_info: "\u{1F4C1} Analisando",
1495
+ run_command: "\u26A1 Executando"
1496
+ };
1497
+ const icon = icons[name] || "\u{1F6E0}\uFE0F Executando";
1498
+ const suffix = path ? `: ${shortPath}` : "\u2026";
1499
+ return `${icon}${suffix}`;
1500
+ }
1501
+ async function processCommand(cmd, session) {
1502
+ const normalized = cmd.trim().toLowerCase();
1503
+ switch (normalized) {
1504
+ case "/help":
1505
+ renderHelp();
1506
+ return "continue";
1507
+ case "/clear": {
1508
+ const shouldClear = await askConfirm("Limpar todas as mensagens desta sess\xE3o?");
1509
+ if (shouldClear) {
1510
+ storage.deleteSession(session.id);
1511
+ logger.success("Sess\xE3o limpa.");
1512
+ return "new";
1513
+ }
1514
+ return "continue";
1515
+ }
1516
+ case "/sessions": {
1517
+ const sessions = storage.listSessions();
1518
+ renderSessionsTable(sessions);
1519
+ return "continue";
1520
+ }
1521
+ case "/new": {
1522
+ const shouldNew = await askConfirm("Iniciar nova sess\xE3o? (a atual ser\xE1 preservada)");
1523
+ if (shouldNew) return "new";
1524
+ return "continue";
1525
+ }
1526
+ case "/exit":
1527
+ case "/quit":
1528
+ return "exit";
1529
+ default:
1530
+ logger.warn(`Comando desconhecido: ${cmd}. Use /help para ver os comandos dispon\xEDveis.`);
1531
+ return "continue";
1532
+ }
1533
+ }
1534
+ async function startChat(sessionId) {
1535
+ let session = null;
1536
+ if (!config.get("apiKey")) {
1537
+ renderError("API key n\xE3o configurada. Use: tuticli config set api-key SUA_API_KEY");
1538
+ return;
1539
+ }
1540
+ if (sessionId) {
1541
+ const found = storage.getSession(sessionId);
1542
+ if (found) {
1543
+ session = found;
1544
+ } else {
1545
+ logger.warn(`Sess\xE3o "${sessionId}" n\xE3o encontrada. Criando nova.`);
1546
+ }
1547
+ }
1548
+ renderWelcome();
1549
+ if (!sessionId) {
1550
+ const cwd = process.cwd();
1551
+ renderDirConfirm(cwd);
1552
+ const useDir = await askConfirm("Usar este diret\xF3rio?");
1553
+ if (!useDir) {
1554
+ logger.warn("Diret\xF3rio n\xE3o confirmado. Apenas chat b\xE1sico dispon\xEDvel.");
1555
+ }
1556
+ }
1557
+ if (!session) {
1558
+ session = storage.createSession(void 0, config.get("model"));
1559
+ logger.success(`Nova sess\xE3o criada: ${session.id.slice(0, 8)}`);
1560
+ }
1561
+ let running = true;
1562
+ while (running) {
1563
+ const result = await runChatLoop(session);
1564
+ if (result === "exit") {
1565
+ running = false;
1566
+ } else if (result === "new") {
1567
+ session = storage.createSession(void 0, config.get("model"));
1568
+ logger.success(`Nova sess\xE3o criada: ${session.id.slice(0, 8)}`);
1569
+ }
1570
+ }
1571
+ storage.close();
1572
+ }
1573
+ async function runChatLoop(session) {
1574
+ const rl = createInterface({ input, output });
1575
+ let resolvePromise;
1576
+ const promise = new Promise((r) => {
1577
+ resolvePromise = r;
1578
+ });
1579
+ let busy = false;
1580
+ rl.setPrompt(chalk3.bold("\n Voc\xEA \u203A "));
1581
+ rl.on("line", async (line) => {
1582
+ if (busy) return;
1583
+ busy = true;
1584
+ try {
1585
+ const trimmed = line.trim();
1586
+ if (!trimmed) {
1587
+ busy = false;
1588
+ rl.prompt();
1589
+ return;
1590
+ }
1591
+ if (trimmed.startsWith("/")) {
1592
+ const result = await processCommand(trimmed, session);
1593
+ if (result === "exit" || result === "new") {
1594
+ rl.close();
1595
+ resolvePromise(result);
1596
+ return;
1597
+ }
1598
+ busy = false;
1599
+ rl.prompt();
1600
+ return;
1601
+ }
1602
+ storage.addMessage(session.id, "user", trimmed);
1603
+ renderMessage({ sessionId: session.id, role: "user", content: trimmed });
1604
+ if (session.name === "Nova conversa") {
1605
+ const newName = trimmed.length > 50 ? trimmed.slice(0, 50) + "\u2026" : trimmed;
1606
+ storage.updateSessionName(session.id, newName);
1607
+ session.name = newName;
1608
+ }
1609
+ const finalText = await handleToolLoop(session.id);
1610
+ if (finalText) {
1611
+ renderMessage({ sessionId: session.id, role: "assistant", content: finalText });
1612
+ storage.addMessage(session.id, "assistant", finalText);
1613
+ }
1614
+ busy = false;
1615
+ rl.prompt();
1616
+ } catch (err) {
1617
+ logger.error(`Erro: ${err}`);
1618
+ busy = false;
1619
+ rl.prompt();
1620
+ }
1621
+ });
1622
+ rl.on("SIGINT", () => {
1623
+ rl.close();
1624
+ resolvePromise("exit");
1625
+ });
1626
+ rl.on("close", () => {
1627
+ resolvePromise("exit");
1628
+ });
1629
+ rl.prompt();
1630
+ return promise;
1631
+ }
1632
+ async function handleToolLoop(sessionId) {
1633
+ const toolMessages = [];
1634
+ const provider = getProvider();
1635
+ const allTools = getAllTools().map((t) => t.definition);
1636
+ const s = createSpinner();
1637
+ for (let round = 0; round < 15; round++) {
1638
+ const dbHistory = storage.getMessages(sessionId);
1639
+ const dbMessages = dbHistory.map((m) => ({
1640
+ role: m.role,
1641
+ content: [{ type: "text", text: m.content }]
1642
+ }));
1643
+ const apiMessages = [...dbMessages, ...toolMessages];
1644
+ if (round === 0) {
1645
+ const stopRotate = spinnerWithRotation(s);
1646
+ const stream = provider.chatStream(apiMessages, config.getAll(), allTools);
1647
+ let textBuffer = "";
1648
+ const streamToolCalls = [];
1649
+ let gotData = false;
1650
+ for await (const chunk of stream) {
1651
+ if (!gotData) {
1652
+ gotData = true;
1653
+ stopRotate();
1654
+ }
1655
+ if (chunk.type === "content" && chunk.content) {
1656
+ textBuffer += chunk.content;
1657
+ } else if (chunk.type === "tool_use" && chunk.tool_call) {
1658
+ streamToolCalls.push({
1659
+ id: chunk.tool_call.id,
1660
+ name: chunk.tool_call.name,
1661
+ input: chunk.tool_call.input
1662
+ });
1663
+ } else if (chunk.type === "error") {
1664
+ if (!gotData) stopRotate();
1665
+ else s.stop();
1666
+ renderError(chunk.error || "Erro desconhecido");
1667
+ return textBuffer || chunk.error || "";
1668
+ }
1669
+ }
1670
+ if (!gotData) stopRotate();
1671
+ else s.stop();
1672
+ if (streamToolCalls.length === 0) {
1673
+ renderStreamStart();
1674
+ await renderStreamChunk(textBuffer);
1675
+ renderStreamEnd();
1676
+ return textBuffer;
1677
+ }
1678
+ if (textBuffer) {
1679
+ toolMessages.push({
1680
+ role: "assistant",
1681
+ content: [{ type: "text", text: textBuffer }]
1682
+ });
1683
+ }
1684
+ for (const tc of streamToolCalls) {
1685
+ let inputObj;
1686
+ try {
1687
+ inputObj = JSON.parse(tc.input);
1688
+ } catch {
1689
+ inputObj = {};
1690
+ }
1691
+ toolMessages.push({
1692
+ role: "assistant",
1693
+ content: [{
1694
+ type: "tool_use",
1695
+ id: tc.id,
1696
+ name: tc.name,
1697
+ input: inputObj
1698
+ }]
1699
+ });
1700
+ s.start(toolMessage(tc.name, inputObj));
1701
+ const result = await runTool(tc.name, inputObj, s);
1702
+ s.stop();
1703
+ if (result.success) {
1704
+ const display = result.data.length > 300 ? result.data.slice(0, 300) + `
1705
+ ... (${result.data.length} caracteres)` : result.data;
1706
+ logger.success(`${tc.name}: ${display}`);
1707
+ } else if (result.error) {
1708
+ logger.error(`${tc.name}: ${result.error}`);
1709
+ }
1710
+ toolMessages.push({
1711
+ role: "user",
1712
+ content: [{
1713
+ type: "tool_result",
1714
+ tool_use_id: tc.id,
1715
+ content: result.success ? result.data : result.error || "Sem sa\xEDda"
1716
+ }]
1717
+ });
1718
+ }
1719
+ } else {
1720
+ const stopRotate = spinnerWithRotation(s);
1721
+ const response = await provider.chat(apiMessages, config.getAll(), allTools);
1722
+ stopRotate();
1723
+ if (response.toolCalls.length === 0) {
1724
+ return response.text;
1725
+ }
1726
+ if (response.text) {
1727
+ toolMessages.push({
1728
+ role: "assistant",
1729
+ content: [{ type: "text", text: response.text }]
1730
+ });
1731
+ }
1732
+ for (const tc of response.toolCalls) {
1733
+ let inputObj;
1734
+ try {
1735
+ inputObj = JSON.parse(tc.input);
1736
+ } catch {
1737
+ inputObj = {};
1738
+ }
1739
+ toolMessages.push({
1740
+ role: "assistant",
1741
+ content: [{
1742
+ type: "tool_use",
1743
+ id: tc.id,
1744
+ name: tc.name,
1745
+ input: inputObj
1746
+ }]
1747
+ });
1748
+ s.start(toolMessage(tc.name, inputObj));
1749
+ const result = await runTool(tc.name, inputObj, s);
1750
+ s.stop();
1751
+ if (result.success) {
1752
+ const display = result.data.length > 300 ? result.data.slice(0, 300) + "..." : result.data;
1753
+ logger.success(`${tc.name}: ${display}`);
1754
+ } else if (result.error) {
1755
+ logger.error(`${tc.name}: ${result.error}`);
1756
+ }
1757
+ toolMessages.push({
1758
+ role: "user",
1759
+ content: [{
1760
+ type: "tool_result",
1761
+ tool_use_id: tc.id,
1762
+ content: result.success ? result.data : result.error || "Sem sa\xEDda"
1763
+ }]
1764
+ });
1765
+ }
1766
+ }
1767
+ }
1768
+ return "";
1769
+ }
1770
+ async function runTool(name, input2, s) {
1771
+ const tool = getTool(name);
1772
+ if (!tool) {
1773
+ return { success: false, data: "", error: `Ferramenta desconhecida: ${name}` };
1774
+ }
1775
+ const confirmCheck = tool.needsConfirmation(input2);
1776
+ if (confirmCheck.needed) {
1777
+ s.stop();
1778
+ const ok = await askConfirm(confirmCheck.message || `Permitir ${name}?`);
1779
+ s.start("Continuando\u2026");
1780
+ if (!ok) {
1781
+ return { success: false, data: "", error: "Opera\xE7\xE3o cancelada pelo usu\xE1rio" };
1782
+ }
1783
+ }
1784
+ const result = await tool.execute(input2);
1785
+ return result;
1786
+ }
1787
+
1788
+ // src/commands/chat.ts
1789
+ async function chatCommand(options) {
1790
+ const config2 = new ConfigManager();
1791
+ if (!config2.get("apiKey")) {
1792
+ logger.warn("API key n\xE3o configurada.");
1793
+ logger.info("Configure com: tuticli config set api-key SUA_API_KEY");
1794
+ return;
1795
+ }
1796
+ try {
1797
+ await startChat(options.session);
1798
+ } catch (err) {
1799
+ logger.error(`Erro no chat: ${err.message}`);
1800
+ }
1801
+ }
1802
+
1803
+ // src/commands/models.ts
1804
+ import chalk4 from "chalk";
1805
+ async function modelsCommand() {
1806
+ const config2 = new ConfigManager();
1807
+ if (!config2.get("apiKey")) {
1808
+ logger.warn("API key n\xE3o configurada.");
1809
+ logger.info("Configure com: tuticli config set api-key SUA_API_KEY");
1810
+ return;
1811
+ }
1812
+ process.stderr.write("Buscando modelos\u2026\r");
1813
+ try {
1814
+ const provider = getProvider();
1815
+ const models = await provider.listModels(config2.getAll());
1816
+ process.stderr.write(" ".repeat(20) + "\r");
1817
+ console.log(chalk4.bold("\n Modelos dispon\xEDveis:\n"));
1818
+ models.forEach((model, i) => {
1819
+ const isActive = model === config2.get("model");
1820
+ const prefix = isActive ? chalk4.bold("\u2605") : chalk4.dim(`${String(i + 1).padStart(2, " ")}.`);
1821
+ const name = isActive ? chalk4.bold(model) : model;
1822
+ const tag = isActive ? chalk4.dim(" (em uso)") : "";
1823
+ console.log(` ${prefix} ${name}${tag}`);
1824
+ });
1825
+ console.log();
1826
+ } catch (err) {
1827
+ process.stderr.write(" ".repeat(20) + "\r");
1828
+ logger.error(`Erro ao listar modelos: ${err.message}`);
1829
+ }
1830
+ }
1831
+
1832
+ // src/commands/config.ts
1833
+ function configCommand(subcommand, key, value) {
1834
+ const manager = new ConfigManager();
1835
+ switch (subcommand) {
1836
+ case "set":
1837
+ if (!key || !value) {
1838
+ logger.error("Uso: tuticli config set <chave> <valor>");
1839
+ logger.info("Chaves: api-key, model, endpoint, max-tokens");
1840
+ return;
1841
+ }
1842
+ manager.set(key, value);
1843
+ break;
1844
+ case "get":
1845
+ if (!key) {
1846
+ logger.error("Uso: tuticli config get <chave>");
1847
+ return;
1848
+ }
1849
+ {
1850
+ const config2 = manager.getAll();
1851
+ const configKey = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1852
+ const val = config2[configKey];
1853
+ if (val !== void 0) {
1854
+ const display = configKey === "apiKey" ? val.slice(0, 4) + "\u2026" + val.slice(-4) : String(val);
1855
+ console.log(`${key}: ${display}`);
1856
+ } else {
1857
+ logger.warn(`Chave "${key}" n\xE3o encontrada`);
1858
+ }
1859
+ }
1860
+ break;
1861
+ case "show":
1862
+ manager.show();
1863
+ break;
1864
+ case "reset":
1865
+ manager.reset();
1866
+ break;
1867
+ default:
1868
+ logger.error("Subcomando inv\xE1lido. Use: set, get, show, reset");
1869
+ }
1870
+ }
1871
+
1872
+ // src/commands/clear.ts
1873
+ import { confirm } from "@clack/prompts";
1874
+ async function clearCommand(options) {
1875
+ const storage2 = new Storage();
1876
+ if (options.session) {
1877
+ const session = storage2.getSession(options.session);
1878
+ if (!session) {
1879
+ logger.warn(`Sess\xE3o "${options.session}" n\xE3o encontrada.`);
1880
+ storage2.close();
1881
+ return;
1882
+ }
1883
+ const msgCount = storage2.getMessageCount(session.id);
1884
+ const proceed = options.force || await confirm({
1885
+ message: `Limpar sess\xE3o "${session.name}" (${msgCount} mensagens)?`
1886
+ });
1887
+ if (proceed) {
1888
+ storage2.deleteSession(session.id);
1889
+ logger.success(`Sess\xE3o "${session.name}" removida.`);
1890
+ }
1891
+ } else if (options.all) {
1892
+ const sessions = storage2.listSessions();
1893
+ if (sessions.length === 0) {
1894
+ logger.info("Nenhuma sess\xE3o para limpar.");
1895
+ storage2.close();
1896
+ return;
1897
+ }
1898
+ renderSessionsTable(sessions);
1899
+ const proceed = options.force || await confirm({
1900
+ message: `Remover TODAS as ${sessions.length} sess\xF5es e mensagens?`
1901
+ });
1902
+ if (proceed) {
1903
+ storage2.clearAll();
1904
+ logger.success(`Todo o hist\xF3rico foi limpo (${sessions.length} sess\xF5es removidas).`);
1905
+ }
1906
+ } else {
1907
+ const sessions = storage2.listSessions();
1908
+ if (sessions.length === 0) {
1909
+ logger.info("Nenhuma sess\xE3o para limpar.");
1910
+ storage2.close();
1911
+ return;
1912
+ }
1913
+ renderSessionsTable(sessions);
1914
+ logger.info("Use --all para limpar tudo, ou --session <id> para limpar uma espec\xEDfica.");
1915
+ }
1916
+ storage2.close();
1917
+ }
1918
+
1919
+ // src/commands/version.ts
1920
+ import chalk5 from "chalk";
1921
+ var VERSION = "0.1.0";
1922
+ function versionCommand() {
1923
+ const nodeVer = process.versions.node;
1924
+ const platform = process.platform;
1925
+ const arch = process.arch;
1926
+ console.log(chalk5.bold(`
1927
+ tuticli v${VERSION}`));
1928
+ console.log(chalk5.dim(` node ${nodeVer} | ${platform} (${arch})`));
1929
+ console.log(chalk5.dim(` CLI interativo para Claude AI
1930
+ `));
1931
+ }
1932
+
1933
+ // src/commands/update.ts
1934
+ import { execSync as execSync2 } from "child_process";
1935
+ import chalk6 from "chalk";
1936
+ var VERSION2 = "0.1.0";
1937
+ async function updateCommand() {
1938
+ console.log(chalk6.bold("\n Verificando atualiza\xE7\xF5es\u2026\n"));
1939
+ try {
1940
+ const result = execSync2("npm view tuticli version --no-optional --silent 2>/dev/null", {
1941
+ encoding: "utf-8",
1942
+ timeout: 1e4
1943
+ }).trim();
1944
+ const latest = result;
1945
+ if (latest === VERSION2) {
1946
+ console.log(chalk6.bold(` \u2714 Voc\xEA j\xE1 est\xE1 na vers\xE3o mais recente (v${VERSION2})
1947
+ `));
1948
+ return;
1949
+ }
1950
+ console.log(chalk6.bold(` \u26A0 Vers\xE3o dispon\xEDvel: v${latest}`));
1951
+ console.log(chalk6.dim(` Vers\xE3o atual: v${VERSION2}
1952
+ `));
1953
+ const { confirm: confirm2 } = await import("@clack/prompts");
1954
+ const shouldUpdate = await confirm2({
1955
+ message: `Atualizar para v${latest}?`
1956
+ });
1957
+ if (shouldUpdate) {
1958
+ console.log(chalk6.dim("\n Executando: npm install -g tuticli@latest\n"));
1959
+ execSync2("npm install -g tuticli@latest", { stdio: "inherit" });
1960
+ console.log(chalk6.bold(`
1961
+ \u2714 Atualizado para v${latest}
1962
+ `));
1963
+ }
1964
+ } catch (err) {
1965
+ console.log(chalk6.dim(` Vers\xE3o atual: v${VERSION2}
1966
+ `));
1967
+ console.log(chalk6.dim(" N\xE3o foi poss\xEDvel verificar atualiza\xE7\xF5es."));
1968
+ console.log(chalk6.dim(" Verifique manualmente com: npm view tuticli\n"));
1969
+ }
1970
+ }
1971
+
1972
+ // src/cli.ts
1973
+ var VERSION3 = "0.1.0";
1974
+ var program = new Command();
1975
+ program.name("tuticli").description("CLI interativo para Claude AI").version(VERSION3).helpOption("-h, --help", "Mostra ajuda");
1976
+ program.command("chat").description("Inicia uma sess\xE3o de chat interativa").option("--new", "For\xE7a cria\xE7\xE3o de nova sess\xE3o").option("-s, --session <id>", "ID da sess\xE3o para continuar").action(async (options) => {
1977
+ await chatCommand(options);
1978
+ });
1979
+ program.command("models").description("Lista modelos dispon\xEDveis do provider").action(async () => {
1980
+ await modelsCommand();
1981
+ });
1982
+ program.command("config").description("Gerencia configura\xE7\xF5es do CLI").argument("[subcommand]", "Subcomando: set, get, show, reset").argument("[key]", "Chave da configura\xE7\xE3o").argument("[value]", "Valor da configura\xE7\xE3o").action((subcommand, key, value) => {
1983
+ if (!subcommand || subcommand === "show") {
1984
+ configCommand("show");
1985
+ return;
1986
+ }
1987
+ configCommand(subcommand, key, value);
1988
+ });
1989
+ program.command("clear").description("Limpa hist\xF3rico de conversas").option("--all", "Limpa todas as sess\xF5es").option("-s, --session <id>", "Limpa uma sess\xE3o espec\xEDfica").option("-f, --force", "Pula confirma\xE7\xE3o").action(async (options) => {
1990
+ await clearCommand(options);
1991
+ });
1992
+ program.command("version").description("Mostra a vers\xE3o instalada").action(() => {
1993
+ versionCommand();
1994
+ });
1995
+ program.command("update").description("Verifica e instala atualiza\xE7\xF5es").action(async () => {
1996
+ await updateCommand();
1997
+ });
1998
+ if (process.argv.length <= 2) {
1999
+ process.argv.push("chat");
2000
+ }
2001
+ program.parse(process.argv);