@andreprado/agentkit 0.1.0-alpha.10

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 (122) hide show
  1. package/README.md +69 -0
  2. package/bin/agentkit.mjs +23 -0
  3. package/docs/guides/add-channel.md +114 -0
  4. package/docs/guides/add-knowledge.md +134 -0
  5. package/docs/guides/add-tool.md +342 -0
  6. package/docs/guides/agentkit-skills-architecture.md +471 -0
  7. package/docs/guides/channel-security.md +81 -0
  8. package/docs/guides/channels-implementation-map.md +243 -0
  9. package/docs/guides/channels-production-handoff.md +102 -0
  10. package/docs/guides/connect-telegram.md +110 -0
  11. package/docs/guides/connect-whatsapp-zapster.md +119 -0
  12. package/docs/guides/create-agent.md +220 -0
  13. package/docs/guides/prepare-deploy.md +209 -0
  14. package/docs/guides/run-evals.md +179 -0
  15. package/docs/guides/security-rules.md +156 -0
  16. package/docs/guides/use-provider.md +140 -0
  17. package/docs/llms-full.txt +876 -0
  18. package/docs/llms.txt +83 -0
  19. package/docs/portable-deploy-release-checklist.md +41 -0
  20. package/package.json +47 -0
  21. package/src/cli/args.ts +36 -0
  22. package/src/cli/cloud-client.ts +265 -0
  23. package/src/cli/commands/channels.ts +810 -0
  24. package/src/cli/commands/knowledge.ts +136 -0
  25. package/src/cli/constants.ts +4 -0
  26. package/src/cli/deploy-chat-ui.ts +392 -0
  27. package/src/cli/deploy-readiness.ts +348 -0
  28. package/src/cli/flags.ts +162 -0
  29. package/src/cli/help.ts +184 -0
  30. package/src/cli/index.ts +1276 -0
  31. package/src/cli/process.ts +31 -0
  32. package/src/cloud/artifact.ts +139 -0
  33. package/src/cloud/client.ts +79 -0
  34. package/src/cloud/contracts.ts +63 -0
  35. package/src/cloud/index.ts +3 -0
  36. package/src/create-project.ts +177 -0
  37. package/src/index.ts +408 -0
  38. package/src/providers/index.ts +25 -0
  39. package/src/providers/pi.ts +286 -0
  40. package/src/providers/test.ts +133 -0
  41. package/src/providers/types.ts +34 -0
  42. package/src/runtime/build.ts +43 -0
  43. package/src/runtime/channel-buffer.ts +30 -0
  44. package/src/runtime/channel-test-harness.ts +112 -0
  45. package/src/runtime/channels/telegram.ts +360 -0
  46. package/src/runtime/channels/website.ts +132 -0
  47. package/src/runtime/channels/whatsapp-meta.ts +71 -0
  48. package/src/runtime/channels/whatsapp-zapster.ts +278 -0
  49. package/src/runtime/channels.ts +138 -0
  50. package/src/runtime/chat.ts +218 -0
  51. package/src/runtime/config.ts +684 -0
  52. package/src/runtime/conversations.ts +38 -0
  53. package/src/runtime/core/deploy-state.ts +54 -0
  54. package/src/runtime/core/manifest.ts +213 -0
  55. package/src/runtime/core/targets.ts +133 -0
  56. package/src/runtime/database.ts +256 -0
  57. package/src/runtime/db-commands.ts +167 -0
  58. package/src/runtime/deploy-readiness.ts +105 -0
  59. package/src/runtime/deploy.ts +1 -0
  60. package/src/runtime/dev-server.ts +1247 -0
  61. package/src/runtime/docs.ts +36 -0
  62. package/src/runtime/env.ts +152 -0
  63. package/src/runtime/errors.ts +13 -0
  64. package/src/runtime/evals.ts +509 -0
  65. package/src/runtime/inspect.ts +203 -0
  66. package/src/runtime/knowledge/chunk.ts +333 -0
  67. package/src/runtime/knowledge/config.ts +135 -0
  68. package/src/runtime/knowledge/embeddings.ts +133 -0
  69. package/src/runtime/knowledge/ingest.ts +521 -0
  70. package/src/runtime/knowledge/prompt-policy.ts +30 -0
  71. package/src/runtime/knowledge/retrieve.ts +283 -0
  72. package/src/runtime/knowledge/schema.ts +56 -0
  73. package/src/runtime/knowledge/tool.ts +64 -0
  74. package/src/runtime/knowledge/vector.ts +258 -0
  75. package/src/runtime/runtime-contract.ts +93 -0
  76. package/src/runtime/spec.ts +152 -0
  77. package/src/runtime/sync.ts +144 -0
  78. package/src/runtime/targets/cloudflare/build.ts +2517 -0
  79. package/src/runtime/targets/container/build.ts +146 -0
  80. package/src/runtime/targets/container/server.ts +33 -0
  81. package/src/runtime/targets/vps/deploy.ts +206 -0
  82. package/src/runtime/tool-runner.ts +65 -0
  83. package/src/runtime/tools.ts +470 -0
  84. package/src/runtime/traces.ts +41 -0
  85. package/src/storage/sqlite.ts +1118 -0
  86. package/src/templates/blank.ts +394 -0
  87. package/src/templates/dentista.ts +1003 -0
  88. package/src/templates/index.ts +33 -0
  89. package/src/templates/skills/agentkit-build-agent/SKILL.md +51 -0
  90. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +20 -0
  91. package/src/templates/skills/agentkit-build-agent/templates/sales-qualifier.instructions.md +17 -0
  92. package/src/templates/skills/agentkit-build-agent/templates/support-agent.instructions.md +16 -0
  93. package/src/templates/skills/agentkit-capsule/SKILL.md +62 -0
  94. package/src/templates/skills/agentkit-capsule/references/docs-router.md +15 -0
  95. package/src/templates/skills/agentkit-channels/SKILL.md +62 -0
  96. package/src/templates/skills/agentkit-channels/references/channel-buffering.md +58 -0
  97. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +41 -0
  98. package/src/templates/skills/agentkit-channels/references/telegram.md +38 -0
  99. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +44 -0
  100. package/src/templates/skills/agentkit-database/SKILL.md +45 -0
  101. package/src/templates/skills/agentkit-database/templates/appointments.schema.sql +15 -0
  102. package/src/templates/skills/agentkit-database/templates/leads.schema.sql +17 -0
  103. package/src/templates/skills/agentkit-deploy/SKILL.md +44 -0
  104. package/src/templates/skills/agentkit-evals/SKILL.md +60 -0
  105. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +22 -0
  106. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +14 -0
  107. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +14 -0
  108. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +18 -0
  109. package/src/templates/skills/agentkit-knowledge/SKILL.md +40 -0
  110. package/src/templates/skills/agentkit-knowledge/templates/faq.md +14 -0
  111. package/src/templates/skills/agentkit-knowledge/templates/policies.md +14 -0
  112. package/src/templates/skills/agentkit-knowledge/templates/prices.csv +3 -0
  113. package/src/templates/skills/agentkit-prompts/SKILL.md +45 -0
  114. package/src/templates/skills/agentkit-prompts/templates/knowledge-grounded-faq.instructions.md +11 -0
  115. package/src/templates/skills/agentkit-provider/SKILL.md +57 -0
  116. package/src/templates/skills/agentkit-security/SKILL.md +55 -0
  117. package/src/templates/skills/agentkit-tools/SKILL.md +36 -0
  118. package/src/templates/skills/agentkit-tools/examples/database-write.tool.md +35 -0
  119. package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +37 -0
  120. package/src/templates/skills/agentkit-tools/examples/lookup-order.tool.md +46 -0
  121. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +52 -0
  122. package/src/templates/support.ts +401 -0
@@ -0,0 +1,38 @@
1
+ import { openCapsuleStore, type ConversationRecord, type ConversationSummary } from "../storage/sqlite";
2
+ import { loadAgentCapsule } from "./config";
3
+ import { AgentKitError } from "./errors";
4
+
5
+ export async function listConversationsFromCwd(cwd = process.cwd()): Promise<ConversationSummary[]> {
6
+ const capsule = await loadAgentCapsule(cwd);
7
+ const store = await openCapsuleStore(capsule);
8
+
9
+ try {
10
+ return store.listConversations();
11
+ } finally {
12
+ store.close();
13
+ }
14
+ }
15
+
16
+ export async function getConversationFromCwd(
17
+ cwd: string,
18
+ conversationId: string,
19
+ ): Promise<ConversationRecord> {
20
+ if (!conversationId) {
21
+ throw new AgentKitError("validation_error", "Missing conversation id.");
22
+ }
23
+
24
+ const capsule = await loadAgentCapsule(cwd);
25
+ const store = await openCapsuleStore(capsule);
26
+
27
+ try {
28
+ const conversation = store.getConversation(conversationId);
29
+
30
+ if (!conversation) {
31
+ throw new AgentKitError("conversation_not_found", `Conversation "${conversationId}" was not found.`);
32
+ }
33
+
34
+ return conversation;
35
+ } finally {
36
+ store.close();
37
+ }
38
+ }
@@ -0,0 +1,54 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ import { findAgentCapsuleRoot } from "../config";
5
+ import type { RuntimeTargetId } from "./targets";
6
+
7
+ export type LocalDeployState = {
8
+ project_id?: string;
9
+ deploy_id: string;
10
+ url?: string;
11
+ target: RuntimeTargetId;
12
+ runtime: "edge" | "container";
13
+ resources: Record<string, unknown>;
14
+ owner_state?: "anonymous" | "claimed";
15
+ access?: {
16
+ mode?: string;
17
+ private_token?: string;
18
+ };
19
+ created_at?: string;
20
+ };
21
+
22
+ export function normalizeDeployState(input: unknown): LocalDeployState | null {
23
+ if (!input || typeof input !== "object") {
24
+ return null;
25
+ }
26
+
27
+ const state = input as Partial<LocalDeployState>;
28
+
29
+ if (typeof state.deploy_id !== "string" || state.deploy_id.length === 0) {
30
+ return null;
31
+ }
32
+
33
+ return {
34
+ ...state,
35
+ deploy_id: state.deploy_id,
36
+ target: isRuntimeTarget(state.target) ? state.target : "cloudflare",
37
+ runtime: state.runtime === "container" ? "container" : "edge",
38
+ resources: state.resources && typeof state.resources === "object" ? state.resources : {},
39
+ };
40
+ }
41
+
42
+ export async function readLocalDeployStateIfExists(cwd: string): Promise<LocalDeployState | null> {
43
+ try {
44
+ const root = await findAgentCapsuleRoot(cwd);
45
+ const source = await readFile(join(root, ".agentkit/deploy.json"), "utf8");
46
+ return normalizeDeployState(JSON.parse(source));
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function isRuntimeTarget(value: unknown): value is RuntimeTargetId {
53
+ return value === "local" || value === "cloudflare" || value === "container" || value === "vps";
54
+ }
@@ -0,0 +1,213 @@
1
+ import { relative, resolve } from "node:path";
2
+
3
+ import type { AgentConfig, AgentTool } from "../../index";
4
+ import type { LoadedAgentCapsule } from "../config";
5
+ import { resolveKnowledgeConfig } from "../knowledge/config";
6
+ import { KNOWLEDGE_SEARCH_TOOL_NAME } from "../knowledge/tool";
7
+
8
+ export type ManifestSchema = {
9
+ source: string;
10
+ sha256: string;
11
+ };
12
+
13
+ export type SharedAgentManifest = {
14
+ schema_version: 1;
15
+ name: string;
16
+ runtime: AgentConfig["runtime"];
17
+ provider: AgentConfig["provider"];
18
+ prompt: string;
19
+ secrets: string[];
20
+ tools: Array<{
21
+ name: string;
22
+ description: string;
23
+ inputSchema: AgentTool["inputSchema"];
24
+ outputSchema?: AgentTool["outputSchema"];
25
+ secrets: string[];
26
+ permissions: string[];
27
+ timeoutMs: number | null;
28
+ }>;
29
+ channels: NonNullable<AgentConfig["channels"]>;
30
+ knowledge: {
31
+ enabled: boolean;
32
+ sources: Array<{
33
+ kind: "file" | "url";
34
+ value: string;
35
+ title: string | null;
36
+ }>;
37
+ embedding: {
38
+ provider: string;
39
+ model: string | null;
40
+ dimensions: number | null;
41
+ secret: string | null;
42
+ };
43
+ retrieval: {
44
+ topK: number;
45
+ hybrid: boolean;
46
+ };
47
+ internalTool: string | null;
48
+ };
49
+ access: AgentConfig["access"];
50
+ storage: {
51
+ driver: AgentConfig["storage"]["driver"];
52
+ database:
53
+ | {
54
+ driver: "none";
55
+ }
56
+ | {
57
+ driver: "turso";
58
+ schema: string | null;
59
+ schema_sha256: string | null;
60
+ };
61
+ files:
62
+ | {
63
+ driver: "none";
64
+ }
65
+ | {
66
+ driver: "object";
67
+ bucket: string;
68
+ prefix: string;
69
+ maxUploadBytes: number;
70
+ };
71
+ };
72
+ };
73
+
74
+ export function buildSharedAgentManifest(
75
+ capsule: LoadedAgentCapsule,
76
+ options: { databaseSchema?: ManifestSchema | null } = {},
77
+ ): SharedAgentManifest {
78
+ const requiredSecrets = new Set(capsule.config.secrets);
79
+ const database = buildDatabaseManifest(capsule, options.databaseSchema ?? null);
80
+ const files = buildFilesManifest(capsule);
81
+ const knowledge = resolveKnowledgeConfig(capsule.config);
82
+
83
+ if (capsule.config.access.mode === "private") {
84
+ requiredSecrets.add("AGENTKIT_PRIVATE_TOKEN");
85
+ }
86
+
87
+ if (capsule.config.access.mode === "token") {
88
+ requiredSecrets.add("AGENTKIT_ACCESS_TOKEN");
89
+ }
90
+
91
+ if (capsule.config.provider.name === "openai") {
92
+ requiredSecrets.add("OPENAI_API_KEY");
93
+ }
94
+
95
+ if (database.driver === "turso") {
96
+ requiredSecrets.add("TURSO_DATABASE_URL");
97
+ requiredSecrets.add("TURSO_AUTH_TOKEN");
98
+ }
99
+
100
+ if (knowledge.embedding.secret) {
101
+ requiredSecrets.add(knowledge.embedding.secret);
102
+ }
103
+
104
+ for (const tool of capsule.config.tools ?? []) {
105
+ for (const secret of tool.secrets ?? []) {
106
+ requiredSecrets.add(secret);
107
+ }
108
+ }
109
+
110
+ for (const channel of capsule.config.channels ?? []) {
111
+ for (const secret of channel.secrets) {
112
+ requiredSecrets.add(secret);
113
+ }
114
+ }
115
+
116
+ return {
117
+ schema_version: 1,
118
+ name: capsule.config.name,
119
+ runtime: capsule.config.runtime,
120
+ provider: capsule.config.provider,
121
+ prompt: toCapsulePath(capsule, capsule.instructionsPath),
122
+ secrets: Array.from(requiredSecrets).sort(),
123
+ tools: (capsule.config.tools ?? []).map((tool) => ({
124
+ name: tool.name,
125
+ description: tool.description,
126
+ inputSchema: tool.inputSchema,
127
+ outputSchema: tool.outputSchema,
128
+ secrets: [...(tool.secrets ?? [])].sort(),
129
+ permissions: [...(tool.permissions ?? [])].sort(),
130
+ timeoutMs: tool.timeoutMs ?? null,
131
+ })),
132
+ channels: capsule.config.channels ?? [],
133
+ knowledge: {
134
+ enabled: knowledge.enabled,
135
+ sources: knowledge.sources.map((source) => ({
136
+ kind: source.kind,
137
+ value: source.value,
138
+ title: source.title,
139
+ })),
140
+ embedding: knowledge.embedding,
141
+ retrieval: knowledge.retrieval,
142
+ internalTool: knowledge.enabled ? KNOWLEDGE_SEARCH_TOOL_NAME : null,
143
+ },
144
+ access: capsule.config.access,
145
+ storage: {
146
+ driver: capsule.config.storage.driver,
147
+ database,
148
+ files,
149
+ },
150
+ };
151
+ }
152
+
153
+ export function toCapsulePath(capsule: LoadedAgentCapsule, path: string): string {
154
+ const localPath = relative(capsule.root, path);
155
+ return localPath.length > 0 ? localPath : ".";
156
+ }
157
+
158
+ function buildDatabaseManifest(
159
+ capsule: LoadedAgentCapsule,
160
+ schema: ManifestSchema | null,
161
+ ): SharedAgentManifest["storage"]["database"] {
162
+ const database = capsule.config.storage.database;
163
+
164
+ if (!database || database.driver === "none") {
165
+ return { driver: "none" };
166
+ }
167
+
168
+ const schemaPath = database.schema ? resolve(capsule.root, database.schema) : null;
169
+
170
+ return {
171
+ driver: "turso",
172
+ schema: schemaPath ? toCapsulePath(capsule, schemaPath) : null,
173
+ schema_sha256: schema?.sha256 ?? null,
174
+ };
175
+ }
176
+
177
+ function buildFilesManifest(capsule: LoadedAgentCapsule): SharedAgentManifest["storage"]["files"] {
178
+ const files = capsule.config.storage.files;
179
+
180
+ if (!files || files.driver === "none") {
181
+ return { driver: "none" };
182
+ }
183
+
184
+ return {
185
+ driver: "object",
186
+ bucket: files.bucket ?? `agentkit-${slugify(capsule.config.name)}-files`,
187
+ prefix: normalizePrefix(files.prefix ?? capsule.config.name),
188
+ maxUploadBytes: files.maxUploadBytes ?? 10 * 1024 * 1024,
189
+ };
190
+ }
191
+
192
+ function slugify(value: string): string {
193
+ const slug = value
194
+ .toLowerCase()
195
+ .replace(/[^a-z0-9-]+/g, "-")
196
+ .replace(/^-+|-+$/g, "")
197
+ .slice(0, 48);
198
+
199
+ return slug || "agent";
200
+ }
201
+
202
+ function normalizePrefix(value: string): string {
203
+ return value
204
+ .split("/")
205
+ .map((part) =>
206
+ part
207
+ .toLowerCase()
208
+ .replace(/[^a-z0-9_-]+/g, "-")
209
+ .replace(/^-+|-+$/g, ""),
210
+ )
211
+ .filter(Boolean)
212
+ .join("/");
213
+ }
@@ -0,0 +1,133 @@
1
+ import { AgentKitError } from "../errors";
2
+
3
+ export const runtimeTargetIds = ["local", "cloudflare", "container", "vps"] as const;
4
+
5
+ export type RuntimeTargetId = (typeof runtimeTargetIds)[number];
6
+ export type RuntimeTargetCapabilityState = "available" | "planned" | "unsupported";
7
+ export type RuntimeStorageCapability = "agentkit" | "durable-object-sqlite" | "local-sqlite" | "postgres";
8
+ export type RuntimeFilesCapability = "none" | "r2" | "local-disk" | "s3-compatible";
9
+ export type RuntimeChannelCapability = "website" | "telegram" | "whatsapp";
10
+ export type RuntimeProviderSupport = "pi-local" | "edge-compatible" | "server-env";
11
+
12
+ export type RuntimeTargetCapabilities = {
13
+ build: RuntimeTargetCapabilityState;
14
+ deploy: RuntimeTargetCapabilityState;
15
+ dev: RuntimeTargetCapabilityState;
16
+ storage: readonly RuntimeStorageCapability[];
17
+ files: readonly RuntimeFilesCapability[];
18
+ channels: readonly RuntimeChannelCapability[];
19
+ providers: readonly RuntimeProviderSupport[];
20
+ };
21
+
22
+ export type RuntimeTarget = {
23
+ id: RuntimeTargetId;
24
+ label: string;
25
+ capabilities: RuntimeTargetCapabilities;
26
+ };
27
+
28
+ export const runtimeTargets: Readonly<Record<RuntimeTargetId, RuntimeTarget>> = {
29
+ local: {
30
+ id: "local",
31
+ label: "Local dev server",
32
+ capabilities: {
33
+ build: "unsupported",
34
+ deploy: "unsupported",
35
+ dev: "available",
36
+ storage: ["local-sqlite"],
37
+ files: ["local-disk"],
38
+ channels: ["website"],
39
+ providers: ["pi-local"],
40
+ },
41
+ },
42
+ cloudflare: {
43
+ id: "cloudflare",
44
+ label: "Cloudflare Worker",
45
+ capabilities: {
46
+ build: "available",
47
+ deploy: "available",
48
+ dev: "planned",
49
+ storage: ["agentkit", "durable-object-sqlite", "postgres"],
50
+ files: ["none", "r2"],
51
+ channels: ["website", "telegram", "whatsapp"],
52
+ providers: ["edge-compatible"],
53
+ },
54
+ },
55
+ container: {
56
+ id: "container",
57
+ label: "Container runtime",
58
+ capabilities: {
59
+ build: "available",
60
+ deploy: "planned",
61
+ dev: "planned",
62
+ storage: ["local-sqlite", "postgres"],
63
+ files: ["none", "local-disk", "s3-compatible"],
64
+ channels: ["website", "telegram", "whatsapp"],
65
+ providers: ["server-env"],
66
+ },
67
+ },
68
+ vps: {
69
+ id: "vps",
70
+ label: "VPS handoff",
71
+ capabilities: {
72
+ build: "unsupported",
73
+ deploy: "available",
74
+ dev: "unsupported",
75
+ storage: ["local-sqlite", "postgres"],
76
+ files: ["none", "local-disk", "s3-compatible"],
77
+ channels: ["website", "telegram", "whatsapp"],
78
+ providers: ["server-env"],
79
+ },
80
+ },
81
+ };
82
+
83
+ export function resolveRuntimeTarget(id: string): RuntimeTarget {
84
+ if (isRuntimeTargetId(id)) {
85
+ return runtimeTargets[id];
86
+ }
87
+
88
+ throw new AgentKitError(
89
+ "unsupported_target",
90
+ `Unsupported target "${id}". Supported targets: ${runtimeTargetIds.join(", ")}.`,
91
+ );
92
+ }
93
+
94
+ export function resolveBuildTarget(id = "cloudflare"): RuntimeTarget {
95
+ const target = resolveRuntimeTarget(id);
96
+
97
+ if (target.capabilities.build !== "available") {
98
+ throw unsupportedCapability("build", target.id, listTargetsWithCapability("build"));
99
+ }
100
+
101
+ return target;
102
+ }
103
+
104
+ export function resolveDeployTarget(id = "cloudflare"): RuntimeTarget {
105
+ const target = resolveRuntimeTarget(id);
106
+
107
+ if (target.capabilities.deploy !== "available") {
108
+ throw unsupportedCapability("deploy", target.id, listTargetsWithCapability("deploy"));
109
+ }
110
+
111
+ return target;
112
+ }
113
+
114
+ export function isRuntimeTargetId(id: string): id is RuntimeTargetId {
115
+ return Object.prototype.hasOwnProperty.call(runtimeTargets, id);
116
+ }
117
+
118
+ export function listTargetsWithCapability(
119
+ capability: "build" | "deploy" | "dev",
120
+ ): readonly RuntimeTargetId[] {
121
+ return runtimeTargetIds.filter((id) => runtimeTargets[id].capabilities[capability] === "available");
122
+ }
123
+
124
+ function unsupportedCapability(
125
+ capability: "build" | "deploy" | "dev",
126
+ id: RuntimeTargetId,
127
+ supportedTargets: readonly RuntimeTargetId[],
128
+ ): AgentKitError {
129
+ return new AgentKitError(
130
+ "unsupported_target",
131
+ `Unsupported ${capability} target "${id}". Supported ${capability} targets: ${supportedTargets.join(", ")}.`,
132
+ );
133
+ }
@@ -0,0 +1,256 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import type { Dirent } from "node:fs";
3
+ import { basename, relative, resolve } from "node:path";
4
+
5
+ import type { DatabaseArgs, DatabaseResult, DatabaseRunner, DatabaseStatement } from "../index";
6
+ import { openCapsuleStore, type SqliteAgentKitStore } from "../storage/sqlite";
7
+ import type { LoadedAgentCapsule } from "./config";
8
+ import { AgentKitError } from "./errors";
9
+
10
+ export type LocalDatabasePrepareResult = {
11
+ store: SqliteAgentKitStore;
12
+ databasePath: string;
13
+ migrations: {
14
+ configured: boolean;
15
+ path: string | null;
16
+ applied: string[];
17
+ skipped: string[];
18
+ };
19
+ schema: {
20
+ configured: boolean;
21
+ applied: boolean;
22
+ path: string | null;
23
+ };
24
+ };
25
+
26
+ export async function openPreparedCapsuleStore(capsule: LoadedAgentCapsule): Promise<LocalDatabasePrepareResult> {
27
+ const store = await openCapsuleStore(capsule);
28
+
29
+ try {
30
+ const migrations = await applyCapsuleDatabaseMigrations(capsule, store);
31
+ const schema = await applyCapsuleDatabaseSchema(capsule, store);
32
+ return {
33
+ store,
34
+ databasePath: store.dbPath,
35
+ migrations,
36
+ schema,
37
+ };
38
+ } catch (error) {
39
+ store.close();
40
+ throw error;
41
+ }
42
+ }
43
+
44
+ export async function applyCapsuleDatabaseMigrations(
45
+ capsule: LoadedAgentCapsule,
46
+ store: SqliteAgentKitStore,
47
+ ): Promise<LocalDatabasePrepareResult["migrations"]> {
48
+ const database = capsule.config.storage.database;
49
+
50
+ if (!database || database.driver !== "turso") {
51
+ return {
52
+ configured: false,
53
+ path: null,
54
+ applied: [],
55
+ skipped: [],
56
+ };
57
+ }
58
+
59
+ const migrationsPath = resolve(capsule.root, database.migrations ?? "migrations");
60
+ const migrations = await readMigrationFiles(migrationsPath);
61
+
62
+ if (migrations.length === 0) {
63
+ return {
64
+ configured: false,
65
+ path: migrationsPath,
66
+ applied: [],
67
+ skipped: [],
68
+ };
69
+ }
70
+
71
+ try {
72
+ const result = store.applyApplicationMigrations(migrations);
73
+ return {
74
+ configured: true,
75
+ path: migrationsPath,
76
+ applied: result.applied,
77
+ skipped: result.skipped,
78
+ };
79
+ } catch (error) {
80
+ if (error instanceof AgentKitError && error.code === "database_schema_error") {
81
+ throw new AgentKitError(
82
+ "database_schema_error",
83
+ [
84
+ `Could not apply database migrations from ${relative(capsule.root, migrationsPath)}.`,
85
+ "Migrations must be ordered .sql files and should use safe, forward-only changes.",
86
+ error.message,
87
+ ].join(" "),
88
+ { cause: error },
89
+ );
90
+ }
91
+
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ async function readMigrationFiles(directory: string): Promise<Array<{ id: string; sql: string }>> {
97
+ let entries: Dirent[];
98
+
99
+ try {
100
+ entries = await readdir(directory, { withFileTypes: true });
101
+ } catch (error) {
102
+ if (isNodeError(error) && error.code === "ENOENT") {
103
+ return [];
104
+ }
105
+
106
+ throw error;
107
+ }
108
+
109
+ const files = entries
110
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".sql"))
111
+ .map((entry) => entry.name)
112
+ .sort();
113
+
114
+ return Promise.all(
115
+ files.map(async (file) => ({
116
+ id: basename(file, ".sql"),
117
+ sql: await readFile(resolve(directory, file), "utf8"),
118
+ })),
119
+ );
120
+ }
121
+
122
+ export async function applyCapsuleDatabaseSchema(
123
+ capsule: LoadedAgentCapsule,
124
+ store: SqliteAgentKitStore,
125
+ ): Promise<LocalDatabasePrepareResult["schema"]> {
126
+ const database = capsule.config.storage.database;
127
+
128
+ if (!database || database.driver !== "turso" || !database.schema) {
129
+ return {
130
+ configured: false,
131
+ applied: false,
132
+ path: null,
133
+ };
134
+ }
135
+
136
+ const schemaPath = resolve(capsule.root, database.schema);
137
+ let schema: string;
138
+
139
+ try {
140
+ schema = await readFile(schemaPath, "utf8");
141
+ } catch (error) {
142
+ throw new AgentKitError(
143
+ "database_schema_error",
144
+ `Could not read storage.database.schema at ${relative(capsule.root, schemaPath)}. Check storage.database.schema in agentkit.config.ts.`,
145
+ { cause: error },
146
+ );
147
+ }
148
+
149
+ try {
150
+ store.applyApplicationSchema(schema);
151
+ } catch (error) {
152
+ if (error instanceof AgentKitError && error.code === "database_schema_error") {
153
+ throw new AgentKitError(
154
+ "database_schema_error",
155
+ [
156
+ `Could not apply storage.database.schema at ${relative(capsule.root, schemaPath)}.`,
157
+ "schema.sql is a v1 bootstrap file and should be idempotent: use CREATE TABLE IF NOT EXISTS, CREATE INDEX IF NOT EXISTS, and only safe additive ALTER TABLE statements.",
158
+ "AgentKit does not run destructive schema changes or versioned migrations automatically in v1.",
159
+ error.message,
160
+ ].join(" "),
161
+ { cause: error },
162
+ );
163
+ }
164
+
165
+ throw error;
166
+ }
167
+
168
+ return {
169
+ configured: true,
170
+ applied: true,
171
+ path: schemaPath,
172
+ };
173
+ }
174
+
175
+ export function createLocalDatabaseRunner(store: SqliteAgentKitStore): DatabaseRunner {
176
+ const run = async (sql: string, args: DatabaseArgs = []): Promise<DatabaseResult> => {
177
+ validateSql(sql, args);
178
+ return store.executeApplicationSql(sql, args);
179
+ };
180
+
181
+ return Object.assign(run, {
182
+ execute: run,
183
+ query: run,
184
+ async transaction<T>(callback: (tx: DatabaseRunner) => Promise<T> | T): Promise<T> {
185
+ if (typeof callback !== "function") {
186
+ throw new AgentKitError("database_query_invalid", "ctx.db.transaction requires a callback.");
187
+ }
188
+
189
+ const tx = createLocalTransactionRunner(store);
190
+ return await Promise.resolve(store.runApplicationTransaction(() => callback(tx)));
191
+ },
192
+ async batch(statements: DatabaseStatement[]): Promise<DatabaseResult[]> {
193
+ validateStatements(statements);
194
+ return store.executeApplicationBatch(statements);
195
+ },
196
+ });
197
+ }
198
+
199
+ function createLocalTransactionRunner(store: SqliteAgentKitStore): DatabaseRunner {
200
+ const run = async (sql: string, args: DatabaseArgs = []): Promise<DatabaseResult> => {
201
+ validateSql(sql, args);
202
+ return store.executeApplicationSql(sql, args);
203
+ };
204
+
205
+ return Object.assign(run, {
206
+ execute: run,
207
+ query: run,
208
+ transaction(): Promise<never> {
209
+ throw new AgentKitError(
210
+ "database_transaction_unsupported",
211
+ "Nested database transactions are not supported. Use one ctx.db.transaction callback or ctx.db.batch call.",
212
+ );
213
+ },
214
+ async batch(statements: DatabaseStatement[]): Promise<DatabaseResult[]> {
215
+ validateStatements(statements);
216
+ return statements.map((statement) => store.executeApplicationSql(statement.sql, statement.args ?? []));
217
+ },
218
+ });
219
+ }
220
+
221
+ function validateSql(sql: string, args: DatabaseArgs): void {
222
+ if (typeof sql !== "string" || sql.trim().length === 0) {
223
+ throw new AgentKitError("database_query_invalid", "Database SQL must be a non-empty string.");
224
+ }
225
+
226
+ if (!Array.isArray(args)) {
227
+ throw new AgentKitError("database_query_invalid", "Database SQL args must be an array.");
228
+ }
229
+
230
+ for (const arg of args) {
231
+ if (arg !== null && typeof arg !== "string" && typeof arg !== "number" && typeof arg !== "boolean") {
232
+ throw new AgentKitError(
233
+ "database_query_invalid",
234
+ "Database SQL args may only contain strings, numbers, booleans, or null.",
235
+ );
236
+ }
237
+ }
238
+ }
239
+
240
+ function validateStatements(statements: DatabaseStatement[]): void {
241
+ if (!Array.isArray(statements)) {
242
+ throw new AgentKitError("database_query_invalid", "ctx.db.batch requires an array of statements.");
243
+ }
244
+
245
+ for (const [index, statement] of statements.entries()) {
246
+ if (!statement || typeof statement !== "object") {
247
+ throw new AgentKitError("database_query_invalid", `ctx.db.batch statement ${index} must be an object.`);
248
+ }
249
+
250
+ validateSql(statement.sql, statement.args ?? []);
251
+ }
252
+ }
253
+
254
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
255
+ return typeof error === "object" && error !== null && "code" in error;
256
+ }