@creative-ia/cortex 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Registra uma tool call no changelog do DynamoDB.
3
+ * Fire-and-forget — erros são silenciados.
4
+ */
5
+ export declare function recordToolCall(toolName: string, args: Record<string, unknown>, resultPreview: string): Promise<void>;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Auto-Changelog — Registra automaticamente no DynamoDB changelog
3
+ * quando uma tool de escrita é executada com sucesso.
4
+ *
5
+ * Tools de leitura (get_*, search_*, list_*) são ignoradas.
6
+ * O registro é fire-and-forget (não bloqueia a resposta da tool).
7
+ */
8
+ import { putEntry } from "../knowledge/dynamo-store.js";
9
+ import { countByDomain } from "../knowledge/dynamo-store.js";
10
+ // Tools que geram registro automatico no changelog
11
+ const WRITE_TOOLS = new Set([
12
+ "save_knowledge",
13
+ "delete_knowledge",
14
+ "create_process",
15
+ "update_process",
16
+ "advance_process",
17
+ "decompose_epic",
18
+ "generate_wiki",
19
+ "generate_report",
20
+ "reverse_engineer",
21
+ "analyze_docs",
22
+ "init_process",
23
+ "validate_idea",
24
+ "analyze_code",
25
+ "log_event",
26
+ ]);
27
+ // Tools que NUNCA geram changelog (leitura, config, busca)
28
+ const READ_TOOLS = new Set([
29
+ "get_code_standards",
30
+ "get_architecture_guidance",
31
+ "get_knowledge",
32
+ "search_knowledge",
33
+ "list_knowledge_domains",
34
+ "get_config",
35
+ "search_logs",
36
+ "insights_query",
37
+ "semantic_search",
38
+ "validate_process",
39
+ ]);
40
+ // Evita changelog recursivo (save_knowledge salvando changelog sobre si mesmo)
41
+ let _recording = false;
42
+ /**
43
+ * Registra uma tool call no changelog do DynamoDB.
44
+ * Fire-and-forget — erros são silenciados.
45
+ */
46
+ export async function recordToolCall(toolName, args, resultPreview) {
47
+ // Ignora tools de leitura
48
+ if (READ_TOOLS.has(toolName))
49
+ return;
50
+ // Ignora tools não mapeadas como escrita
51
+ if (!WRITE_TOOLS.has(toolName))
52
+ return;
53
+ // Evita recursão (save_knowledge chamando recordToolCall que chama save_knowledge)
54
+ if (_recording)
55
+ return;
56
+ // Ignora se o próprio save_knowledge está salvando no domain "changelog"
57
+ if (toolName === "save_knowledge" && args.domain === "changelog")
58
+ return;
59
+ _recording = true;
60
+ try {
61
+ const nextId = await getNextTaskId();
62
+ const title = buildTitle(toolName, args);
63
+ const content = buildContent(toolName, args, resultPreview);
64
+ const tags = buildTags(toolName, args);
65
+ await putEntry({
66
+ domain: "changelog",
67
+ id: nextId,
68
+ title,
69
+ content,
70
+ tags,
71
+ created_at: new Date().toISOString(),
72
+ updated_at: new Date().toISOString(),
73
+ });
74
+ }
75
+ catch {
76
+ // Fire-and-forget — não quebra a tool original
77
+ }
78
+ finally {
79
+ _recording = false;
80
+ }
81
+ }
82
+ async function getNextTaskId() {
83
+ try {
84
+ const count = await countByDomain("changelog");
85
+ return `TASK-${String(count + 1).padStart(3, "0")}`;
86
+ }
87
+ catch {
88
+ // Fallback com timestamp
89
+ return `TASK-AUTO-${Date.now()}`;
90
+ }
91
+ }
92
+ function buildTitle(toolName, args) {
93
+ const toolLabel = toolName.replace(/_/g, " ");
94
+ switch (toolName) {
95
+ case "save_knowledge":
96
+ return `[auto] ${toolLabel}: [${args.domain}] ${args.id} — ${(args.title || "").slice(0, 80)}`;
97
+ case "delete_knowledge":
98
+ return `[auto] ${toolLabel}: [${args.domain}] ${args.id}`;
99
+ case "create_process":
100
+ return `[auto] ${toolLabel}: ${(args.prompt || "").slice(0, 80)}`;
101
+ case "update_process":
102
+ return `[auto] ${toolLabel}: processo ${args.processId}`;
103
+ case "advance_process":
104
+ return `[auto] ${toolLabel}: processo ${args.processId}${args.epicId ? ` epic=${args.epicId}` : ""}`;
105
+ case "decompose_epic":
106
+ return `[auto] ${toolLabel}: processo ${args.processId} epic=${args.epicId}`;
107
+ case "generate_wiki":
108
+ return `[auto] ${toolLabel}: processo ${args.processId}`;
109
+ case "generate_report":
110
+ return `[auto] ${toolLabel}: ${(args.title || "").slice(0, 80)}`;
111
+ case "reverse_engineer":
112
+ return `[auto] ${toolLabel}: ${(args.repoDir || "").split("/").pop()}`;
113
+ case "analyze_docs":
114
+ return `[auto] ${toolLabel}: ${(args.docsDir || "").split("/").pop()}`;
115
+ case "validate_idea":
116
+ return `[auto] ${toolLabel}: ${(args.idea || "").slice(0, 80)}`;
117
+ case "analyze_code":
118
+ return `[auto] ${toolLabel}: ${args.filename || "inline"}`;
119
+ default:
120
+ return `[auto] ${toolLabel}`;
121
+ }
122
+ }
123
+ function buildContent(toolName, args, resultPreview) {
124
+ const sanitizedArgs = { ...args };
125
+ delete sanitizedArgs.licenseKey; // nunca logar licenseKey
126
+ delete sanitizedArgs.code; // codigo pode ser grande
127
+ delete sanitizedArgs.content; // conteudo pode ser grande
128
+ const argsStr = JSON.stringify(sanitizedArgs, null, 2);
129
+ const preview = resultPreview.slice(0, 500);
130
+ return `## Auto-changelog (${new Date().toISOString()})\n\nTool: ${toolName}\nArgs: ${argsStr}\n\nResultado (preview): ${preview}`;
131
+ }
132
+ function buildTags(toolName, args) {
133
+ const tags = ["auto-changelog", toolName.replace(/_/g, "-")];
134
+ if (args.domain)
135
+ tags.push(args.domain);
136
+ if (args.processId)
137
+ tags.push(`process-${args.processId}`);
138
+ if (args.epicId)
139
+ tags.push(`epic-${args.epicId}`);
140
+ return tags;
141
+ }
@@ -1,30 +1,45 @@
1
1
  /**
2
2
  * CloudWatch Logs — Escrita e consulta de logs do MCP Server.
3
3
  * Log group: /creative-ia50/mcp-server
4
+ *
5
+ * Lazy import — AWS SDK so e carregado quando necessario.
4
6
  */
5
- import { CloudWatchLogsClient, CreateLogStreamCommand, PutLogEventsCommand, FilterLogEventsCommand, StartQueryCommand, GetQueryResultsCommand, } from "@aws-sdk/client-cloudwatch-logs";
6
7
  const isLocal = process.env.USE_LOCALSTACK === "true";
7
- const cwl = new CloudWatchLogsClient({
8
- region: process.env.AWS_REGION || "sa-east-1",
9
- ...(isLocal && {
10
- endpoint: "http://localhost:4566",
11
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
12
- }),
13
- });
8
+ const CLOUD_MODE = !!process.env.CORTEX_API_URL;
14
9
  const LOG_GROUP = process.env.LOG_GROUP || "/creative-ia50/mcp-server";
10
+ let _cwl = null;
11
+ let _cmds = {};
12
+ async function getCwl() {
13
+ if (_cwl)
14
+ return _cwl;
15
+ const mod = await import("@aws-sdk/client-cloudwatch-logs");
16
+ _cmds = mod;
17
+ _cwl = new mod.CloudWatchLogsClient({
18
+ region: process.env.AWS_REGION || "sa-east-1",
19
+ ...(isLocal && {
20
+ endpoint: "http://localhost:4566",
21
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
22
+ }),
23
+ });
24
+ return _cwl;
25
+ }
15
26
  // Ensure log stream exists for today
16
27
  async function ensureStream(streamName) {
17
28
  try {
18
- await cwl.send(new CreateLogStreamCommand({ logGroupName: LOG_GROUP, logStreamName: streamName }));
29
+ const cwl = await getCwl();
30
+ await cwl.send(new _cmds.CreateLogStreamCommand({ logGroupName: LOG_GROUP, logStreamName: streamName }));
19
31
  }
20
32
  catch { /* already exists */ }
21
33
  }
22
34
  // Write a log event
23
35
  export async function writeLog(level, message, meta) {
36
+ if (CLOUD_MODE)
37
+ return; // cloud mode — logs go via cloud proxy
24
38
  const stream = `mcp-server/${new Date().toISOString().slice(0, 10)}`;
25
39
  await ensureStream(stream);
26
40
  const event = JSON.stringify({ level, message, ...meta, timestamp: new Date().toISOString() });
27
- await cwl.send(new PutLogEventsCommand({
41
+ const cwl = await getCwl();
42
+ await cwl.send(new _cmds.PutLogEventsCommand({
28
43
  logGroupName: LOG_GROUP,
29
44
  logStreamName: stream,
30
45
  logEvents: [{ timestamp: Date.now(), message: event }],
@@ -32,20 +47,26 @@ export async function writeLog(level, message, meta) {
32
47
  }
33
48
  // Filter logs by pattern
34
49
  export async function filterLogs(params) {
50
+ if (CLOUD_MODE)
51
+ return [];
35
52
  const now = Date.now();
36
- const res = await cwl.send(new FilterLogEventsCommand({
53
+ const cwl = await getCwl();
54
+ const res = await cwl.send(new _cmds.FilterLogEventsCommand({
37
55
  logGroupName: LOG_GROUP,
38
56
  filterPattern: params.pattern || "",
39
- startTime: params.startTime || now - 24 * 60 * 60 * 1000, // default: last 24h
57
+ startTime: params.startTime || now - 24 * 60 * 60 * 1000,
40
58
  endTime: params.endTime || now,
41
59
  limit: params.limit || 50,
42
60
  }));
43
- return (res.events || []).map(e => e.message || "");
61
+ return (res.events || []).map((e) => e.message || "");
44
62
  }
45
63
  // Run CloudWatch Insights query
46
64
  export async function queryLogs(params) {
65
+ if (CLOUD_MODE)
66
+ return [];
47
67
  const now = Date.now();
48
- const start = await cwl.send(new StartQueryCommand({
68
+ const cwl = await getCwl();
69
+ const start = await cwl.send(new _cmds.StartQueryCommand({
49
70
  logGroupName: LOG_GROUP,
50
71
  queryString: params.query,
51
72
  startTime: Math.floor((params.startTime || now - 24 * 60 * 60 * 1000) / 1000),
@@ -54,12 +75,11 @@ export async function queryLogs(params) {
54
75
  }));
55
76
  if (!start.queryId)
56
77
  return [];
57
- // Poll for results (max 10s)
58
78
  for (let i = 0; i < 20; i++) {
59
79
  await new Promise(r => setTimeout(r, 500));
60
- const res = await cwl.send(new GetQueryResultsCommand({ queryId: start.queryId }));
80
+ const res = await cwl.send(new _cmds.GetQueryResultsCommand({ queryId: start.queryId }));
61
81
  if (res.status === "Complete" || res.status === "Failed" || res.status === "Cancelled") {
62
- return (res.results || []).map(row => row.map(f => `${f.field}: ${f.value}`));
82
+ return (res.results || []).map((row) => row.map((f) => `${f.field}: ${f.value}`));
63
83
  }
64
84
  }
65
85
  return [];
@@ -1,3 +1,15 @@
1
+ /**
2
+ * License Validator — Valida licença do cliente.
3
+ *
4
+ * Dois modos:
5
+ * 1. DynamoDB direto (Lambda / dev com AWS_PROFILE) — quando CORTEX_API_URL nao esta definido
6
+ * 2. HTTP remoto (cliente local via npx) — quando CORTEX_API_URL esta definido
7
+ * Chama o endpoint nuvem para validar, com cache em memoria (TTL 5min)
8
+ *
9
+ * RBAC:
10
+ * - role "admin" → acesso total a todos os domains e tools
11
+ * - role "user" → acesso restrito aos domains em allowedDomains
12
+ */
1
13
  export interface License {
2
14
  licenseKey: string;
3
15
  clientName: string;
@@ -10,19 +10,30 @@
10
10
  * - role "admin" → acesso total a todos os domains e tools
11
11
  * - role "user" → acesso restrito aos domains em allowedDomains
12
12
  */
13
- import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
14
- import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
13
+ // Dynamic import AWS SDK so e carregado quando necessario (modo DynamoDB direto).
14
+ // Isso evita o erro "Could not load credentials from any providers" em maquinas
15
+ // que usam CORTEX_API_URL (cloud proxy) e nao tem credenciais AWS configuradas.
15
16
  const isLocal = process.env.USE_LOCALSTACK === "true";
16
- const CORTEX_API_URL = process.env.CORTEX_API_URL; // ex: https://zsul3dtu25.execute-api.sa-east-1.amazonaws.com/prod/mcp
17
- const ddb = CORTEX_API_URL
18
- ? null // nao precisa de DynamoDB quando valida via HTTP
19
- : DynamoDBDocumentClient.from(new DynamoDBClient({
17
+ const CORTEX_API_URL = process.env.CORTEX_API_URL; // ex: https://mcp.creativeia50.com/mcp
18
+ let _ddb = null;
19
+ let _GetCommand = null;
20
+ async function getDdb() {
21
+ if (CORTEX_API_URL)
22
+ return null; // cloud mode — nao precisa de DynamoDB
23
+ if (_ddb)
24
+ return _ddb;
25
+ const { DynamoDBClient } = await import("@aws-sdk/client-dynamodb");
26
+ const { DynamoDBDocumentClient, GetCommand } = await import("@aws-sdk/lib-dynamodb");
27
+ _GetCommand = GetCommand;
28
+ _ddb = DynamoDBDocumentClient.from(new DynamoDBClient({
20
29
  region: process.env.AWS_REGION || "sa-east-1",
21
30
  ...(isLocal && {
22
31
  endpoint: "http://localhost:4566",
23
32
  credentials: { accessKeyId: "test", secretAccessKey: "test" },
24
33
  }),
25
34
  }));
35
+ return _ddb;
36
+ }
26
37
  const TABLE = process.env.LICENSE_TABLE || "creative-ia50-licenses";
27
38
  const licenseCache = new Map();
28
39
  const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutos
@@ -102,7 +113,10 @@ async function validateLicenseRemote(licenseKey, _toolName) {
102
113
  */
103
114
  async function validateLicenseDynamo(licenseKey, toolName) {
104
115
  try {
105
- const res = await ddb.send(new GetCommand({ TableName: TABLE, Key: { licenseKey } }));
116
+ const ddb = await getDdb();
117
+ if (!ddb)
118
+ return { valid: false, reason: "DynamoDB not available (cloud mode?)" };
119
+ const res = await ddb.send(new _GetCommand({ TableName: TABLE, Key: { licenseKey } }));
106
120
  if (!res.Item) {
107
121
  return { valid: false, reason: "License not found" };
108
122
  }
@@ -2,19 +2,35 @@
2
2
  * SSM Parameter Store — Registry de configuração do MCP Server.
3
3
  * Lê parâmetros de /creative-ia50/mcp-server/*
4
4
  */
5
- import { SSMClient, GetParameterCommand, GetParametersByPathCommand } from "@aws-sdk/client-ssm";
5
+ // Lazy import AWS SDK so e carregado quando necessario (evita erro de credenciais em cloud mode)
6
6
  const isLocal = process.env.USE_LOCALSTACK === "true";
7
- const ssm = new SSMClient({
8
- region: process.env.AWS_REGION || "sa-east-1",
9
- ...(isLocal && {
10
- endpoint: "http://localhost:4566",
11
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
12
- }),
13
- });
7
+ const CLOUD_MODE = !!process.env.CORTEX_API_URL;
8
+ let _ssm = null;
9
+ let _GetParameterCommand = null;
10
+ let _GetParametersByPathCommand = null;
11
+ async function getSsm() {
12
+ if (_ssm)
13
+ return _ssm;
14
+ const { SSMClient } = await import("@aws-sdk/client-ssm");
15
+ const mod = await import("@aws-sdk/client-ssm");
16
+ _GetParameterCommand = mod.GetParameterCommand;
17
+ _GetParametersByPathCommand = mod.GetParametersByPathCommand;
18
+ _ssm = new SSMClient({
19
+ region: process.env.AWS_REGION || "sa-east-1",
20
+ ...(isLocal && {
21
+ endpoint: "http://localhost:4566",
22
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
23
+ }),
24
+ });
25
+ return _ssm;
26
+ }
14
27
  const PREFIX = "/creative-ia50/mcp-server";
15
28
  export async function getParam(name) {
29
+ if (CLOUD_MODE)
30
+ return null;
16
31
  try {
17
- const res = await ssm.send(new GetParameterCommand({ Name: `${PREFIX}/${name}` }));
32
+ const ssm = await getSsm();
33
+ const res = await ssm.send(new _GetParameterCommand({ Name: `${PREFIX}/${name}` }));
18
34
  return res.Parameter?.Value || null;
19
35
  }
20
36
  catch {
@@ -22,9 +38,12 @@ export async function getParam(name) {
22
38
  }
23
39
  }
24
40
  export async function getAllParams() {
41
+ if (CLOUD_MODE)
42
+ return {};
25
43
  const result = {};
26
44
  try {
27
- const res = await ssm.send(new GetParametersByPathCommand({
45
+ const ssm = await getSsm();
46
+ const res = await ssm.send(new _GetParametersByPathCommand({
28
47
  Path: PREFIX,
29
48
  Recursive: true,
30
49
  }));
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ import { z } from "zod";
15
15
  import { validateLicense, canAccessDomain } from "./config/license.js";
16
16
  import { isCloudProxyEnabled, callCloudTool } from "./config/cloud-proxy.js";
17
17
  import { withTelemetry, reportTelemetry } from "./config/telemetry.js";
18
+ import { recordToolCall } from "./config/auto-changelog.js";
18
19
  import { getCodeStandards } from "./tools/get-code-standards.js";
19
20
  import { analyzeCode } from "./tools/analyze-code.js";
20
21
  import { getArchitectureGuidance } from "./tools/get-architecture.js";
@@ -51,6 +52,15 @@ async function licenseGate(licenseKey, toolName) {
51
52
  function cloudResult(text) {
52
53
  return { content: [{ type: "text", text }] };
53
54
  }
55
+ // --- Helper: auto-changelog fire-and-forget ---
56
+ // Registra tool calls de escrita no changelog do DynamoDB.
57
+ // Nao bloqueia a resposta — roda em background.
58
+ function autoLog(toolName, args, resultText) {
59
+ if (!CLOUD_MODE) {
60
+ // Apenas em modo direto (Lambda). Em cloud mode o server remoto ja registra.
61
+ recordToolCall(toolName, args, resultText).catch(() => { });
62
+ }
63
+ }
54
64
  async function proxyToCloud(toolName, args) {
55
65
  const start = Date.now();
56
66
  try {
@@ -102,6 +112,7 @@ server.tool("analyze_code", "Analyzes code against L1-L4 standards. Returns viol
102
112
  if ("denied" in gate)
103
113
  return { content: [{ type: "text", text: gate.denied }] };
104
114
  const result = await analyzeCode({ code, language, filename });
115
+ autoLog("analyze_code", { language, filename }, result);
105
116
  return { content: [{ type: "text", text: result }] };
106
117
  });
107
118
  // --- Tool: get_architecture_guidance ---
@@ -131,6 +142,7 @@ server.tool("init_process", "Initializes a new process with UUID, checklist, and
131
142
  if ("denied" in gate)
132
143
  return { content: [{ type: "text", text: gate.denied }] };
133
144
  const result = await initProcess({ title, stakeholder, description });
145
+ autoLog("init_process", { title, stakeholder }, result);
134
146
  return { content: [{ type: "text", text: result }] };
135
147
  });
136
148
  // --- Tool: validate_process ---
@@ -159,6 +171,7 @@ server.tool("validate_idea", "Analyzes a business idea against the company knowl
159
171
  if ("denied" in gate)
160
172
  return { content: [{ type: "text", text: gate.denied }] };
161
173
  const result = await validateIdea({ idea, context });
174
+ autoLog("validate_idea", { idea: idea.slice(0, 200) }, result);
162
175
  return { content: [{ type: "text", text: result }] };
163
176
  });
164
177
  // --- Tool: generate_report ---
@@ -173,6 +186,7 @@ server.tool("generate_report", "Generates a branded HTML report on the client ma
173
186
  if ("denied" in gate)
174
187
  return { content: [{ type: "text", text: gate.denied }] };
175
188
  const result = await withTelemetry("generate_report", () => generateReport({ title, content, reportType, outputDir }), (r) => `report=${reportType} title=${title.slice(0, 50)}`, { reportType });
189
+ autoLog("generate_report", { title, reportType }, result);
176
190
  return { content: [{ type: "text", text: result }] };
177
191
  });
178
192
  // =============================================================================
@@ -196,6 +210,7 @@ server.tool("save_knowledge", "Salva ou atualiza um registro de conhecimento org
196
210
  if (!canAccessDomain(gate.license, domain))
197
211
  return { content: [{ type: "text", text: `Access denied: domain '${domain}' not allowed for this license` }] };
198
212
  const result = await saveKnowledge({ domain, id, title, content, tags, metadata });
213
+ autoLog("save_knowledge", { domain, id, title, tags }, result);
199
214
  return { content: [{ type: "text", text: result }] };
200
215
  });
201
216
  // --- Tool: get_knowledge ---
@@ -247,6 +262,7 @@ server.tool("delete_knowledge", "Remove um registro de conhecimento organizacion
247
262
  if (!canAccessDomain(gate.license, domain))
248
263
  return { content: [{ type: "text", text: `Access denied: domain '${domain}' not allowed for this license` }] };
249
264
  const result = await deleteKnowledgeEntry({ domain, id });
265
+ autoLog("delete_knowledge", { domain, id }, result);
250
266
  return { content: [{ type: "text", text: result }] };
251
267
  });
252
268
  // --- Tool: list_knowledge_domains ---
@@ -290,6 +306,7 @@ server.tool("log_event", "Registra um evento de log no CloudWatch Logs do MCP Se
290
306
  if ("denied" in gate)
291
307
  return { content: [{ type: "text", text: gate.denied }] };
292
308
  const result = await logEvent({ level, message, meta });
309
+ autoLog("log_event", { level, message: message.slice(0, 200) }, result);
293
310
  return { content: [{ type: "text", text: result }] };
294
311
  });
295
312
  // --- Tool: search_logs ---
@@ -355,6 +372,7 @@ server.tool("create_process", "Cria esqueleto de processo: recebe frase em lingu
355
372
  if ("denied" in gate)
356
373
  return { content: [{ type: "text", text: gate.denied }] };
357
374
  const result = await withTelemetry("create_process", () => createProcess({ prompt, stakeholder }), (r) => `prompt=${prompt.slice(0, 80)}`, { stakeholder });
375
+ autoLog("create_process", { prompt: prompt.slice(0, 200), stakeholder }, result);
358
376
  return { content: [{ type: "text", text: result }] };
359
377
  });
360
378
  // --- Tool: update_process ---
@@ -380,6 +398,7 @@ server.tool("update_process", "Acrescenta informações a um processo existente
380
398
  if ("denied" in gate)
381
399
  return { content: [{ type: "text", text: gate.denied }] };
382
400
  const result = await updateProcess({ processId, updates, listProcesses });
401
+ autoLog("update_process", { processId, listProcesses }, result);
383
402
  return { content: [{ type: "text", text: result }] };
384
403
  });
385
404
  // --- Tool: decompose_epic ---
@@ -393,6 +412,7 @@ server.tool("decompose_epic", "Decompõe um épico (EP-XX) em artefatos completo
393
412
  if ("denied" in gate)
394
413
  return { content: [{ type: "text", text: gate.denied }] };
395
414
  const result = await withTelemetry("decompose_epic", () => decomposeEpic({ processId, epicId, processDir }), (r) => `process=${processId} epic=${epicId}`, { processId, epicId });
415
+ autoLog("decompose_epic", { processId, epicId }, result);
396
416
  return { content: [{ type: "text", text: result }] };
397
417
  });
398
418
  // --- Tool: advance_process ---
@@ -406,6 +426,7 @@ server.tool("advance_process", "Orquestrador inteligente: detecta a fase atual d
406
426
  if ("denied" in gate)
407
427
  return { content: [{ type: "text", text: gate.denied }] };
408
428
  const result = await withTelemetry("advance_process", () => advanceProcess({ processId, processDir, epicId }), (r) => `process=${processId}${epicId ? ` epic=${epicId}` : ""}`, { processId, epicId });
429
+ autoLog("advance_process", { processId, epicId }, result);
409
430
  return { content: [{ type: "text", text: result }] };
410
431
  });
411
432
  // --- Tool: generate_wiki ---
@@ -418,6 +439,7 @@ server.tool("generate_wiki", "Gera/atualiza wiki navegável do processo: index.h
418
439
  if ("denied" in gate)
419
440
  return { content: [{ type: "text", text: gate.denied }] };
420
441
  const result = await withTelemetry("generate_wiki", () => generateWiki({ processId, processDir }), (r) => `process=${processId}`, { processId });
442
+ autoLog("generate_wiki", { processId }, JSON.stringify(result));
421
443
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
422
444
  });
423
445
  // --- Tool: reverse_engineer ---
@@ -432,6 +454,7 @@ server.tool("reverse_engineer", "Analisa repositório local ou remoto (Git URL)
432
454
  if ("denied" in gate)
433
455
  return { content: [{ type: "text", text: gate.denied }] };
434
456
  const result = await withTelemetry("reverse_engineer", () => reverseEngineer({ repoDir, processId, processDir, outputFilename }), (r) => `repo=${repoDir.split("/").pop() || repoDir}`, { repoDir, processId });
457
+ autoLog("reverse_engineer", { repoDir: repoDir.split("/").pop() || repoDir, processId }, JSON.stringify(result));
435
458
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
436
459
  });
437
460
  // --- Tool: analyze_docs ---
@@ -446,6 +469,7 @@ server.tool("analyze_docs", "Analisa um diretório de documentação, extrai tex
446
469
  if ("denied" in gate)
447
470
  return { content: [{ type: "text", text: gate.denied }] };
448
471
  const result = await withTelemetry("analyze_docs", () => analyzeDocs({ docsDir, processId, processDir, maxFiles }), (r) => `dir=${docsDir.split("/").pop() || docsDir}`, { docsDir, processId });
472
+ autoLog("analyze_docs", { docsDir: docsDir.split("/").pop() || docsDir, processId }, result);
449
473
  return { content: [{ type: "text", text: result }] };
450
474
  });
451
475
  // --- Start server ---
@@ -5,38 +5,42 @@
5
5
  * PK: domain (ex: "decisions", "changelog", "troubleshooting", "glossary")
6
6
  * SK: id (ex: "ADR-001", "TASK-030", "TS-005", "TERM-001")
7
7
  *
8
- * Suporta LocalStack e AWS real via USE_LOCALSTACK env var.
8
+ * Lazy import AWS SDK so e carregado quando necessario.
9
9
  */
10
- import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
11
- import { DynamoDBDocumentClient, PutCommand, GetCommand, QueryCommand, DeleteCommand, } from "@aws-sdk/lib-dynamodb";
12
10
  const isLocal = process.env.USE_LOCALSTACK === "true";
13
- const ddb = DynamoDBDocumentClient.from(new DynamoDBClient({
14
- region: process.env.AWS_REGION || "sa-east-1",
15
- ...(isLocal && {
16
- endpoint: "http://localhost:4566",
17
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
18
- }),
19
- }));
20
11
  const TABLE = process.env.KNOWLEDGE_TABLE || "creative-ia50-knowledge";
21
- // --- PUT (create or update) ---
12
+ let _ddb = null;
13
+ let _cmds = {};
14
+ async function getDdb() {
15
+ if (_ddb)
16
+ return _ddb;
17
+ const { DynamoDBClient } = await import("@aws-sdk/client-dynamodb");
18
+ const mod = await import("@aws-sdk/lib-dynamodb");
19
+ _cmds = mod;
20
+ _ddb = mod.DynamoDBDocumentClient.from(new DynamoDBClient({
21
+ region: process.env.AWS_REGION || "sa-east-1",
22
+ ...(isLocal && {
23
+ endpoint: "http://localhost:4566",
24
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
25
+ }),
26
+ }));
27
+ return _ddb;
28
+ }
22
29
  export async function putEntry(entry) {
23
- await ddb.send(new PutCommand({
30
+ const ddb = await getDdb();
31
+ await ddb.send(new _cmds.PutCommand({
24
32
  TableName: TABLE,
25
- Item: {
26
- ...entry,
27
- primary_tag: entry.tags?.[0] || "untagged",
28
- updated_at: new Date().toISOString(),
29
- },
33
+ Item: { ...entry, primary_tag: entry.tags?.[0] || "untagged", updated_at: new Date().toISOString() },
30
34
  }));
31
35
  }
32
- // --- GET single entry ---
33
36
  export async function getEntry(domain, id) {
34
- const res = await ddb.send(new GetCommand({ TableName: TABLE, Key: { domain, id } }));
37
+ const ddb = await getDdb();
38
+ const res = await ddb.send(new _cmds.GetCommand({ TableName: TABLE, Key: { domain, id } }));
35
39
  return res.Item || null;
36
40
  }
37
- // --- QUERY by domain (list all entries in a domain) ---
38
41
  export async function queryByDomain(domain, limit) {
39
- const res = await ddb.send(new QueryCommand({
42
+ const ddb = await getDdb();
43
+ const res = await ddb.send(new _cmds.QueryCommand({
40
44
  TableName: TABLE,
41
45
  KeyConditionExpression: "#d = :domain",
42
46
  ExpressionAttributeNames: { "#d": "domain" },
@@ -46,11 +50,9 @@ export async function queryByDomain(domain, limit) {
46
50
  }));
47
51
  return res.Items || [];
48
52
  }
49
- // --- SEARCH by tag (uses GSI tag-index) ---
50
53
  export async function searchByTag(tag, limit) {
51
- // Scan with filter — for small datasets this is fine.
52
- // For large scale, use a GSI on tags (flattened).
53
- const res = await ddb.send(new QueryCommand({
54
+ const ddb = await getDdb();
55
+ const res = await ddb.send(new _cmds.QueryCommand({
54
56
  TableName: TABLE,
55
57
  IndexName: "tag-index",
56
58
  KeyConditionExpression: "primary_tag = :tag",
@@ -59,22 +61,19 @@ export async function searchByTag(tag, limit) {
59
61
  }));
60
62
  return res.Items || [];
61
63
  }
62
- // --- SEARCH by text (simple contains on title + content) ---
63
64
  export async function searchByText(domain, text, limit) {
64
65
  const all = await queryByDomain(domain, 500);
65
66
  const lower = text.toLowerCase();
66
- const filtered = all.filter((e) => e.title.toLowerCase().includes(lower) ||
67
- e.content.toLowerCase().includes(lower) ||
68
- (e.tags && e.tags.some((t) => t.toLowerCase().includes(lower))));
67
+ const filtered = all.filter((e) => e.title.toLowerCase().includes(lower) || e.content.toLowerCase().includes(lower) || (e.tags && e.tags.some((t) => t.toLowerCase().includes(lower))));
69
68
  return limit ? filtered.slice(0, limit) : filtered;
70
69
  }
71
- // --- DELETE ---
72
70
  export async function deleteEntry(domain, id) {
73
- await ddb.send(new DeleteCommand({ TableName: TABLE, Key: { domain, id } }));
71
+ const ddb = await getDdb();
72
+ await ddb.send(new _cmds.DeleteCommand({ TableName: TABLE, Key: { domain, id } }));
74
73
  }
75
- // --- COUNT entries in a domain ---
76
74
  export async function countByDomain(domain) {
77
- const res = await ddb.send(new QueryCommand({
75
+ const ddb = await getDdb();
76
+ const res = await ddb.send(new _cmds.QueryCommand({
78
77
  TableName: TABLE,
79
78
  KeyConditionExpression: "#d = :domain",
80
79
  ExpressionAttributeNames: { "#d": "domain" },
@@ -1,22 +1,32 @@
1
1
  /**
2
2
  * Bedrock Titan Embeddings — Gera embeddings para semantic search.
3
3
  * Model: amazon.titan-embed-text-v2:0 (256 dimensions)
4
- * Custo: ~$0.0002 per 1K input tokens
4
+ *
5
+ * Lazy import — AWS SDK so e carregado quando necessario.
5
6
  */
6
- import { BedrockRuntimeClient, InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime";
7
7
  const isLocal = process.env.USE_LOCALSTACK === "true";
8
- const bedrock = new BedrockRuntimeClient({
9
- region: process.env.AWS_REGION || "sa-east-1",
10
- ...(isLocal && {
11
- endpoint: "http://localhost:4566",
12
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
13
- }),
14
- });
15
8
  const MODEL_ID = "amazon.titan-embed-text-v2:0";
16
9
  const DIMENSIONS = 256;
10
+ let _bedrock = null;
11
+ let _InvokeModelCommand = null;
12
+ async function getBedrock() {
13
+ if (_bedrock)
14
+ return _bedrock;
15
+ const mod = await import("@aws-sdk/client-bedrock-runtime");
16
+ _InvokeModelCommand = mod.InvokeModelCommand;
17
+ _bedrock = new mod.BedrockRuntimeClient({
18
+ region: process.env.AWS_REGION || "sa-east-1",
19
+ ...(isLocal && {
20
+ endpoint: "http://localhost:4566",
21
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
22
+ }),
23
+ });
24
+ return _bedrock;
25
+ }
17
26
  export async function getEmbedding(text) {
27
+ const bedrock = await getBedrock();
18
28
  const body = JSON.stringify({ inputText: text.slice(0, 8000), dimensions: DIMENSIONS });
19
- const res = await bedrock.send(new InvokeModelCommand({
29
+ const res = await bedrock.send(new _InvokeModelCommand({
20
30
  modelId: MODEL_ID,
21
31
  body: new TextEncoder().encode(body),
22
32
  contentType: "application/json",
@@ -2,29 +2,40 @@
2
2
  * Knowledge Loader — Carrega skills e knowledge do S3 (LocalStack ou AWS).
3
3
  * O conteúdo NUNCA é exposto diretamente ao cliente.
4
4
  * As tools processam o conteúdo e retornam apenas respostas derivadas.
5
+ *
6
+ * Lazy import — AWS SDK so e carregado quando necessario.
5
7
  */
6
- import { S3Client, GetObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
7
8
  const isLocal = process.env.USE_LOCALSTACK === "true";
8
- const s3 = new S3Client({
9
- region: process.env.AWS_REGION || "us-east-1",
10
- ...(isLocal && {
11
- endpoint: "http://localhost:4566",
12
- forcePathStyle: true,
13
- credentials: { accessKeyId: "test", secretAccessKey: "test" },
14
- }),
15
- });
16
9
  const BUCKET = process.env.SKILLS_BUCKET || "creative-ia50-skills";
10
+ let _s3 = null;
11
+ let _cmds = {};
12
+ async function getS3() {
13
+ if (_s3)
14
+ return _s3;
15
+ const mod = await import("@aws-sdk/client-s3");
16
+ _cmds = mod;
17
+ _s3 = new mod.S3Client({
18
+ region: process.env.AWS_REGION || "us-east-1",
19
+ ...(isLocal && {
20
+ endpoint: "http://localhost:4566",
21
+ forcePathStyle: true,
22
+ credentials: { accessKeyId: "test", secretAccessKey: "test" },
23
+ }),
24
+ });
25
+ return _s3;
26
+ }
17
27
  async function getObject(key) {
18
- const cmd = new GetObjectCommand({ Bucket: BUCKET, Key: key });
28
+ const s3 = await getS3();
29
+ const cmd = new _cmds.GetObjectCommand({ Bucket: BUCKET, Key: key });
19
30
  const res = await s3.send(cmd);
20
31
  return (await res.Body?.transformToString()) || "";
21
32
  }
22
33
  async function listObjects(prefix) {
23
- const cmd = new ListObjectsV2Command({ Bucket: BUCKET, Prefix: prefix });
34
+ const s3 = await getS3();
35
+ const cmd = new _cmds.ListObjectsV2Command({ Bucket: BUCKET, Prefix: prefix });
24
36
  const res = await s3.send(cmd);
25
37
  return (res.Contents || []).map((o) => o.Key).filter(Boolean);
26
38
  }
27
- // --- Public API ---
28
39
  export async function loadCodeStandards() {
29
40
  const raw = await getObject("knowledge/code-standards.json");
30
41
  return JSON.parse(raw);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@creative-ia/cortex",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Cortex by Creative IA — Centro de inteligencia para arquitetura de software. MCP Server com 24 tools.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
Binary file