@andreprado/agentkit 0.1.0-alpha.15 → 0.1.0-alpha.17

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 (41) hide show
  1. package/README.md +8 -0
  2. package/docs/guides/add-channel.md +65 -0
  3. package/docs/guides/add-managed-composio.md +137 -0
  4. package/docs/guides/channel-security.md +32 -0
  5. package/docs/guides/channels-production-handoff.md +1 -1
  6. package/docs/guides/connect-telegram.md +60 -0
  7. package/docs/guides/connect-whatsapp-zapster.md +65 -0
  8. package/docs/guides/prepare-deploy.md +3 -1
  9. package/docs/llms-full.txt +13 -1
  10. package/docs/llms.txt +7 -0
  11. package/package.json +1 -1
  12. package/src/cli/commands/channels.ts +121 -7
  13. package/src/cli/commands/transcribe.ts +171 -0
  14. package/src/cli/deploy-readiness.ts +59 -3
  15. package/src/cli/help.ts +15 -0
  16. package/src/cli/index.ts +130 -0
  17. package/src/create-project.ts +4 -32
  18. package/src/index.ts +231 -0
  19. package/src/runtime/channel-test-harness.ts +2 -0
  20. package/src/runtime/channels/telegram.ts +326 -10
  21. package/src/runtime/channels/whatsapp-zapster.ts +319 -0
  22. package/src/runtime/channels.ts +47 -1
  23. package/src/runtime/chat.ts +2 -1
  24. package/src/runtime/config.ts +214 -4
  25. package/src/runtime/core/manifest.ts +61 -2
  26. package/src/runtime/deploy-readiness.ts +31 -1
  27. package/src/runtime/dev-server.ts +72 -4
  28. package/src/runtime/inspect.ts +142 -4
  29. package/src/runtime/integrations/composio.ts +257 -0
  30. package/src/runtime/skills.ts +95 -0
  31. package/src/runtime/targets/cloudflare/build.ts +162 -5
  32. package/src/runtime/tool-runner.ts +2 -1
  33. package/src/runtime/transcription.ts +483 -0
  34. package/src/templates/skills/agentkit-capsule/SKILL.md +1 -0
  35. package/src/templates/skills/agentkit-channels/SKILL.md +36 -1
  36. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +16 -1
  37. package/src/templates/skills/agentkit-channels/references/telegram.md +34 -0
  38. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +29 -0
  39. package/src/templates/skills/agentkit-deploy/SKILL.md +1 -1
  40. package/src/templates/skills/agentkit-integrations/SKILL.md +66 -0
  41. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +3 -2
@@ -4,9 +4,16 @@ import type { AgentChannel, AgentProvider, AgentRuntime, AccessMode } from "../i
4
4
  import type { LoadedAgentCapsule } from "./config";
5
5
  import { loadAgentCapsule } from "./config";
6
6
  import { loadCapsuleEnv } from "./env";
7
+ import {
8
+ createManagedComposioTools,
9
+ managedComposioRequiredSecrets,
10
+ resolveManagedComposioIntegrations,
11
+ } from "./integrations/composio";
7
12
  import { resolveKnowledgeConfig } from "./knowledge/config";
8
13
  import { KNOWLEDGE_SEARCH_TOOL_NAME } from "./knowledge/tool";
9
14
  import { resolveRuntimeTimeZone } from "./prompt-context";
15
+ import { checkDefaultSkillStatus, type DefaultSkillStatus } from "./skills";
16
+ import { resolveTranscriptionConfig } from "./transcription";
10
17
 
11
18
  export type SecretState = "set" | "missing";
12
19
 
@@ -20,7 +27,21 @@ export type AgentInspectState = {
20
27
  };
21
28
  prompt: string;
22
29
  tools: string[];
30
+ integrations: AgentInspectIntegration[];
23
31
  channels: AgentInspectChannel[];
32
+ transcription: {
33
+ enabled: boolean;
34
+ provider: string | null;
35
+ model: string | null;
36
+ secret: string | null;
37
+ language: string | null;
38
+ prompt: string | null;
39
+ limits: {
40
+ maxDurationSeconds: number | null;
41
+ maxBytes: number | null;
42
+ };
43
+ rawAudioTtlSeconds: number | null;
44
+ };
24
45
  knowledge: {
25
46
  enabled: boolean;
26
47
  sources: string[];
@@ -57,6 +78,8 @@ export type AgentInspectState = {
57
78
  secrets: Record<string, SecretState>;
58
79
  managedSecrets: string[];
59
80
  userSecrets: string[];
81
+ skills: AgentInspectSkills;
82
+ warnings: AgentInspectWarning[];
60
83
  };
61
84
 
62
85
  export type AgentInspectDatabase = {
@@ -80,6 +103,30 @@ export type AgentInspectChannel = {
80
103
  provider: AgentChannel["provider"];
81
104
  secrets: string[];
82
105
  buffer?: AgentChannel["buffer"];
106
+ audio?: AgentChannel["audio"];
107
+ };
108
+
109
+ export type AgentInspectSkills = {
110
+ status: "current" | "missing" | "outdated" | "not_checked";
111
+ checked: number;
112
+ missing: string[];
113
+ outdated: string[];
114
+ syncCommand: string | null;
115
+ };
116
+
117
+ export type AgentInspectWarning = {
118
+ code: string;
119
+ message: string;
120
+ command?: string;
121
+ };
122
+
123
+ export type AgentInspectIntegration = {
124
+ name: string;
125
+ provider: "composio";
126
+ mode: "managed";
127
+ toolkits: string[];
128
+ tools: string[];
129
+ userId: string;
83
130
  };
84
131
 
85
132
  export async function inspectAgentCapsule(
@@ -87,17 +134,20 @@ export async function inspectAgentCapsule(
87
134
  env: Record<string, string | undefined> = process.env,
88
135
  ): Promise<AgentInspectState> {
89
136
  const capsule = await loadAgentCapsule(cwd);
90
- return buildInspectState(capsule, await loadCapsuleEnv(capsule.root, env));
137
+ return buildInspectState(capsule, await loadCapsuleEnv(capsule.root, env), await checkDefaultSkillStatus(capsule.root));
91
138
  }
92
139
 
93
140
  export function buildInspectState(
94
141
  capsule: LoadedAgentCapsule,
95
142
  env: Record<string, string | undefined> = process.env,
143
+ skillStatus: DefaultSkillStatus | null = null,
96
144
  ): AgentInspectState {
97
145
  const declaredSecrets = new Set(capsule.config.secrets);
98
146
  const knowledge = resolveKnowledgeConfig(capsule.config);
147
+ const integrationTools = createManagedComposioTools(capsule.config);
148
+ const transcription = resolveTranscriptionConfig(capsule.config.transcription);
99
149
 
100
- for (const tool of capsule.config.tools ?? []) {
150
+ for (const tool of [...integrationTools, ...(capsule.config.tools ?? [])]) {
101
151
  for (const secret of tool.secrets ?? []) {
102
152
  declaredSecrets.add(secret);
103
153
  }
@@ -113,8 +163,15 @@ export function buildInspectState(
113
163
  declaredSecrets.add(knowledge.embedding.secret);
114
164
  }
115
165
 
166
+ if (transcription.secret) {
167
+ declaredSecrets.add(transcription.secret);
168
+ }
169
+
116
170
  const managedSecrets = managedCloudSecrets(capsule);
117
- const userSecrets = Array.from(declaredSecrets).sort();
171
+ const managedSecretSet = new Set(managedSecrets);
172
+ const userSecrets = Array.from(declaredSecrets)
173
+ .filter((secret) => !managedSecretSet.has(secret))
174
+ .sort();
118
175
  const secrets: Record<string, SecretState> = {};
119
176
 
120
177
  for (const secret of userSecrets) {
@@ -123,6 +180,9 @@ export function buildInspectState(
123
180
 
124
181
  const database = inspectDatabase(capsule);
125
182
 
183
+ const skills = inspectSkills(skillStatus);
184
+ const warnings = inspectWarnings(capsule, transcription, skills);
185
+
126
186
  return {
127
187
  agent: capsule.config.name,
128
188
  runtime: capsule.config.runtime,
@@ -135,14 +195,36 @@ export function buildInspectState(
135
195
  }),
136
196
  },
137
197
  prompt: toCapsulePath(capsule.root, capsule.instructionsPath),
138
- tools: (capsule.config.tools ?? []).map((tool) => tool.name),
198
+ tools: [...integrationTools, ...(capsule.config.tools ?? [])].map((tool) => tool.name),
199
+ integrations: resolveManagedComposioIntegrations(capsule.config).map((integration) => ({
200
+ name: integration.name,
201
+ provider: integration.provider,
202
+ mode: integration.mode,
203
+ toolkits: integration.toolkits,
204
+ tools: integration.allowedTools,
205
+ userId: integration.userId,
206
+ })),
139
207
  channels: (capsule.config.channels ?? []).map((channel) => ({
140
208
  name: channel.name,
141
209
  type: channel.type,
142
210
  provider: channel.provider,
143
211
  secrets: [...channel.secrets],
144
212
  ...(channel.buffer ? { buffer: channel.buffer } : {}),
213
+ ...(channel.audio ? { audio: channel.audio } : {}),
145
214
  })),
215
+ transcription: {
216
+ enabled: transcription.enabled,
217
+ provider: transcription.enabled ? transcription.provider : null,
218
+ model: transcription.enabled ? transcription.model : null,
219
+ secret: transcription.secret,
220
+ language: transcription.language,
221
+ prompt: transcription.prompt,
222
+ limits: {
223
+ maxDurationSeconds: transcription.limits.maxDurationSeconds ?? null,
224
+ maxBytes: transcription.limits.maxBytes ?? null,
225
+ },
226
+ rawAudioTtlSeconds: transcription.rawAudioTtlSeconds,
227
+ },
146
228
  knowledge: {
147
229
  enabled: knowledge.enabled,
148
230
  sources: knowledge.sources.map((source) => source.value),
@@ -174,6 +256,8 @@ export function buildInspectState(
174
256
  secrets,
175
257
  managedSecrets,
176
258
  userSecrets,
259
+ skills,
260
+ warnings,
177
261
  };
178
262
  }
179
263
 
@@ -203,6 +287,56 @@ function inspectDatabase(capsule: LoadedAgentCapsule): AgentInspectDatabase {
203
287
  };
204
288
  }
205
289
 
290
+ function inspectSkills(status: DefaultSkillStatus | null): AgentInspectSkills {
291
+ if (!status) {
292
+ return {
293
+ status: "not_checked",
294
+ checked: 0,
295
+ missing: [],
296
+ outdated: [],
297
+ syncCommand: null,
298
+ };
299
+ }
300
+
301
+ return {
302
+ status: status.status,
303
+ checked: status.checked,
304
+ missing: status.missing,
305
+ outdated: status.outdated,
306
+ syncCommand: status.status === "current" ? null : "npm run agentkit -- skills sync",
307
+ };
308
+ }
309
+
310
+ function inspectWarnings(
311
+ capsule: LoadedAgentCapsule,
312
+ transcription: ReturnType<typeof resolveTranscriptionConfig>,
313
+ skills: AgentInspectSkills,
314
+ ): AgentInspectWarning[] {
315
+ const warnings: AgentInspectWarning[] = [];
316
+
317
+ if (skills.status === "missing" || skills.status === "outdated") {
318
+ warnings.push({
319
+ code: "local_skills_outdated",
320
+ message: "Local AgentKit skills are older than or missing from the installed package docs.",
321
+ command: "npm run agentkit -- skills sync",
322
+ });
323
+ }
324
+
325
+ if (
326
+ transcription.enabled &&
327
+ transcription.provider === "openai" &&
328
+ (capsule.config.channels ?? []).some((channel) => channel.type === "telegram" && channel.audio?.mode === "transcribe")
329
+ ) {
330
+ warnings.push({
331
+ code: "telegram_audio_openai_format",
332
+ message: "Telegram voice notes arrive as OGG/Opus; use Groq for the default Telegram voice-note transcription path.",
333
+ command: "Set transcription.provider to \"groq\" and secret to \"GROQ_API_KEY\".",
334
+ });
335
+ }
336
+
337
+ return warnings;
338
+ }
339
+
206
340
  function managedCloudSecrets(capsule: LoadedAgentCapsule): string[] {
207
341
  const secrets = new Set<string>();
208
342
 
@@ -211,5 +345,9 @@ function managedCloudSecrets(capsule: LoadedAgentCapsule): string[] {
211
345
  secrets.add("TURSO_DATABASE_URL");
212
346
  }
213
347
 
348
+ for (const secret of managedComposioRequiredSecrets(capsule.config)) {
349
+ secrets.add(secret);
350
+ }
351
+
214
352
  return Array.from(secrets).sort();
215
353
  }
@@ -0,0 +1,257 @@
1
+ import type { AgentConfig, AgentTool, ComposioManagedIntegration } from "../../index";
2
+ import { AgentKitError } from "../errors";
3
+
4
+ export const MANAGED_COMPOSIO_TOOL_NAME = "agentkit_composio_execute";
5
+ export const MANAGED_COMPOSIO_API_KEY_SECRET = "COMPOSIO_API_KEY";
6
+ export const MANAGED_COMPOSIO_WEBHOOK_SECRET = "COMPOSIO_WEBHOOK_SECRET";
7
+ export const MANAGED_COMPOSIO_ENTITLEMENT = "managed_composio";
8
+ export const MANAGED_COMPOSIO_USER_ID_BINDING = "AGENTKIT_MANAGED_COMPOSIO_USER_ID";
9
+
10
+ export type ResolvedComposioManagedIntegration = ComposioManagedIntegration & {
11
+ userId: string;
12
+ allowedTools: string[];
13
+ };
14
+
15
+ export type ManagedComposioResolutionOptions = {
16
+ userId?: string;
17
+ userIdScope?: string;
18
+ };
19
+
20
+ type ManagedComposioToolInput = {
21
+ toolSlug: string;
22
+ arguments: Record<string, unknown>;
23
+ };
24
+
25
+ type ManagedComposioToolOutput = {
26
+ successful: boolean;
27
+ data: unknown;
28
+ error: unknown;
29
+ logId: string | null;
30
+ toolkit: string;
31
+ toolSlug: string;
32
+ userId: string;
33
+ };
34
+
35
+ export function resolveManagedComposioIntegrations(
36
+ config: Pick<AgentConfig, "name" | "integrations">,
37
+ options: ManagedComposioResolutionOptions = {},
38
+ ): ResolvedComposioManagedIntegration[] {
39
+ return (config.integrations ?? [])
40
+ .filter((integration): integration is ComposioManagedIntegration => integration.provider === "composio" && integration.mode === "managed")
41
+ .map((integration) => ({
42
+ ...integration,
43
+ userId: options.userId ?? managedComposioUserId(config.name, options.userIdScope),
44
+ allowedTools: allowedComposioTools(integration),
45
+ }));
46
+ }
47
+
48
+ export function hasManagedComposioIntegration(config: Pick<AgentConfig, "name" | "integrations">): boolean {
49
+ return resolveManagedComposioIntegrations(config).length > 0;
50
+ }
51
+
52
+ export function managedComposioRequiredSecrets(config: Pick<AgentConfig, "name" | "integrations">): string[] {
53
+ return hasManagedComposioIntegration(config) ? [MANAGED_COMPOSIO_API_KEY_SECRET] : [];
54
+ }
55
+
56
+ export function createManagedComposioTools(
57
+ config: Pick<AgentConfig, "name" | "integrations">,
58
+ options: ManagedComposioResolutionOptions = {},
59
+ ): AgentTool[] {
60
+ const integrations = resolveManagedComposioIntegrations(config, options);
61
+
62
+ return integrations.flatMap((integration) => {
63
+ if (integration.allowedTools.length === 0) {
64
+ return [];
65
+ }
66
+
67
+ const toolkitByTool = new Map<string, string>();
68
+
69
+ for (const [toolkit, tools] of Object.entries(integration.tools ?? {})) {
70
+ for (const tool of tools) {
71
+ toolkitByTool.set(tool, toolkit);
72
+ }
73
+ }
74
+
75
+ const tool: AgentTool<ManagedComposioToolInput, ManagedComposioToolOutput> = {
76
+ name: MANAGED_COMPOSIO_TOOL_NAME,
77
+ description:
78
+ "Executes one explicitly allowed AgentKit-managed Composio action for this deployed agent. Use only for configured connected apps.",
79
+ visibility: "user",
80
+ secrets: [MANAGED_COMPOSIO_API_KEY_SECRET],
81
+ permissions: integration.allowedTools.map((toolName) => `composio:${toolName.toLowerCase()}`),
82
+ timeoutMs: 60_000,
83
+ inputSchema: {
84
+ type: "object",
85
+ properties: {
86
+ toolSlug: {
87
+ type: "string",
88
+ enum: integration.allowedTools,
89
+ description: "Allowed Composio action slug to execute.",
90
+ },
91
+ arguments: {
92
+ type: "object",
93
+ additionalProperties: true,
94
+ description: "Structured arguments for the selected Composio action.",
95
+ },
96
+ },
97
+ required: ["toolSlug", "arguments"],
98
+ additionalProperties: false,
99
+ },
100
+ outputSchema: {
101
+ type: "object",
102
+ properties: {
103
+ successful: { type: "boolean" },
104
+ data: { type: ["object", "array", "string", "number", "boolean", "null"] },
105
+ error: { type: ["object", "string", "null"] },
106
+ logId: { type: ["string", "null"] },
107
+ toolkit: { type: "string" },
108
+ toolSlug: { type: "string" },
109
+ userId: { type: "string" },
110
+ },
111
+ required: ["successful", "toolkit", "toolSlug", "userId"],
112
+ additionalProperties: true,
113
+ },
114
+ async execute(input, ctx) {
115
+ const toolkit = toolkitByTool.get(input.toolSlug);
116
+
117
+ if (!toolkit || !integration.allowedTools.includes(input.toolSlug)) {
118
+ throw new AgentKitError(
119
+ "composio_tool_not_allowed",
120
+ `Composio action "${input.toolSlug}" is not allowed by this agent's managed Composio settings.`,
121
+ );
122
+ }
123
+
124
+ const result = await executeComposioTool({
125
+ apiKey: ctx.secrets[MANAGED_COMPOSIO_API_KEY_SECRET],
126
+ userId: integration.userId,
127
+ toolSlug: input.toolSlug,
128
+ arguments: input.arguments,
129
+ signal: ctx.signal,
130
+ });
131
+
132
+ return {
133
+ successful: result.successful,
134
+ data: result.data ?? null,
135
+ error: result.error ?? null,
136
+ logId: result.log_id ?? null,
137
+ toolkit,
138
+ toolSlug: input.toolSlug,
139
+ userId: integration.userId,
140
+ };
141
+ },
142
+ render(output) {
143
+ if (output.successful) {
144
+ return `Composio action ${output.toolSlug} completed.`;
145
+ }
146
+
147
+ return {
148
+ text: `Composio action ${output.toolSlug} did not complete successfully.`,
149
+ blocks: [
150
+ {
151
+ type: "card",
152
+ title: "Composio connection or action issue",
153
+ subtitle: typeof output.error === "string" ? output.error : undefined,
154
+ fields: [
155
+ { label: "Toolkit", value: output.toolkit },
156
+ { label: "Action", value: output.toolSlug },
157
+ ],
158
+ },
159
+ ],
160
+ };
161
+ },
162
+ };
163
+
164
+ return [tool];
165
+ });
166
+ }
167
+
168
+ export function managedComposioUserId(agentName: string, scope = "local"): string {
169
+ return `ak_agent_${stableHash(`${scope}:${agentName}`).slice(0, 24)}`;
170
+ }
171
+
172
+ function allowedComposioTools(integration: ComposioManagedIntegration): string[] {
173
+ return Array.from(new Set(Object.values(integration.tools ?? {}).flat())).sort();
174
+ }
175
+
176
+ async function executeComposioTool(input: {
177
+ apiKey: string | undefined;
178
+ userId: string;
179
+ toolSlug: string;
180
+ arguments: Record<string, unknown>;
181
+ signal: AbortSignal;
182
+ }): Promise<{ successful: boolean; data?: unknown; error?: unknown; log_id?: string }> {
183
+ if (!input.apiKey) {
184
+ throw new AgentKitError("composio_secret_missing", "AgentKit-managed Composio requires COMPOSIO_API_KEY at runtime.");
185
+ }
186
+
187
+ const response = await fetch(
188
+ `https://backend.composio.dev/api/v3.1/tools/execute/${encodeURIComponent(input.toolSlug)}`,
189
+ {
190
+ method: "POST",
191
+ headers: {
192
+ "content-type": "application/json",
193
+ "x-api-key": input.apiKey,
194
+ },
195
+ body: JSON.stringify({
196
+ user_id: input.userId,
197
+ arguments: input.arguments,
198
+ }),
199
+ signal: input.signal,
200
+ },
201
+ );
202
+ const payload = await response.json().catch(() => null);
203
+
204
+ if (!response.ok) {
205
+ throw new AgentKitError(
206
+ "composio_execution_failed",
207
+ `Composio action ${input.toolSlug} failed with HTTP ${response.status}: ${readComposioError(payload)}`,
208
+ );
209
+ }
210
+
211
+ if (!isRecord(payload)) {
212
+ return { successful: true, data: payload };
213
+ }
214
+
215
+ return {
216
+ successful: payload.successful !== false,
217
+ data: payload.data,
218
+ error: payload.error,
219
+ log_id: typeof payload.log_id === "string" ? payload.log_id : undefined,
220
+ };
221
+ }
222
+
223
+ function readComposioError(payload: unknown): string {
224
+ if (isRecord(payload)) {
225
+ if (typeof payload.error === "string") {
226
+ return payload.error;
227
+ }
228
+
229
+ if (isRecord(payload.error) && typeof payload.error.message === "string") {
230
+ return payload.error.message;
231
+ }
232
+ }
233
+
234
+ return "unknown error";
235
+ }
236
+
237
+ function stableHash(value: string): string {
238
+ let first = 2166136261;
239
+ let second = 0x811c9dc5;
240
+ let third = 0x01000193;
241
+
242
+ for (let index = 0; index < value.length; index += 1) {
243
+ const code = value.charCodeAt(index);
244
+ first ^= code;
245
+ first = Math.imul(first, 16777619);
246
+ second ^= code + index;
247
+ second = Math.imul(second, 1597334677);
248
+ third ^= code * (index + 1);
249
+ third = Math.imul(third, 2246822519);
250
+ }
251
+
252
+ return [first, second, third].map((hash) => (hash >>> 0).toString(16).padStart(8, "0")).join("");
253
+ }
254
+
255
+ function isRecord(value: unknown): value is Record<string, unknown> {
256
+ return typeof value === "object" && value !== null && !Array.isArray(value);
257
+ }
@@ -0,0 +1,95 @@
1
+ import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
2
+ import { dirname, join, relative } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ export type DefaultSkillFile = {
6
+ path: string;
7
+ contents: string;
8
+ };
9
+
10
+ export type DefaultSkillStatus = {
11
+ status: "current" | "missing" | "outdated";
12
+ checked: number;
13
+ missing: string[];
14
+ outdated: string[];
15
+ };
16
+
17
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
18
+
19
+ export async function syncDefaultSkillFiles(projectPath: string): Promise<{ files: string[] }> {
20
+ const files = await loadDefaultSkillFiles();
21
+
22
+ for (const file of files) {
23
+ const absolutePath = join(projectPath, file.path);
24
+
25
+ await mkdir(dirname(absolutePath), { recursive: true });
26
+ await writeFile(absolutePath, file.contents);
27
+ }
28
+
29
+ return { files: files.map((file) => file.path) };
30
+ }
31
+
32
+ export async function checkDefaultSkillStatus(projectPath: string): Promise<DefaultSkillStatus> {
33
+ const files = await loadDefaultSkillFiles();
34
+ const missing: string[] = [];
35
+ const outdated: string[] = [];
36
+
37
+ for (const file of files) {
38
+ const absolutePath = join(projectPath, file.path);
39
+
40
+ if (!(await pathExists(absolutePath))) {
41
+ missing.push(file.path);
42
+ continue;
43
+ }
44
+
45
+ const current = await readFile(absolutePath, "utf8");
46
+ if (current !== file.contents) {
47
+ outdated.push(file.path);
48
+ }
49
+ }
50
+
51
+ return {
52
+ status: missing.length > 0 ? "missing" : outdated.length > 0 ? "outdated" : "current",
53
+ checked: files.length,
54
+ missing,
55
+ outdated,
56
+ };
57
+ }
58
+
59
+ export async function loadDefaultSkillFiles(): Promise<DefaultSkillFile[]> {
60
+ const sourceRoot = join(moduleDir, "..", "templates", "skills");
61
+ const sourceFiles = await listFiles(sourceRoot);
62
+
63
+ return Promise.all(
64
+ sourceFiles.map(async (sourcePath) => ({
65
+ path: join("skills", relative(sourceRoot, sourcePath)).replaceAll("\\", "/"),
66
+ contents: await readFile(sourcePath, "utf8"),
67
+ })),
68
+ );
69
+ }
70
+
71
+ async function listFiles(directory: string): Promise<string[]> {
72
+ const entries = await readdir(directory, { withFileTypes: true });
73
+ const files = await Promise.all(
74
+ entries.map(async (entry) => {
75
+ const path = join(directory, entry.name);
76
+
77
+ if (entry.isDirectory()) {
78
+ return listFiles(path);
79
+ }
80
+
81
+ return entry.isFile() ? [path] : [];
82
+ }),
83
+ );
84
+
85
+ return files.flat().sort();
86
+ }
87
+
88
+ async function pathExists(path: string): Promise<boolean> {
89
+ try {
90
+ await stat(path);
91
+ return true;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }