epa-testeprojetoia 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 (100) hide show
  1. package/.idea/epa_mcp.iml +8 -0
  2. package/.idea/modules.xml +8 -0
  3. package/.idea/php.xml +19 -0
  4. package/.idea/vcs.xml +6 -0
  5. package/AGENTS.md +10 -0
  6. package/README.md +339 -0
  7. package/dist/agent/agentHelpers.js +21 -0
  8. package/dist/agent/openaiAgent.js +170 -0
  9. package/dist/agent/slidingWindow.js +13 -0
  10. package/dist/api/epaApiClient.js +59 -0
  11. package/dist/cli/index.js +47 -0
  12. package/dist/config/credentialStore.js +92 -0
  13. package/dist/config/ensureCliAuth.js +127 -0
  14. package/dist/config/loadConfig.js +40 -0
  15. package/dist/config/setup.js +23 -0
  16. package/dist/core/createReferenceTool.js +12 -0
  17. package/dist/core/createTool.js +32 -0
  18. package/dist/mocks/requestMocks.js +47 -0
  19. package/dist/server/server.js +41 -0
  20. package/dist/server/stdioServer.js +3 -0
  21. package/dist/services/request/requestFilters.js +1 -0
  22. package/dist/services/requestService.js +81 -0
  23. package/dist/services/teamService.js +18 -0
  24. package/dist/sql/createSqlConnection.js +13 -0
  25. package/dist/sql/fetchSchemaSummary.js +38 -0
  26. package/dist/sql/generateSqlFromQuestion.js +83 -0
  27. package/dist/sql/generateSqlPlan.js +111 -0
  28. package/dist/sql/getSqlAgentErrorMessage.js +15 -0
  29. package/dist/sql/loadSqlAgentConfig.js +24 -0
  30. package/dist/sql/loadSqlConfig.js +43 -0
  31. package/dist/sql/parseSqlQuestionHints.js +29 -0
  32. package/dist/sql/runSqlAgentCli.js +163 -0
  33. package/dist/sql/runSqlCli.js +34 -0
  34. package/dist/sql/selectRelevantTables.js +136 -0
  35. package/dist/sql/sqlGuard.js +21 -0
  36. package/dist/sql/sqlPlan.js +16 -0
  37. package/dist/tests/requestService.test.js +110 -0
  38. package/dist/tools/analytics/teamReport.draft.js +16 -0
  39. package/dist/tools/loadTools.js +40 -0
  40. package/dist/tools/requests/assignees.js +50 -0
  41. package/dist/tools/requests/clients.draft.js +10 -0
  42. package/dist/tools/requests/create.draft.js +51 -0
  43. package/dist/tools/requests/list.js +50 -0
  44. package/dist/tools/requests/priorities.js +2 -0
  45. package/dist/tools/requests/services.draft.js +20 -0
  46. package/dist/tools/requests/types.js +16 -0
  47. package/dist/tools/requests/units.draft.js +10 -0
  48. package/dist/tools/requests/view.draft.js +20 -0
  49. package/dist/utils/buildDateRange.js +18 -0
  50. package/dist/utils/findIdByDescription.js +4 -0
  51. package/dist/utils/resolveAssigneeId.js +14 -0
  52. package/dist/utils/toolNameMaps.js +23 -0
  53. package/package.json +31 -0
  54. package/src/agent/agentHelpers.ts +25 -0
  55. package/src/agent/openaiAgent.ts +205 -0
  56. package/src/agent/slidingWindow.ts +17 -0
  57. package/src/api/epaApiClient.ts +82 -0
  58. package/src/cli/index.ts +61 -0
  59. package/src/config/credentialStore.ts +130 -0
  60. package/src/config/ensureCliAuth.ts +152 -0
  61. package/src/config/loadConfig.ts +62 -0
  62. package/src/config/setup.ts +35 -0
  63. package/src/core/createReferenceTool.ts +17 -0
  64. package/src/core/createTool.ts +51 -0
  65. package/src/mocks/requestMocks.ts +52 -0
  66. package/src/server/server.ts +61 -0
  67. package/src/server/stdioServer.ts +5 -0
  68. package/src/services/request/requestFilters.ts +12 -0
  69. package/src/services/requestService.ts +126 -0
  70. package/src/services/teamService.ts +27 -0
  71. package/src/sql/createSqlConnection.ts +15 -0
  72. package/src/sql/fetchSchemaSummary.ts +64 -0
  73. package/src/sql/generateSqlFromQuestion.ts +105 -0
  74. package/src/sql/generateSqlPlan.ts +133 -0
  75. package/src/sql/getSqlAgentErrorMessage.ts +24 -0
  76. package/src/sql/loadSqlAgentConfig.ts +33 -0
  77. package/src/sql/loadSqlConfig.ts +75 -0
  78. package/src/sql/parseSqlQuestionHints.ts +46 -0
  79. package/src/sql/runSqlAgentCli.ts +204 -0
  80. package/src/sql/runSqlCli.ts +40 -0
  81. package/src/sql/selectRelevantTables.ts +184 -0
  82. package/src/sql/sqlGuard.ts +28 -0
  83. package/src/sql/sqlPlan.ts +28 -0
  84. package/src/tests/requestService.test.ts +152 -0
  85. package/src/tools/analytics/teamReport.draft.ts +25 -0
  86. package/src/tools/loadTools.ts +59 -0
  87. package/src/tools/requests/assignees.ts +59 -0
  88. package/src/tools/requests/clients.draft.ts +18 -0
  89. package/src/tools/requests/create.draft.ts +59 -0
  90. package/src/tools/requests/list.ts +57 -0
  91. package/src/tools/requests/priorities.ts +6 -0
  92. package/src/tools/requests/services.draft.ts +24 -0
  93. package/src/tools/requests/types.ts +18 -0
  94. package/src/tools/requests/units.draft.ts +18 -0
  95. package/src/tools/requests/view.draft.ts +27 -0
  96. package/src/utils/buildDateRange.ts +22 -0
  97. package/src/utils/findIdByDescription.ts +10 -0
  98. package/src/utils/resolveAssigneeId.ts +24 -0
  99. package/src/utils/toolNameMaps.ts +33 -0
  100. package/tsconfig.json +11 -0
@@ -0,0 +1,205 @@
1
+ import OpenAI from "openai"
2
+ import readline from "readline"
3
+ import path from "path"
4
+ import { fileURLToPath } from "url"
5
+
6
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js"
7
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
8
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"
9
+ import { loadConfig } from "../config/loadConfig.js"
10
+ import { ensureCliAuth } from "../config/ensureCliAuth.js"
11
+ import { createMcpServer } from "../server/server.js"
12
+ import { applySlidingWindow } from "./slidingWindow.js"
13
+ import { buildToolNameMaps, getErrorMessage, isSpawnPermissionError } from "./agentHelpers.js"
14
+
15
+ const DEFAULT_MAX_NON_SYSTEM_MESSAGES = 30
16
+
17
+ export async function startAgent() {
18
+ await ensureCliAuth()
19
+
20
+ const config = loadConfig()
21
+ const apiKey = process.env.OPENAI_API_KEY || config.openaiApiKey
22
+
23
+ if (!apiKey) {
24
+ throw new Error("OpenAI API key nao encontrada. Defina OPENAI_API_KEY ou configure via setup.")
25
+ }
26
+
27
+ const openai = new OpenAI({ apiKey })
28
+
29
+ const currentFilePath = fileURLToPath(import.meta.url)
30
+ const isDevRuntime = currentFilePath.endsWith(".ts")
31
+ const cliEntry = path.resolve(
32
+ path.dirname(currentFilePath),
33
+ "..",
34
+ "cli",
35
+ isDevRuntime ? "index.ts" : "index.js"
36
+ )
37
+
38
+ const client = new Client(
39
+ {
40
+ name: "epa-agent",
41
+ version: "0.1.0"
42
+ },
43
+ { capabilities: {} }
44
+ )
45
+
46
+ try {
47
+ const transport = new StdioClientTransport(
48
+ isDevRuntime
49
+ ? {
50
+ command: process.execPath,
51
+ args: ["--import", "tsx", cliEntry, "server"]
52
+ }
53
+ : {
54
+ command: process.execPath,
55
+ args: [cliEntry, "server"]
56
+ }
57
+ )
58
+ await client.connect(transport)
59
+ } catch (error) {
60
+ if (!isSpawnPermissionError(error)) {
61
+ throw error
62
+ }
63
+
64
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
65
+ const server = await createMcpServer()
66
+ await server.connect(serverTransport)
67
+ await client.connect(clientTransport)
68
+ console.log("Aviso: modo in-memory ativado por restricao de spawn no ambiente.")
69
+ }
70
+
71
+ const toolsResponse = await client.listTools()
72
+ const toolNames = toolsResponse.tools.map((tool: any) => tool.name)
73
+ const { mcpToOpenAi, openAiToMcp } = buildToolNameMaps(toolNames)
74
+
75
+ const tools = toolsResponse.tools.map((tool: any) => ({
76
+ type: "function" as const,
77
+ function: {
78
+ name: mcpToOpenAi.get(tool.name) ?? tool.name,
79
+ description: tool.description,
80
+ parameters: tool.inputSchema
81
+ }
82
+ }))
83
+
84
+ const rl = readline.createInterface({
85
+ input: process.stdin,
86
+ output: process.stdout
87
+ })
88
+
89
+ console.log("EPA Agent iniciado")
90
+
91
+ const messages: any[] = [
92
+ {
93
+ role: "system",
94
+ content: `
95
+ Voce e um assistente que opera o sistema EPA.
96
+
97
+ Sempre utilize as ferramentas disponiveis quando necessario.
98
+
99
+ Fluxo recomendado para criar solicitacoes:
100
+
101
+ 1. consultar requests.types
102
+ 2. consultar requests.services
103
+ 3. consultar requests.priorities/requests.assignees/requests.clients
104
+ 4. executar requests.create
105
+
106
+ Para listar solicitacoes por usuario e periodo, prefira requests.list com:
107
+ - assignee_name (ex: suporte.simeon)
108
+ - period=last_12_months para "ultimo ano"
109
+
110
+ Regra para analise:
111
+ - Se voce ja recebeu uma lista de solicitacoes no contexto atual, faca a analise diretamente desses dados.
112
+ - Nao chame analytics.teamReport para analisar dados que ja estao no contexto.
113
+ - So use analytics.teamReport quando o usuario pedir explicitamente para buscar analise de um usuario especifico ou da equipe (ex: "analise da equipe", "relatorio do usuario X").
114
+ - Se analytics.teamReport nao for necessario, responda sem chamar tools.
115
+ `
116
+ }
117
+ ]
118
+ const configuredMax = Number(process.env.AGENT_MAX_CONTEXT_MESSAGES)
119
+ const maxNonSystemMessages = Number.isFinite(configuredMax) && configuredMax > 0
120
+ ? configuredMax
121
+ : DEFAULT_MAX_NON_SYSTEM_MESSAGES
122
+
123
+ rl.on("line", async (input) => {
124
+
125
+ try {
126
+ messages.push({
127
+ role: "user",
128
+ content: input
129
+ })
130
+ applySlidingWindow(messages, maxNonSystemMessages)
131
+
132
+ let running = true
133
+
134
+ while (running) {
135
+ const completion = await openai.chat.completions.create({
136
+ model: "gpt-4o-mini",
137
+ messages,
138
+ tools,
139
+ tool_choice: "auto"
140
+ })
141
+
142
+ const message = completion.choices[0].message
143
+
144
+ messages.push(message)
145
+ applySlidingWindow(messages, maxNonSystemMessages)
146
+
147
+ if (!message.tool_calls) {
148
+ console.log("\n[agent]", message.content, "\n")
149
+ running = false
150
+ break
151
+ }
152
+
153
+ for (const toolCall of message.tool_calls) {
154
+ const openAiToolName = toolCall.function.name
155
+ const toolName = openAiToMcp.get(openAiToolName) ?? openAiToolName
156
+
157
+ let args: unknown
158
+ try {
159
+ args = JSON.parse(toolCall.function.arguments || "{}")
160
+ } catch (error) {
161
+ const errorMessage = getErrorMessage(error)
162
+ console.error(`\n[erro] Argumentos invalidos para ${toolName}: ${errorMessage}\n`)
163
+ messages.push({
164
+ role: "tool",
165
+ tool_call_id: toolCall.id,
166
+ content: JSON.stringify([{ type: "text", text: `Erro: argumentos invalidos (${errorMessage})` }])
167
+ })
168
+ applySlidingWindow(messages, maxNonSystemMessages)
169
+ continue
170
+ }
171
+
172
+ console.log(`\n[tool] Executando: ${toolName}`)
173
+
174
+ try {
175
+ const result = await client.callTool({
176
+ name: toolName,
177
+ arguments: args as Record<string, unknown>
178
+ })
179
+
180
+ messages.push({
181
+ role: "tool",
182
+ tool_call_id: toolCall.id,
183
+ content: JSON.stringify(result.content)
184
+ })
185
+ applySlidingWindow(messages, maxNonSystemMessages)
186
+ } catch (error) {
187
+ const errorMessage = getErrorMessage(error)
188
+ console.error(`\n[erro] Falha ao executar ${toolName}: ${errorMessage}\n`)
189
+ messages.push({
190
+ role: "tool",
191
+ tool_call_id: toolCall.id,
192
+ content: JSON.stringify([{ type: "text", text: `Erro ao executar tool: ${errorMessage}` }])
193
+ })
194
+ applySlidingWindow(messages, maxNonSystemMessages)
195
+ }
196
+ }
197
+ }
198
+ } catch (error) {
199
+ const errorMessage = getErrorMessage(error)
200
+ console.error(`\n[erro] Falha no processamento da mensagem: ${errorMessage}\n`)
201
+ }
202
+
203
+ })
204
+
205
+ }
@@ -0,0 +1,17 @@
1
+ export function applySlidingWindow(messages: any[], maxNonSystemMessages: number) {
2
+ if (!Number.isFinite(maxNonSystemMessages) || maxNonSystemMessages < 1) {
3
+ return
4
+ }
5
+
6
+ const systemMessage = messages.find((message) => message?.role === "system")
7
+ const nonSystemMessages = messages.filter((message) => message?.role !== "system")
8
+ const trimmedNonSystem = nonSystemMessages.slice(-maxNonSystemMessages)
9
+
10
+ messages.length = 0
11
+
12
+ if (systemMessage) {
13
+ messages.push(systemMessage)
14
+ }
15
+
16
+ messages.push(...trimmedNonSystem)
17
+ }
@@ -0,0 +1,82 @@
1
+ import axios, { AxiosInstance } from "axios"
2
+ import { loadConfig } from "../config/loadConfig.js"
3
+
4
+ export class EpaApiClient {
5
+
6
+ private client: AxiosInstance
7
+
8
+ constructor() {
9
+
10
+ const config = loadConfig()
11
+
12
+ const timeoutMs = Number(
13
+ config.epaApiTimeoutMs ??
14
+ process.env.EPA_API_TIMEOUT_MS ??
15
+ 15000
16
+ )
17
+
18
+ this.client = axios.create({
19
+ baseURL: config.epaApiUrl,
20
+ timeout: Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 15000,
21
+ headers: {
22
+ Authorization: `Bearer ${config.epaApiToken}`
23
+ }
24
+ })
25
+
26
+ }
27
+
28
+ async get(endpoint: string, params?: any) {
29
+
30
+ try {
31
+ const response = await this.client.get(endpoint, { params })
32
+ return response.data
33
+ } catch (error) {
34
+ throw this.formatError(error, "GET", endpoint)
35
+ }
36
+
37
+ }
38
+
39
+ async post(endpoint: string, body: any = {}, params?: any) {
40
+
41
+ try {
42
+ const response = await this.client.post(endpoint, body, { params })
43
+ return response.data
44
+ } catch (error) {
45
+ throw this.formatError(error, "POST", endpoint)
46
+ }
47
+
48
+ }
49
+
50
+ private formatError(error: unknown, method: string, endpoint: string): Error {
51
+
52
+ if (axios.isAxiosError(error)) {
53
+ const status = error.response?.status
54
+ const apiMessage = this.extractApiMessage(error.response?.data)
55
+ const suffix = status ? `status ${status}` : "sem status HTTP"
56
+ return new Error(`[EPA API] ${method} ${endpoint} falhou (${suffix}): ${apiMessage}`)
57
+ }
58
+
59
+ const message = error instanceof Error ? error.message : String(error)
60
+ return new Error(`[EPA API] ${method} ${endpoint} falhou: ${message}`)
61
+
62
+ }
63
+
64
+ private extractApiMessage(data: unknown): string {
65
+
66
+ if (typeof data === "string" && data.trim()) {
67
+ return data
68
+ }
69
+
70
+ if (data && typeof data === "object") {
71
+ const maybeMessage = (data as any).message ?? (data as any).error
72
+ if (typeof maybeMessage === "string" && maybeMessage.trim()) {
73
+ return maybeMessage
74
+ }
75
+ return JSON.stringify(data)
76
+ }
77
+
78
+ return "erro sem detalhes retornados pela API"
79
+
80
+ }
81
+
82
+ }
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import { startServer } from "../server/server.js";
5
+ import { setupConfig } from "../config/setup.js";
6
+ import { ensureCliAuth } from "../config/ensureCliAuth.js";
7
+ import { startAgent } from "../agent/openaiAgent.js"
8
+ import { runSqlCli } from "../sql/runSqlCli.js";
9
+ import { runSqlAgentCli } from "../sql/runSqlAgentCli.js";
10
+
11
+ const program = new Command();
12
+
13
+ program
14
+ .command("setup")
15
+ .description("Configurar URL da API do EPA")
16
+ .action(async () => {
17
+ await setupConfig();
18
+ });
19
+
20
+ program
21
+ .command("login")
22
+ .description("Realiza login na API do EPA e salva token para CLI")
23
+ .action(async () => {
24
+ await ensureCliAuth();
25
+ });
26
+
27
+ program
28
+ .command("server")
29
+ .description("Iniciar MCP Server")
30
+ .action(async () => {
31
+
32
+ console.error("Iniciando EPA MCP Server...");
33
+
34
+ await startServer();
35
+
36
+ });
37
+
38
+ program
39
+ .command("agent")
40
+ .description("Inicia agente OpenAI conectado ao MCP")
41
+ .action(async () => {
42
+
43
+ await startAgent()
44
+
45
+ });
46
+
47
+ program
48
+ .command("sql [query]")
49
+ .description("Executa consulta SQL técnica read-only no MySQL/MariaDB")
50
+ .action(async (query?: string) => {
51
+ await runSqlCli(query)
52
+ });
53
+
54
+ program
55
+ .command("sql-agent [question]")
56
+ .description("Usa LLM para gerar e executar SQL read-only no MySQL/MariaDB")
57
+ .action(async (question?: string) => {
58
+ await runSqlAgentCli(question)
59
+ });
60
+
61
+ program.parse();
@@ -0,0 +1,130 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+
4
+ type ConfigJson = {
5
+ epaApiUrl?: string
6
+ epaApiToken?: string
7
+ epaApiTimeoutMs?: number
8
+ openaiApiKey?: string
9
+ }
10
+
11
+ function parseEnvFile(raw: string): Record<string, string> {
12
+ const result: Record<string, string> = {}
13
+ const lines = raw.split(/\r?\n/)
14
+
15
+ for (const line of lines) {
16
+ const trimmed = line.trim()
17
+ if (!trimmed || trimmed.startsWith("#")) {
18
+ continue
19
+ }
20
+
21
+ const index = trimmed.indexOf("=")
22
+ if (index < 0) {
23
+ continue
24
+ }
25
+
26
+ const key = trimmed.slice(0, index).trim()
27
+ const value = trimmed.slice(index + 1).trim()
28
+ result[key] = value
29
+ }
30
+
31
+ return result
32
+ }
33
+
34
+ function stringifyEnv(values: Record<string, string>): string {
35
+ return Object.entries(values)
36
+ .map(([key, value]) => `${key}=${value}`)
37
+ .join("\n")
38
+ }
39
+
40
+ export function resolveCredentialStorePath(): { kind: "env" | "config"; path: string } {
41
+ const envPath = path.join(process.cwd(), ".env")
42
+ if (fs.existsSync(envPath)) {
43
+ return { kind: "env", path: envPath }
44
+ }
45
+
46
+ const configPath = path.join(process.cwd(), "config", "config.json")
47
+ return { kind: "config", path: configPath }
48
+ }
49
+
50
+ export function readStoredCredentials(): {
51
+ epaApiUrl?: string
52
+ epaApiToken?: string
53
+ openaiApiKey?: string
54
+ epaApiTimeoutMs?: number
55
+ } {
56
+ const store = resolveCredentialStorePath()
57
+
58
+ if (store.kind === "env") {
59
+ const raw = fs.readFileSync(store.path, "utf-8")
60
+ const values = parseEnvFile(raw)
61
+ const timeout = Number(values.EPA_API_TIMEOUT_MS ?? values.epaApiTimeoutMs)
62
+
63
+ return {
64
+ epaApiUrl: values.EPA_API_URL ?? values.epaApiUrl,
65
+ epaApiToken: values.EPA_API_TOKEN ?? values.epaApiToken,
66
+ openaiApiKey: values.OPENAI_API_KEY ?? values.openaiApiKey,
67
+ epaApiTimeoutMs: Number.isFinite(timeout) && timeout > 0 ? timeout : undefined
68
+ }
69
+ }
70
+
71
+ if (!fs.existsSync(store.path)) {
72
+ return {}
73
+ }
74
+
75
+ const raw = fs.readFileSync(store.path, "utf-8")
76
+ const json = JSON.parse(raw) as ConfigJson
77
+ return {
78
+ epaApiUrl: json.epaApiUrl,
79
+ epaApiToken: json.epaApiToken,
80
+ openaiApiKey: json.openaiApiKey,
81
+ epaApiTimeoutMs: json.epaApiTimeoutMs
82
+ }
83
+ }
84
+
85
+ export function writeCredentials(values: {
86
+ epaApiUrl?: string
87
+ epaApiToken?: string
88
+ openaiApiKey?: string
89
+ epaApiTimeoutMs?: number
90
+ }) {
91
+ const store = resolveCredentialStorePath()
92
+
93
+ if (store.kind === "env") {
94
+ const existing = fs.existsSync(store.path)
95
+ ? parseEnvFile(fs.readFileSync(store.path, "utf-8"))
96
+ : {}
97
+
98
+ if (values.epaApiUrl) {
99
+ existing.EPA_API_URL = values.epaApiUrl
100
+ }
101
+ if (values.epaApiToken) {
102
+ existing.EPA_API_TOKEN = values.epaApiToken
103
+ }
104
+ if (values.openaiApiKey) {
105
+ existing.OPENAI_API_KEY = values.openaiApiKey
106
+ }
107
+ if (values.epaApiTimeoutMs !== undefined) {
108
+ existing.EPA_API_TIMEOUT_MS = String(values.epaApiTimeoutMs)
109
+ }
110
+
111
+ fs.writeFileSync(store.path, `${stringifyEnv(existing)}\n`)
112
+ return
113
+ }
114
+
115
+ const configDir = path.dirname(store.path)
116
+ if (!fs.existsSync(configDir)) {
117
+ fs.mkdirSync(configDir)
118
+ }
119
+
120
+ const current = fs.existsSync(store.path)
121
+ ? (JSON.parse(fs.readFileSync(store.path, "utf-8")) as ConfigJson)
122
+ : {}
123
+
124
+ const next: ConfigJson = {
125
+ ...current,
126
+ ...values
127
+ }
128
+
129
+ fs.writeFileSync(store.path, JSON.stringify(next, null, 2))
130
+ }
@@ -0,0 +1,152 @@
1
+ import axios from "axios"
2
+ import readline from "readline"
3
+ import { readStoredCredentials, writeCredentials } from "./credentialStore.js"
4
+
5
+ function askQuestion(rl: readline.Interface, question: string): Promise<string> {
6
+ return new Promise((resolve) => rl.question(question, resolve))
7
+ }
8
+
9
+ function askHiddenQuestion(question: string): Promise<string> {
10
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
11
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
12
+ return new Promise((resolve) => {
13
+ rl.question(question, (answer) => {
14
+ rl.close()
15
+ resolve(answer)
16
+ })
17
+ })
18
+ }
19
+
20
+ return new Promise((resolve, reject) => {
21
+ let value = ""
22
+ const stdin = process.stdin
23
+ const stdout = process.stdout
24
+ const canSetRawMode = typeof stdin.setRawMode === "function"
25
+
26
+ stdout.write(question)
27
+ stdout.write("\x1B[8m")
28
+ stdin.setEncoding("utf8")
29
+ if (canSetRawMode) {
30
+ stdin.setRawMode(true)
31
+ }
32
+ stdin.resume()
33
+
34
+ const onData = (char: string) => {
35
+
36
+ if (char === "\u0003") {
37
+ cleanup()
38
+ reject(new Error("Entrada cancelada pelo usuário."))
39
+ return
40
+ }
41
+
42
+ if (char === "\r" || char === "\n") {
43
+ stdout.write("\n")
44
+ cleanup()
45
+ resolve(value)
46
+ return
47
+ }
48
+
49
+ if (char === "\u0008" || char === "\u007F") {
50
+ if (value.length > 0) {
51
+ value = value.slice(0, -1)
52
+ stdout.write("\b \b")
53
+ }
54
+ return
55
+ }
56
+
57
+ const sanitized = char.replace(/[\r\n]/g, "")
58
+ if (!sanitized) {
59
+ return
60
+ }
61
+
62
+ value += sanitized
63
+ stdout.write("*".repeat(sanitized.length))
64
+ }
65
+
66
+ const cleanup = () => {
67
+ stdin.off("data", onData)
68
+ if (canSetRawMode) {
69
+ stdin.setRawMode(false)
70
+ }
71
+ stdout.write("\x1B[28m")
72
+ stdin.pause()
73
+ }
74
+
75
+ stdin.on("data", onData)
76
+ })
77
+ }
78
+
79
+ async function isTokenValid(baseURL: string, token: string): Promise<boolean> {
80
+ try {
81
+ await axios.get(`${baseURL}/api/api/prioridade`, {
82
+ headers: { Authorization: `Bearer ${token}` },
83
+ timeout: 15000
84
+ })
85
+ return true
86
+ } catch {
87
+ return false
88
+ }
89
+ }
90
+
91
+ async function loginToEpa(baseURL: string, login: string, senha: string): Promise<string> {
92
+ const response = await axios.post(
93
+ `${baseURL}/api/api/novo/login`,
94
+ { login, senha },
95
+ { timeout: 15000 }
96
+ )
97
+
98
+ const token = response?.data?.access_token
99
+ if (!token || typeof token !== "string") {
100
+ throw new Error("Login realizado, mas access_token nao foi retornado pela API.")
101
+ }
102
+
103
+ return token
104
+ }
105
+
106
+ export async function ensureCliAuth() {
107
+ const stored = readStoredCredentials()
108
+ const rl = readline.createInterface({
109
+ input: process.stdin,
110
+ output: process.stdout
111
+ })
112
+
113
+ try {
114
+ let epaApiUrl = process.env.EPA_API_URL || process.env.epaApiUrl || stored.epaApiUrl
115
+ let epaApiToken = process.env.EPA_API_TOKEN || process.env.epaApiToken || stored.epaApiToken
116
+ const openaiApiKey = process.env.OPENAI_API_KEY || process.env.openaiApiKey || stored.openaiApiKey
117
+ const epaApiTimeoutMs = stored.epaApiTimeoutMs ?? 15000
118
+
119
+ if (!epaApiUrl) {
120
+ epaApiUrl = (await askQuestion(rl, "EPA API URL: ")).trim()
121
+ }
122
+
123
+ let tokenValid = false
124
+ if (epaApiUrl && epaApiToken) {
125
+ tokenValid = await isTokenValid(epaApiUrl, epaApiToken)
126
+ }
127
+
128
+ if (!epaApiToken || !tokenValid) {
129
+ console.log("Token EPA ausente ou invalido. Realizando login...")
130
+ const login = (await askQuestion(rl, "Login EPA: ")).trim()
131
+ rl.pause()
132
+ const senha = (await askHiddenQuestion("Senha EPA: ")).trim()
133
+ rl.resume()
134
+ epaApiToken = await loginToEpa(epaApiUrl, login, senha)
135
+ console.log("Login realizado com sucesso.")
136
+ }
137
+
138
+ let nextOpenAi = openaiApiKey
139
+ if (!nextOpenAi) {
140
+ nextOpenAi = (await askQuestion(rl, "OpenAI API KEY (CLI): ")).trim()
141
+ }
142
+
143
+ writeCredentials({
144
+ epaApiUrl,
145
+ epaApiToken,
146
+ openaiApiKey: nextOpenAi || undefined,
147
+ epaApiTimeoutMs
148
+ })
149
+ } finally {
150
+ rl.close()
151
+ }
152
+ }
@@ -0,0 +1,62 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+ import dotenv from "dotenv"
4
+
5
+ dotenv.config()
6
+
7
+ export interface AppConfig {
8
+ epaApiUrl: string
9
+ epaApiToken: string
10
+ openaiApiKey?: string
11
+ epaApiTimeoutMs?: number
12
+ }
13
+
14
+ function readConfigFile(): Partial<AppConfig> {
15
+ const configPath = path.join(process.cwd(), "config", "config.json")
16
+
17
+ if (!fs.existsSync(configPath)) {
18
+ return {}
19
+ }
20
+
21
+ const raw = fs.readFileSync(configPath, "utf-8")
22
+ return JSON.parse(raw) as Partial<AppConfig>
23
+ }
24
+
25
+ function getEnvValue(...keys: string[]): string | undefined {
26
+ for (const key of keys) {
27
+ const value = process.env[key]
28
+ if (value && value.trim()) {
29
+ return value.trim()
30
+ }
31
+ }
32
+
33
+ return undefined
34
+ }
35
+
36
+ export function loadConfig(): AppConfig {
37
+ const fileConfig = readConfigFile()
38
+
39
+ const epaApiUrl = getEnvValue("EPA_API_URL", "epaApiUrl") ?? fileConfig.epaApiUrl
40
+ const epaApiToken = getEnvValue("EPA_API_TOKEN", "epaApiToken") ?? fileConfig.epaApiToken
41
+ const openaiApiKey = getEnvValue("OPENAI_API_KEY", "openaiApiKey") ?? fileConfig.openaiApiKey
42
+
43
+ const timeoutRaw =
44
+ getEnvValue("EPA_API_TIMEOUT_MS", "epaApiTimeoutMs") ??
45
+ (fileConfig.epaApiTimeoutMs !== undefined ? String(fileConfig.epaApiTimeoutMs) : undefined)
46
+
47
+ const timeout = timeoutRaw ? Number(timeoutRaw) : undefined
48
+ const epaApiTimeoutMs = Number.isFinite(timeout) && Number(timeout) > 0 ? Number(timeout) : undefined
49
+
50
+ if (!epaApiUrl || !epaApiToken) {
51
+ throw new Error(
52
+ 'Configuracao incompleta. Defina "EPA_API_URL" e "EPA_API_TOKEN" nas variaveis de ambiente ou em config/config.json.'
53
+ )
54
+ }
55
+
56
+ return {
57
+ epaApiUrl,
58
+ epaApiToken,
59
+ openaiApiKey,
60
+ epaApiTimeoutMs
61
+ }
62
+ }