efarmz-slackbot-data 1.0.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 (131) hide show
  1. package/.clever.json +12 -0
  2. package/.dockerignore +13 -0
  3. package/.env.example +28 -0
  4. package/.github/workflows/deploy-production.yaml +34 -0
  5. package/.prettierrc +6 -0
  6. package/.tasks/F1-bootstrap.md +110 -0
  7. package/.tasks/F2-domain-layer.md +173 -0
  8. package/.tasks/F3-application-layer.md +166 -0
  9. package/.tasks/F4-infrastructure-layer.md +229 -0
  10. package/.tasks/F5-config-main.md +160 -0
  11. package/.tasks/F6-schemas-deployment.md +129 -0
  12. package/CLAUDE.md +163 -0
  13. package/Dockerfile +15 -0
  14. package/PRD.md +119 -0
  15. package/docs/schemas/.gitkeep +0 -0
  16. package/docs/schemas/_guidelines.md +89 -0
  17. package/docs/schemas/efarmz_db.md +759 -0
  18. package/docs/schemas/example.md +16 -0
  19. package/eslint.config.mjs +18 -0
  20. package/package.json +54 -0
  21. package/releaserc.json +15 -0
  22. package/src/.gitkeep +0 -0
  23. package/src/application/agent/.gitkeep +0 -0
  24. package/src/application/agent/AgentContext.test.ts +263 -0
  25. package/src/application/agent/AgentContext.ts +93 -0
  26. package/src/application/agent/AgentLoop.test.ts +275 -0
  27. package/src/application/agent/AgentLoop.ts +101 -0
  28. package/src/application/agent/AgentRunResult.ts +11 -0
  29. package/src/application/agent/LLMMessage.ts +16 -0
  30. package/src/application/agent/tools/RunSqlTool.ts +23 -0
  31. package/src/application/formatting/.gitkeep +0 -0
  32. package/src/application/formatting/CsvRenderer.test.ts +162 -0
  33. package/src/application/formatting/CsvRenderer.ts +34 -0
  34. package/src/application/formatting/MonospaceTableRenderer.test.ts +129 -0
  35. package/src/application/formatting/MonospaceTableRenderer.ts +58 -0
  36. package/src/application/formatting/RenderedResponse.ts +7 -0
  37. package/src/application/formatting/ResponseRenderer.test.ts +159 -0
  38. package/src/application/formatting/ResponseRenderer.ts +39 -0
  39. package/src/application/formatting/ScalarRenderer.test.ts +36 -0
  40. package/src/application/formatting/ScalarRenderer.ts +12 -0
  41. package/src/application/usecases/.gitkeep +0 -0
  42. package/src/application/usecases/AnswerQuestion.test.ts +362 -0
  43. package/src/application/usecases/AnswerQuestion.ts +69 -0
  44. package/src/application/usecases/ParseQuestion.test.ts +39 -0
  45. package/src/application/usecases/ParseQuestion.ts +9 -0
  46. package/src/config/.gitkeep +0 -0
  47. package/src/config/Container.test.ts +35 -0
  48. package/src/config/Container.ts +74 -0
  49. package/src/config/constants.ts +9 -0
  50. package/src/config/env.test.ts +103 -0
  51. package/src/config/env.ts +41 -0
  52. package/src/domain/entities/.gitkeep +0 -0
  53. package/src/domain/entities/Conversation.test.ts +69 -0
  54. package/src/domain/entities/Conversation.ts +26 -0
  55. package/src/domain/entities/ConversationMessage.test.ts +49 -0
  56. package/src/domain/entities/ConversationMessage.ts +18 -0
  57. package/src/domain/entities/index.ts +2 -0
  58. package/src/domain/errors/.gitkeep +0 -0
  59. package/src/domain/errors/AgentLoopExceededError.ts +12 -0
  60. package/src/domain/errors/DomainError.test.ts +106 -0
  61. package/src/domain/errors/DomainError.ts +11 -0
  62. package/src/domain/errors/InvalidSqlError.ts +15 -0
  63. package/src/domain/errors/LLMError.ts +15 -0
  64. package/src/domain/errors/SchemaLoadError.ts +15 -0
  65. package/src/domain/errors/SqlExecutionError.ts +15 -0
  66. package/src/domain/errors/index.ts +15 -0
  67. package/src/domain/ports/.gitkeep +0 -0
  68. package/src/domain/ports/AdminLogger.ts +16 -0
  69. package/src/domain/ports/ConversationRepository.ts +10 -0
  70. package/src/domain/ports/LLMProvider.ts +33 -0
  71. package/src/domain/ports/Logger.ts +8 -0
  72. package/src/domain/ports/SchemaCatalog.ts +5 -0
  73. package/src/domain/ports/SlackMessenger.ts +8 -0
  74. package/src/domain/ports/SqlExecutor.ts +8 -0
  75. package/src/domain/ports/SqlValidator.ts +5 -0
  76. package/src/domain/ports/index.ts +17 -0
  77. package/src/domain/value-objects/.gitkeep +0 -0
  78. package/src/domain/value-objects/LLMProviderName.ts +6 -0
  79. package/src/domain/value-objects/QueryResult.test.ts +51 -0
  80. package/src/domain/value-objects/QueryResult.ts +18 -0
  81. package/src/domain/value-objects/Question.test.ts +59 -0
  82. package/src/domain/value-objects/Question.ts +22 -0
  83. package/src/domain/value-objects/QuestionFlags.test.ts +59 -0
  84. package/src/domain/value-objects/QuestionFlags.ts +18 -0
  85. package/src/domain/value-objects/ResponseRendering.ts +7 -0
  86. package/src/domain/value-objects/SqlQuery.test.ts +40 -0
  87. package/src/domain/value-objects/SqlQuery.ts +12 -0
  88. package/src/domain/value-objects/ThreadId.test.ts +68 -0
  89. package/src/domain/value-objects/ThreadId.ts +27 -0
  90. package/src/domain/value-objects/index.ts +13 -0
  91. package/src/infrastructure/llm/.gitkeep +0 -0
  92. package/src/infrastructure/llm/AnthropicLLMProvider.test.ts +229 -0
  93. package/src/infrastructure/llm/AnthropicLLMProvider.ts +45 -0
  94. package/src/infrastructure/llm/index.ts +4 -0
  95. package/src/infrastructure/llm/mappers/AnthropicMessageMapper.test.ts +173 -0
  96. package/src/infrastructure/llm/mappers/AnthropicMessageMapper.ts +34 -0
  97. package/src/infrastructure/llm/prompts/SystemPromptBuilder.test.ts +41 -0
  98. package/src/infrastructure/llm/prompts/SystemPromptBuilder.ts +31 -0
  99. package/src/infrastructure/llm/prompts/ToolDefinitions.ts +7 -0
  100. package/src/infrastructure/logging/.gitkeep +0 -0
  101. package/src/infrastructure/logging/PinoLogger.test.ts +59 -0
  102. package/src/infrastructure/logging/PinoLogger.ts +28 -0
  103. package/src/infrastructure/logging/index.ts +1 -0
  104. package/src/infrastructure/persistence/.gitkeep +0 -0
  105. package/src/infrastructure/persistence/InMemoryConversationRepository.test.ts +325 -0
  106. package/src/infrastructure/persistence/InMemoryConversationRepository.ts +69 -0
  107. package/src/infrastructure/persistence/PostgresPoolFactory.ts +11 -0
  108. package/src/infrastructure/persistence/PostgresSqlExecutor.test.ts +130 -0
  109. package/src/infrastructure/persistence/PostgresSqlExecutor.ts +34 -0
  110. package/src/infrastructure/persistence/index.ts +3 -0
  111. package/src/infrastructure/schemas/.gitkeep +0 -0
  112. package/src/infrastructure/schemas/FileSystemSchemaCatalog.test.ts +163 -0
  113. package/src/infrastructure/schemas/FileSystemSchemaCatalog.ts +35 -0
  114. package/src/infrastructure/schemas/index.ts +4 -0
  115. package/src/infrastructure/slack/.gitkeep +0 -0
  116. package/src/infrastructure/slack/BoltSlackMessenger.test.ts +59 -0
  117. package/src/infrastructure/slack/BoltSlackMessenger.ts +36 -0
  118. package/src/infrastructure/slack/SlackAdminLogger.test.ts +54 -0
  119. package/src/infrastructure/slack/SlackAdminLogger.ts +27 -0
  120. package/src/infrastructure/slack/SlackApp.ts +9 -0
  121. package/src/infrastructure/slack/handlers/AppMentionHandler.ts +52 -0
  122. package/src/infrastructure/slack/handlers/DirectMessageHandler.ts +65 -0
  123. package/src/infrastructure/slack/index.ts +5 -0
  124. package/src/infrastructure/sql/.gitkeep +0 -0
  125. package/src/infrastructure/sql/RegexSqlValidator.test.ts +242 -0
  126. package/src/infrastructure/sql/RegexSqlValidator.ts +53 -0
  127. package/src/infrastructure/sql/index.ts +1 -0
  128. package/src/main.ts +19 -0
  129. package/tsconfig.json +23 -0
  130. package/vitest.config.ts +15 -0
  131. package/vitest.setup.ts +23 -0
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { z } from 'zod';
3
+
4
+ describe(`env.ts`, () => {
5
+ const validEnv = {
6
+ SLACK_BOT_TOKEN: `xoxb-test-token`,
7
+ SLACK_SIGNING_SECRET: `test-secret`,
8
+ ANTHROPIC_API_KEY: `sk-ant-test-key`,
9
+ DATABASE_URL: `postgres://user:pass@localhost:5432/db`,
10
+ };
11
+
12
+ it(`should validate with all required variables`, () => {
13
+ const envSchema = z.object({
14
+ PORT: z.coerce.number().int().positive().default(3000),
15
+ NODE_ENV: z.enum([`development`, `production`, `test`]).default(`development`),
16
+ LOG_LEVEL: z.enum([`debug`, `info`, `warn`, `error`]).default(`info`),
17
+
18
+ SLACK_BOT_TOKEN: z.string().min(1, `SLACK_BOT_TOKEN is required`),
19
+ SLACK_SIGNING_SECRET: z.string().min(1, `SLACK_SIGNING_SECRET is required`),
20
+ SLACK_ADMIN_CHANNEL_ID: z.string().optional(),
21
+
22
+ LLM_MODEL: z.string().default(`claude-haiku-4-5-20251001`),
23
+ ANTHROPIC_API_KEY: z.string().min(1, `ANTHROPIC_API_KEY is required`),
24
+ AGENT_MAX_TURNS: z.coerce.number().int().positive().default(5),
25
+
26
+ DATABASE_URL: z.string().url(`DATABASE_URL must be a valid URL`),
27
+ SQL_TIMEOUT_MS: z.coerce.number().int().positive().default(30000),
28
+ SQL_MAX_ROWS: z.coerce.number().int().positive().default(100),
29
+ SQL_CSV_THRESHOLD: z.coerce.number().int().positive().default(15),
30
+
31
+ CONVERSATION_MAX_MESSAGES: z.coerce.number().int().positive().default(8),
32
+ CONVERSATION_TTL_MS: z.coerce.number().int().positive().default(3600000),
33
+ CONVERSATION_MAX_THREADS: z.coerce.number().int().positive().default(200),
34
+
35
+ SCHEMAS_DIR: z.string().default(`./docs/schemas`),
36
+ });
37
+
38
+ const result = envSchema.safeParse(validEnv);
39
+ expect(result.success).toBe(true);
40
+ if (result.success) {
41
+ expect(result.data.SLACK_BOT_TOKEN).toBe(`xoxb-test-token`);
42
+ expect(result.data.PORT).toBe(3000);
43
+ expect(result.data.LOG_LEVEL).toBe(`info`);
44
+ }
45
+ });
46
+
47
+ it(`should fail if SLACK_BOT_TOKEN is missing`, () => {
48
+ const envSchema = z.object({
49
+ SLACK_BOT_TOKEN: z.string().min(1, `SLACK_BOT_TOKEN is required`),
50
+ SLACK_SIGNING_SECRET: z.string().min(1, `SLACK_SIGNING_SECRET is required`),
51
+ ANTHROPIC_API_KEY: z.string().min(1, `ANTHROPIC_API_KEY is required`),
52
+ DATABASE_URL: z.string().url(`DATABASE_URL must be a valid URL`),
53
+ PORT: z.coerce.number().int().positive().default(3000),
54
+ NODE_ENV: z.enum([`development`, `production`, `test`]).default(`development`),
55
+ LOG_LEVEL: z.enum([`debug`, `info`, `warn`, `error`]).default(`info`),
56
+ SLACK_ADMIN_CHANNEL_ID: z.string().optional(),
57
+ LLM_MODEL: z.string().default(`claude-haiku-4-5-20251001`),
58
+ AGENT_MAX_TURNS: z.coerce.number().int().positive().default(5),
59
+ SQL_TIMEOUT_MS: z.coerce.number().int().positive().default(30000),
60
+ SQL_MAX_ROWS: z.coerce.number().int().positive().default(100),
61
+ SQL_CSV_THRESHOLD: z.coerce.number().int().positive().default(15),
62
+ CONVERSATION_MAX_MESSAGES: z.coerce.number().int().positive().default(8),
63
+ CONVERSATION_TTL_MS: z.coerce.number().int().positive().default(3600000),
64
+ CONVERSATION_MAX_THREADS: z.coerce.number().int().positive().default(200),
65
+ SCHEMAS_DIR: z.string().default(`./docs/schemas`),
66
+ });
67
+
68
+ const { SLACK_BOT_TOKEN, ...envWithoutToken } = validEnv;
69
+ const result = envSchema.safeParse(envWithoutToken);
70
+ expect(result.success).toBe(false);
71
+ });
72
+
73
+ it(`should apply defaults correctly`, () => {
74
+ const envSchema = z.object({
75
+ PORT: z.coerce.number().int().positive().default(3000),
76
+ NODE_ENV: z.enum([`development`, `production`, `test`]).default(`development`),
77
+ LOG_LEVEL: z.enum([`debug`, `info`, `warn`, `error`]).default(`info`),
78
+ SLACK_BOT_TOKEN: z.string().min(1),
79
+ SLACK_SIGNING_SECRET: z.string().min(1),
80
+ SLACK_ADMIN_CHANNEL_ID: z.string().optional(),
81
+ LLM_MODEL: z.string().default(`claude-haiku-4-5-20251001`),
82
+ ANTHROPIC_API_KEY: z.string().min(1),
83
+ AGENT_MAX_TURNS: z.coerce.number().int().positive().default(5),
84
+ DATABASE_URL: z.string().url(),
85
+ SQL_TIMEOUT_MS: z.coerce.number().int().positive().default(30000),
86
+ SQL_MAX_ROWS: z.coerce.number().int().positive().default(100),
87
+ SQL_CSV_THRESHOLD: z.coerce.number().int().positive().default(15),
88
+ CONVERSATION_MAX_MESSAGES: z.coerce.number().int().positive().default(8),
89
+ CONVERSATION_TTL_MS: z.coerce.number().int().positive().default(3600000),
90
+ CONVERSATION_MAX_THREADS: z.coerce.number().int().positive().default(200),
91
+ SCHEMAS_DIR: z.string().default(`./docs/schemas`),
92
+ });
93
+
94
+ const result = envSchema.safeParse(validEnv);
95
+ expect(result.success).toBe(true);
96
+ if (result.success) {
97
+ expect(result.data.PORT).toBe(3000);
98
+ expect(result.data.AGENT_MAX_TURNS).toBe(5);
99
+ expect(result.data.SQL_MAX_ROWS).toBe(100);
100
+ expect(result.data.SCHEMAS_DIR).toBe(`./docs/schemas`);
101
+ }
102
+ });
103
+ });
@@ -0,0 +1,41 @@
1
+ import { config } from 'dotenv';
2
+ import { z } from 'zod';
3
+
4
+ config({ override: true });
5
+
6
+
7
+ const envSchema = z.object({
8
+ PORT: z.coerce.number().int().positive().default(3000),
9
+ NODE_ENV: z.enum([`development`, `production`, `test`]).default(`development`),
10
+ LOG_LEVEL: z.enum([`debug`, `info`, `warn`, `error`]).default(`info`),
11
+
12
+ SLACK_BOT_TOKEN: z.string().min(1, `SLACK_BOT_TOKEN is required`),
13
+ SLACK_SIGNING_SECRET: z.string().min(1, `SLACK_SIGNING_SECRET is required`),
14
+ SLACK_ADMIN_CHANNEL_ID: z.string().optional(),
15
+
16
+ LLM_MODEL: z.string().default(`claude-haiku-4-5-20251001`),
17
+ ANTHROPIC_API_KEY: z.string().min(1, `ANTHROPIC_API_KEY is required`),
18
+ AGENT_MAX_TURNS: z.coerce.number().int().positive().default(5),
19
+
20
+ DATABASE_URL: z.string().url(`DATABASE_URL must be a valid URL`),
21
+ SQL_TIMEOUT_MS: z.coerce.number().int().positive().default(30000),
22
+ SQL_MAX_ROWS: z.coerce.number().int().positive().default(100),
23
+ SQL_CSV_THRESHOLD: z.coerce.number().int().positive().default(15),
24
+
25
+ CONVERSATION_MAX_MESSAGES: z.coerce.number().int().positive().default(8),
26
+ CONVERSATION_TTL_MS: z.coerce.number().int().positive().default(3600000),
27
+ CONVERSATION_MAX_THREADS: z.coerce.number().int().positive().default(200),
28
+
29
+ SCHEMAS_DIR: z.string().default(`./docs/schemas`),
30
+ });
31
+
32
+ const parsed = envSchema.safeParse(process.env);
33
+
34
+ if (!parsed.success) {
35
+ console.error(`❌ Invalid environment variables:`);
36
+ console.error(parsed.error.format());
37
+ process.exit(1);
38
+ }
39
+
40
+ export const env = parsed.data;
41
+ export type Env = z.infer<typeof envSchema>;
File without changes
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import Conversation, { CONVERSATION_MAX_MESSAGES } from "./Conversation.js";
3
+ import ConversationMessage from "./ConversationMessage.js";
4
+ import ThreadId from "../value-objects/ThreadId.js";
5
+
6
+ describe("Conversation", () => {
7
+ it("creates an empty conversation with a thread ID", () => {
8
+ const threadId = ThreadId.create("C123", "1234567890.123456");
9
+ const conversation = Conversation.create(threadId);
10
+
11
+ expect(conversation.threadId).toBe(threadId);
12
+ expect(conversation.messages).toEqual([]);
13
+ });
14
+
15
+ it("appends a message to the conversation", () => {
16
+ const threadId = ThreadId.create("C123", "1234567890.123456");
17
+ const conversation = Conversation.create(threadId);
18
+ const message = ConversationMessage.create("user", "Hello");
19
+
20
+ const updated = conversation.append(message);
21
+
22
+ expect(updated.messages).toHaveLength(1);
23
+ expect(updated.messages[0]).toEqual(message);
24
+ });
25
+
26
+ it("maintains immutability - original conversation is unchanged", () => {
27
+ const threadId = ThreadId.create("C123", "1234567890.123456");
28
+ const conversation = Conversation.create(threadId);
29
+ const message = ConversationMessage.create("user", "Hello");
30
+
31
+ const updated = conversation.append(message);
32
+
33
+ expect(conversation.messages).toHaveLength(0);
34
+ expect(updated.messages).toHaveLength(1);
35
+ });
36
+
37
+ it("enforces LRU purge when exceeding max messages", () => {
38
+ const threadId = ThreadId.create("C123", "1234567890.123456");
39
+ let conversation = Conversation.create(threadId);
40
+
41
+ for (let i = 0; i < CONVERSATION_MAX_MESSAGES + 1; i += 1) {
42
+ const message = ConversationMessage.create("user", `Message ${String(i)}`);
43
+ conversation = conversation.append(message);
44
+ }
45
+
46
+ expect(conversation.messages).toHaveLength(CONVERSATION_MAX_MESSAGES);
47
+ expect(conversation.messages[0]?.content).toBe(`Message 1`);
48
+ });
49
+
50
+ it("preserves messages in order with multiple appends", () => {
51
+ const threadId = ThreadId.create("C123", "1234567890.123456");
52
+ let conversation = Conversation.create(threadId);
53
+
54
+ const messages = [
55
+ ConversationMessage.create("user", "First"),
56
+ ConversationMessage.create("assistant", "Second"),
57
+ ConversationMessage.create("user", "Third"),
58
+ ];
59
+
60
+ for (const msg of messages) {
61
+ conversation = conversation.append(msg);
62
+ }
63
+
64
+ expect(conversation.messages).toHaveLength(3);
65
+ expect(conversation.messages[0]?.content).toBe("First");
66
+ expect(conversation.messages[1]?.content).toBe("Second");
67
+ expect(conversation.messages[2]?.content).toBe("Third");
68
+ });
69
+ });
@@ -0,0 +1,26 @@
1
+ import ThreadId from "@/domain/value-objects/ThreadId.js";
2
+ import type { ConversationMessage } from "./ConversationMessage.js";
3
+
4
+ const CONVERSATION_MAX_MESSAGES = 10;
5
+
6
+ class Conversation {
7
+ private constructor(
8
+ public readonly threadId: ThreadId,
9
+ public readonly messages: readonly ConversationMessage[],
10
+ ) {}
11
+
12
+ static create(threadId: ThreadId): Conversation {
13
+ return new Conversation(threadId, []);
14
+ }
15
+
16
+ append(message: ConversationMessage): Conversation {
17
+ const newMessages = [...this.messages, message];
18
+ if (newMessages.length > CONVERSATION_MAX_MESSAGES) {
19
+ newMessages.shift();
20
+ }
21
+ return new Conversation(this.threadId, newMessages);
22
+ }
23
+ }
24
+
25
+ export default Conversation;
26
+ export { CONVERSATION_MAX_MESSAGES };
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import ConversationMessage from "./ConversationMessage.js";
3
+
4
+ describe("ConversationMessage", () => {
5
+ let beforeTime: Date;
6
+ let afterTime: Date;
7
+
8
+ beforeEach(() => {
9
+ beforeTime = new Date();
10
+ });
11
+
12
+ it("creates a message with user role", () => {
13
+ const message = ConversationMessage.create("user", "Hello");
14
+
15
+ expect(message.role).toBe("user");
16
+ expect(message.content).toBe("Hello");
17
+ expect(message.timestamp).toBeInstanceOf(Date);
18
+ });
19
+
20
+ it("creates a message with assistant role", () => {
21
+ const message = ConversationMessage.create("assistant", "Hi there");
22
+
23
+ expect(message.role).toBe("assistant");
24
+ expect(message.content).toBe("Hi there");
25
+ });
26
+
27
+ it("timestamps the message at creation", () => {
28
+ beforeTime = new Date();
29
+ const message = ConversationMessage.create("user", "Test");
30
+ afterTime = new Date();
31
+
32
+ expect(message.timestamp.getTime()).toBeGreaterThanOrEqual(beforeTime.getTime());
33
+ expect(message.timestamp.getTime()).toBeLessThanOrEqual(afterTime.getTime());
34
+ });
35
+
36
+ it("preserves message content as-is", () => {
37
+ const content = " Multiple spaces and\nnewlines\n\n ";
38
+ const message = ConversationMessage.create("user", content);
39
+
40
+ expect(message.content).toBe(content);
41
+ });
42
+
43
+ it("handles empty content", () => {
44
+ const message = ConversationMessage.create("user", "");
45
+
46
+ expect(message.content).toBe("");
47
+ expect(message.role).toBe("user");
48
+ });
49
+ });
@@ -0,0 +1,18 @@
1
+ interface ConversationMessage {
2
+ role: "user" | "assistant";
3
+ content: string;
4
+ timestamp: Date;
5
+ }
6
+
7
+ const ConversationMessage = {
8
+ create(role: "user" | "assistant", content: string): ConversationMessage {
9
+ return {
10
+ role,
11
+ content,
12
+ timestamp: new Date(),
13
+ };
14
+ },
15
+ };
16
+
17
+ export default ConversationMessage;
18
+ export type { ConversationMessage };
@@ -0,0 +1,2 @@
1
+ export { default as Conversation, CONVERSATION_MAX_MESSAGES } from "./Conversation.js";
2
+ export { default as ConversationMessage } from "./ConversationMessage.js";
File without changes
@@ -0,0 +1,12 @@
1
+ import DomainError from "./DomainError.js";
2
+
3
+ class AgentLoopExceededError extends DomainError {
4
+ readonly code = `AGENT_LOOP_EXCEEDED`;
5
+
6
+ constructor(public readonly maxTurns: number) {
7
+ super(`Agent loop exceeded maximum turns (${maxTurns.toString()})`);
8
+ Object.setPrototypeOf(this, AgentLoopExceededError.prototype);
9
+ }
10
+ }
11
+
12
+ export default AgentLoopExceededError;
@@ -0,0 +1,106 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ DomainError,
4
+ InvalidSqlError,
5
+ SqlExecutionError,
6
+ LLMError,
7
+ SchemaLoadError,
8
+ AgentLoopExceededError,
9
+ } from "./index.js";
10
+
11
+ describe("DomainError hierarchy", () => {
12
+
13
+ describe("InvalidSqlError", () => {
14
+ it("should create instance with sql and reason", () => {
15
+ const error = new InvalidSqlError(`DROP TABLE users`, `mutation not allowed`);
16
+ expect(error.code).toBe(`INVALID_SQL`);
17
+ expect(error.sql).toBe(`DROP TABLE users`);
18
+ expect(error.reason).toBe(`mutation not allowed`);
19
+ expect(error.message).toBe(`Invalid SQL: mutation not allowed`);
20
+ expect(error).toBeInstanceOf(DomainError);
21
+ expect(error).toBeInstanceOf(Error);
22
+ });
23
+
24
+ it("should have correct name", () => {
25
+ const error = new InvalidSqlError(`DROP TABLE users`, `mutation not allowed`);
26
+ expect(error.name).toBe(`InvalidSqlError`);
27
+ });
28
+ });
29
+
30
+ describe("SqlExecutionError", () => {
31
+ it("should create instance with sql and pgError", () => {
32
+ const error = new SqlExecutionError(
33
+ `SELECT * FROM nonexistent`,
34
+ `relation "nonexistent" does not exist`,
35
+ );
36
+ expect(error.code).toBe(`SQL_EXECUTION_ERROR`);
37
+ expect(error.sql).toBe(`SELECT * FROM nonexistent`);
38
+ expect(error.pgError).toBe(`relation "nonexistent" does not exist`);
39
+ expect(error.message).toBe(
40
+ `SQL execution failed: relation "nonexistent" does not exist`,
41
+ );
42
+ expect(error).toBeInstanceOf(DomainError);
43
+ });
44
+ });
45
+
46
+ describe("LLMError", () => {
47
+ it("should create instance with provider and details", () => {
48
+ const error = new LLMError(`anthropic`, `rate limit exceeded`);
49
+ expect(error.code).toBe(`LLM_ERROR`);
50
+ expect(error.provider).toBe(`anthropic`);
51
+ expect(error.details).toBe(`rate limit exceeded`);
52
+ expect(error.message).toBe(`LLM error from anthropic: rate limit exceeded`);
53
+ expect(error).toBeInstanceOf(DomainError);
54
+ });
55
+ });
56
+
57
+ describe("SchemaLoadError", () => {
58
+ it("should create instance with schemaName and reason", () => {
59
+ const error = new SchemaLoadError(`efarmz_db`, `file not found`);
60
+ expect(error.code).toBe(`SCHEMA_LOAD_ERROR`);
61
+ expect(error.schemaName).toBe(`efarmz_db`);
62
+ expect(error.reason).toBe(`file not found`);
63
+ expect(error.message).toBe(`Failed to load schema efarmz_db: file not found`);
64
+ expect(error).toBeInstanceOf(DomainError);
65
+ });
66
+ });
67
+
68
+ describe("AgentLoopExceededError", () => {
69
+ it("should create instance with maxTurns", () => {
70
+ const error = new AgentLoopExceededError(10);
71
+ expect(error.code).toBe(`AGENT_LOOP_EXCEEDED`);
72
+ expect(error.maxTurns).toBe(10);
73
+ expect(error.message).toBe(`Agent loop exceeded maximum turns (10)`);
74
+ expect(error).toBeInstanceOf(DomainError);
75
+ });
76
+ });
77
+
78
+ it("all error codes should be unique", () => {
79
+ const codes = [
80
+ new InvalidSqlError(``, ``).code,
81
+ new SqlExecutionError(``, ``).code,
82
+ new LLMError(``, ``).code,
83
+ new SchemaLoadError(``, ``).code,
84
+ new AgentLoopExceededError(0).code,
85
+ ];
86
+ const uniqueCodes = new Set(codes);
87
+ expect(uniqueCodes.size).toBe(codes.length);
88
+ });
89
+
90
+ it("all errors should be instanceof DomainError", () => {
91
+ const errors: DomainError[] = [
92
+ new InvalidSqlError(``, ``),
93
+ new SqlExecutionError(``, ``),
94
+ new LLMError(``, ``),
95
+ new SchemaLoadError(``, ``),
96
+ new AgentLoopExceededError(0),
97
+ ];
98
+
99
+ for (const error of errors) {
100
+ expect(error).toBeInstanceOf(DomainError);
101
+ expect(error).toBeInstanceOf(Error);
102
+ expect(error.code).toBeDefined();
103
+ expect(typeof error.code).toBe(`string`);
104
+ }
105
+ });
106
+ });
@@ -0,0 +1,11 @@
1
+ abstract class DomainError extends Error {
2
+ abstract readonly code: string;
3
+
4
+ constructor(message: string) {
5
+ super(message);
6
+ this.name = this.constructor.name;
7
+ Object.setPrototypeOf(this, DomainError.prototype);
8
+ }
9
+ }
10
+
11
+ export default DomainError;
@@ -0,0 +1,15 @@
1
+ import DomainError from "./DomainError.js";
2
+
3
+ class InvalidSqlError extends DomainError {
4
+ readonly code = `INVALID_SQL`;
5
+
6
+ constructor(
7
+ public readonly sql: string,
8
+ public readonly reason: string,
9
+ ) {
10
+ super(`Invalid SQL: ${reason}`);
11
+ Object.setPrototypeOf(this, InvalidSqlError.prototype);
12
+ }
13
+ }
14
+
15
+ export default InvalidSqlError;
@@ -0,0 +1,15 @@
1
+ import DomainError from "./DomainError.js";
2
+
3
+ class LLMError extends DomainError {
4
+ readonly code = `LLM_ERROR`;
5
+
6
+ constructor(
7
+ public readonly provider: string,
8
+ public readonly details: string,
9
+ ) {
10
+ super(`LLM error from ${provider}: ${details}`);
11
+ Object.setPrototypeOf(this, LLMError.prototype);
12
+ }
13
+ }
14
+
15
+ export default LLMError;
@@ -0,0 +1,15 @@
1
+ import DomainError from "./DomainError.js";
2
+
3
+ class SchemaLoadError extends DomainError {
4
+ readonly code = `SCHEMA_LOAD_ERROR`;
5
+
6
+ constructor(
7
+ public readonly schemaName: string,
8
+ public readonly reason: string,
9
+ ) {
10
+ super(`Failed to load schema ${schemaName}: ${reason}`);
11
+ Object.setPrototypeOf(this, SchemaLoadError.prototype);
12
+ }
13
+ }
14
+
15
+ export default SchemaLoadError;
@@ -0,0 +1,15 @@
1
+ import DomainError from "./DomainError.js";
2
+
3
+ class SqlExecutionError extends DomainError {
4
+ readonly code = `SQL_EXECUTION_ERROR`;
5
+
6
+ constructor(
7
+ public readonly sql: string,
8
+ public readonly pgError: string,
9
+ ) {
10
+ super(`SQL execution failed: ${pgError}`);
11
+ Object.setPrototypeOf(this, SqlExecutionError.prototype);
12
+ }
13
+ }
14
+
15
+ export default SqlExecutionError;
@@ -0,0 +1,15 @@
1
+ import DomainError from "./DomainError.js";
2
+ import InvalidSqlError from "./InvalidSqlError.js";
3
+ import SqlExecutionError from "./SqlExecutionError.js";
4
+ import LLMError from "./LLMError.js";
5
+ import SchemaLoadError from "./SchemaLoadError.js";
6
+ import AgentLoopExceededError from "./AgentLoopExceededError.js";
7
+
8
+ export {
9
+ DomainError,
10
+ InvalidSqlError,
11
+ SqlExecutionError,
12
+ LLMError,
13
+ SchemaLoadError,
14
+ AgentLoopExceededError,
15
+ };
File without changes
@@ -0,0 +1,16 @@
1
+ interface AdminLogEntry {
2
+ userId: string;
3
+ question: string;
4
+ sqls: string[];
5
+ rowCount: number;
6
+ durationMs: number;
7
+ inputTokens: number;
8
+ outputTokens: number;
9
+ }
10
+
11
+ interface AdminLogger {
12
+ logQuery(entry: AdminLogEntry): Promise<void>;
13
+ }
14
+
15
+ export default AdminLogger;
16
+ export type { AdminLogEntry };
@@ -0,0 +1,10 @@
1
+ import type ThreadId from "@/domain/value-objects/ThreadId.js";
2
+ import type Conversation from "@/domain/entities/Conversation.js";
3
+
4
+ interface ConversationRepository {
5
+ get(threadId: ThreadId): Promise<Conversation | null>;
6
+ save(conversation: Conversation): Promise<void>;
7
+ purge(threadId: ThreadId): Promise<void>;
8
+ }
9
+
10
+ export default ConversationRepository;
@@ -0,0 +1,33 @@
1
+ interface LLMToolUse {
2
+ id: string;
3
+ name: string;
4
+ input: Record<string, unknown>;
5
+ }
6
+
7
+ interface LLMRequest {
8
+ systemPrompt: string;
9
+ messages: Array<{
10
+ role: "user" | "assistant";
11
+ content: string;
12
+ }>;
13
+ tools?: Array<{
14
+ name: string;
15
+ description: string;
16
+ inputSchema: object;
17
+ }>;
18
+ }
19
+
20
+ interface LLMResponse {
21
+ content: string;
22
+ toolUses: LLMToolUse[];
23
+ stopReason: "end_turn" | "tool_use" | "max_tokens";
24
+ inputTokens: number;
25
+ outputTokens: number;
26
+ }
27
+
28
+ interface LLMProvider {
29
+ generate(request: LLMRequest): Promise<LLMResponse>;
30
+ }
31
+
32
+ export default LLMProvider;
33
+ export type { LLMToolUse, LLMRequest, LLMResponse };
@@ -0,0 +1,8 @@
1
+ interface Logger {
2
+ debug(msg: string, meta?: object): void;
3
+ info(msg: string, meta?: object): void;
4
+ warn(msg: string, meta?: object): void;
5
+ error(msg: string, meta?: object): void;
6
+ }
7
+
8
+ export default Logger;
@@ -0,0 +1,5 @@
1
+ interface SchemaCatalog {
2
+ getDocumentation(): string;
3
+ }
4
+
5
+ export default SchemaCatalog;
@@ -0,0 +1,8 @@
1
+ import type ThreadId from "@/domain/value-objects/ThreadId.js";
2
+
3
+ interface SlackMessenger {
4
+ post(threadId: ThreadId, text: string): Promise<void>;
5
+ postCsv(threadId: ThreadId, text: string, csvBuffer: Buffer, filename: string): Promise<void>;
6
+ }
7
+
8
+ export default SlackMessenger;
@@ -0,0 +1,8 @@
1
+ import type SqlQuery from "@/domain/value-objects/SqlQuery.js";
2
+ import type { QueryResult } from "@/domain/value-objects/QueryResult.js";
3
+
4
+ interface SqlExecutor {
5
+ execute(query: SqlQuery): Promise<QueryResult>;
6
+ }
7
+
8
+ export default SqlExecutor;
@@ -0,0 +1,5 @@
1
+ interface SqlValidator {
2
+ validate(raw: string): string;
3
+ }
4
+
5
+ export default SqlValidator;
@@ -0,0 +1,17 @@
1
+ export { default as AdminLogger } from "./AdminLogger.js";
2
+ export type { AdminLogEntry } from "./AdminLogger.js";
3
+
4
+ export { default as ConversationRepository } from "./ConversationRepository.js";
5
+
6
+ export { default as LLMProvider } from "./LLMProvider.js";
7
+ export type { LLMRequest, LLMResponse, LLMToolUse } from "./LLMProvider.js";
8
+
9
+ export { default as Logger } from "./Logger.js";
10
+
11
+ export { default as SchemaCatalog } from "./SchemaCatalog.js";
12
+
13
+ export { default as SlackMessenger } from "./SlackMessenger.js";
14
+
15
+ export { default as SqlExecutor } from "./SqlExecutor.js";
16
+
17
+ export { default as SqlValidator } from "./SqlValidator.js";
File without changes
@@ -0,0 +1,6 @@
1
+ enum LLMProviderName {
2
+ ANTHROPIC = `anthropic`,
3
+ OPENAI = `openai`,
4
+ }
5
+
6
+ export default LLMProviderName;