@andreprado/agentkit 0.1.0-alpha.9 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +18 -1
  2. package/docs/guides/add-channel.md +251 -7
  3. package/docs/guides/add-knowledge.md +10 -0
  4. package/docs/guides/add-managed-composio.md +165 -0
  5. package/docs/guides/add-tool.md +10 -3
  6. package/docs/guides/channel-security.md +162 -32
  7. package/docs/guides/connect-discord.md +178 -0
  8. package/docs/guides/connect-slack.md +126 -0
  9. package/docs/guides/connect-telegram.md +61 -1
  10. package/docs/guides/connect-whatsapp-evolution.md +121 -0
  11. package/docs/guides/connect-whatsapp-uazapi.md +139 -0
  12. package/docs/guides/connect-whatsapp-zapster.md +119 -16
  13. package/docs/guides/create-agent.md +31 -4
  14. package/docs/guides/debug-channel.md +159 -0
  15. package/docs/guides/improve-from-production.md +151 -0
  16. package/docs/guides/prepare-deploy.md +32 -14
  17. package/docs/guides/replay-production-traces.md +72 -0
  18. package/docs/guides/run-evals.md +95 -25
  19. package/docs/guides/security-rules.md +9 -5
  20. package/docs/guides/send-feedback.md +135 -0
  21. package/docs/guides/use-jev.md +67 -0
  22. package/docs/guides/use-provider.md +70 -3
  23. package/docs/llms-full.txt +295 -25
  24. package/docs/llms.txt +54 -7
  25. package/package.json +3 -7
  26. package/src/cli/args.ts +23 -2
  27. package/src/cli/cloud-client.ts +121 -9
  28. package/src/cli/commands/channels.ts +856 -36
  29. package/src/cli/commands/feedback.ts +438 -0
  30. package/src/cli/commands/provider.ts +47 -0
  31. package/src/cli/commands/transcribe.ts +171 -0
  32. package/src/cli/deploy-chat-ui.ts +232 -18
  33. package/src/cli/deploy-readiness.ts +227 -14
  34. package/src/cli/help.ts +67 -9
  35. package/src/cli/index.ts +740 -35
  36. package/src/cli/new-command.ts +41 -0
  37. package/src/cloud/client.ts +4 -3
  38. package/src/cloud/contracts.ts +1 -1
  39. package/src/create-project.ts +18 -35
  40. package/src/index.ts +565 -11
  41. package/src/providers/codex-auth.ts +111 -0
  42. package/src/providers/pi.ts +88 -19
  43. package/src/providers/test.ts +36 -0
  44. package/src/providers/types.ts +8 -0
  45. package/src/runtime/channel-test-harness.ts +21 -1
  46. package/src/runtime/channels/discord.ts +896 -0
  47. package/src/runtime/channels/generic-webhook.ts +974 -0
  48. package/src/runtime/channels/slack.ts +646 -0
  49. package/src/runtime/channels/telegram.ts +343 -12
  50. package/src/runtime/channels/whatsapp-evolution.ts +1357 -0
  51. package/src/runtime/channels/whatsapp-meta.ts +9 -0
  52. package/src/runtime/channels/whatsapp-uazapi.ts +1323 -0
  53. package/src/runtime/channels/whatsapp-zapster.ts +674 -40
  54. package/src/runtime/channels.ts +83 -3
  55. package/src/runtime/chat.ts +70 -44
  56. package/src/runtime/config.ts +489 -20
  57. package/src/runtime/core/manifest.ts +75 -5
  58. package/src/runtime/core/targets.ts +5 -5
  59. package/src/runtime/deploy-readiness.ts +34 -4
  60. package/src/runtime/dev-server.ts +639 -39
  61. package/src/runtime/env.ts +8 -3
  62. package/src/runtime/evals.ts +445 -74
  63. package/src/runtime/improve.ts +868 -0
  64. package/src/runtime/inspect.ts +173 -4
  65. package/src/runtime/integrations/composio.ts +425 -0
  66. package/src/runtime/knowledge/retrieve.ts +25 -5
  67. package/src/runtime/knowledge/schema.ts +45 -1
  68. package/src/runtime/prompt-context.ts +141 -0
  69. package/src/runtime/runtime-contract.ts +71 -7
  70. package/src/runtime/skills.ts +95 -0
  71. package/src/runtime/targets/cloudflare/build.ts +1010 -208
  72. package/src/runtime/targets/container/server.ts +1 -1
  73. package/src/runtime/targets/vps/deploy.ts +26 -9
  74. package/src/runtime/tool-runner.ts +9 -1
  75. package/src/runtime/tools.ts +26 -2
  76. package/src/runtime/transcription.ts +483 -0
  77. package/src/storage/sqlite.ts +7 -2
  78. package/src/templates/blank.ts +37 -9
  79. package/src/templates/dentista.ts +40 -14
  80. package/src/templates/skills/agentkit-build-agent/SKILL.md +34 -5
  81. package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +2 -1
  82. package/src/templates/skills/agentkit-capsule/SKILL.md +32 -3
  83. package/src/templates/skills/agentkit-capsule/references/docs-router.md +2 -2
  84. package/src/templates/skills/agentkit-channels/SKILL.md +66 -1
  85. package/src/templates/skills/agentkit-channels/references/channel-buffering.md +8 -1
  86. package/src/templates/skills/agentkit-channels/references/channel-debugging.md +28 -3
  87. package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
  88. package/src/templates/skills/agentkit-channels/references/slack.md +56 -0
  89. package/src/templates/skills/agentkit-channels/references/telegram.md +34 -0
  90. package/src/templates/skills/agentkit-channels/references/whatsapp-evolution.md +57 -0
  91. package/src/templates/skills/agentkit-channels/references/whatsapp-uazapi.md +54 -0
  92. package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +42 -8
  93. package/src/templates/skills/agentkit-database/SKILL.md +11 -0
  94. package/src/templates/skills/agentkit-deploy/SKILL.md +9 -1
  95. package/src/templates/skills/agentkit-evals/SKILL.md +77 -13
  96. package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +13 -6
  97. package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +8 -4
  98. package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +8 -4
  99. package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +16 -7
  100. package/src/templates/skills/agentkit-improve/SKILL.md +96 -0
  101. package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
  102. package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
  103. package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
  104. package/src/templates/skills/agentkit-integrations/SKILL.md +98 -0
  105. package/src/templates/skills/agentkit-knowledge/SKILL.md +4 -1
  106. package/src/templates/skills/agentkit-prompts/SKILL.md +3 -1
  107. package/src/templates/skills/agentkit-provider/SKILL.md +29 -4
  108. package/src/templates/skills/agentkit-security/SKILL.md +5 -2
  109. package/src/templates/skills/agentkit-tools/SKILL.md +8 -1
  110. package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +8 -8
  111. package/src/templates/skills/agentkit-tools/examples/jev-service-fit.tool.md +110 -0
  112. package/src/templates/skills/agentkit-troubleshooting/SKILL.md +25 -1
  113. package/src/templates/support.ts +42 -12
  114. package/docs/guides/agentkit-skills-architecture.md +0 -471
  115. package/docs/guides/channels-implementation-map.md +0 -243
  116. package/docs/guides/channels-production-handoff.md +0 -101
  117. package/docs/portable-deploy-release-checklist.md +0 -41
@@ -108,7 +108,25 @@ async function searchLexical(db: DatabaseRunner, query: string, limit: number):
108
108
  }
109
109
 
110
110
  async function searchLexicalFallback(db: DatabaseRunner, query: string, limit: number): Promise<KnowledgeSearchResult[]> {
111
- const like = `%${query.toLowerCase()}%`;
111
+ const tokens = queryTokens(query);
112
+
113
+ if (tokens.length === 0) {
114
+ return [];
115
+ }
116
+
117
+ const conditions = tokens
118
+ .map(
119
+ () => `(
120
+ lower(c.content) LIKE ? ESCAPE '\\'
121
+ OR lower(COALESCE(c.title, '')) LIKE ? ESCAPE '\\'
122
+ OR lower(COALESCE(c.section, '')) LIKE ? ESCAPE '\\'
123
+ )`,
124
+ )
125
+ .join(" AND ");
126
+ const args = tokens.flatMap((token) => {
127
+ const like = likePattern(token);
128
+ return [like, like, like];
129
+ });
112
130
  const result = await db.query(
113
131
  `
114
132
  SELECT
@@ -121,13 +139,11 @@ async function searchLexicalFallback(db: DatabaseRunner, query: string, limit: n
121
139
  s.source_key AS source_key
122
140
  FROM agentkit_knowledge_chunks c
123
141
  JOIN agentkit_knowledge_sources s ON s.id = c.source_id
124
- WHERE lower(c.content) LIKE ?
125
- OR lower(COALESCE(c.title, '')) LIKE ?
126
- OR lower(COALESCE(c.section, '')) LIKE ?
142
+ WHERE ${conditions}
127
143
  ORDER BY c.created_at DESC, c.ordinal ASC
128
144
  LIMIT ?
129
145
  `,
130
- [like, like, like, limit],
146
+ [...args, limit],
131
147
  );
132
148
 
133
149
  return result.rows.map((row, index) => knowledgeSearchResultFromRow(row, lexicalScore(index), null));
@@ -269,6 +285,10 @@ function queryTokens(query: string): string[] {
269
285
  return Array.from(new Set(query.toLowerCase().match(/[\p{L}\p{N}_-]+/gu) ?? [])).slice(0, 12);
270
286
  }
271
287
 
288
+ function likePattern(token: string): string {
289
+ return `%${token.replace(/[\\%_]/g, (value) => `\\${value}`)}%`;
290
+ }
291
+
272
292
  function parseEmbedding(value: unknown): number[] | null {
273
293
  if (typeof value !== "string") {
274
294
  return null;
@@ -1,6 +1,12 @@
1
1
  export const KNOWLEDGE_MIGRATION_ID = "0006_create_knowledge_tables";
2
2
 
3
- export const KNOWLEDGE_SCHEMA_SQL = `
3
+ type KnowledgeSchemaDatabase = {
4
+ exec(sql: string): unknown;
5
+ };
6
+
7
+ export type KnowledgeLexicalIndexMode = "fts5" | "fallback";
8
+
9
+ export const KNOWLEDGE_BASE_SCHEMA_SQL = `
4
10
  CREATE TABLE IF NOT EXISTS agentkit_knowledge_sources (
5
11
  id TEXT PRIMARY KEY,
6
12
  source_key TEXT NOT NULL UNIQUE,
@@ -43,7 +49,9 @@ export const KNOWLEDGE_SCHEMA_SQL = `
43
49
 
44
50
  CREATE INDEX IF NOT EXISTS agentkit_knowledge_chunks_source_ordinal_idx
45
51
  ON agentkit_knowledge_chunks(source_id, ordinal);
52
+ `;
46
53
 
54
+ export const KNOWLEDGE_FTS5_SCHEMA_SQL = `
47
55
  CREATE VIRTUAL TABLE IF NOT EXISTS agentkit_knowledge_fts
48
56
  USING fts5(
49
57
  chunk_id UNINDEXED,
@@ -54,3 +62,39 @@ export const KNOWLEDGE_SCHEMA_SQL = `
54
62
  tokenize='unicode61'
55
63
  );
56
64
  `;
65
+
66
+ export const KNOWLEDGE_FTS_FALLBACK_SCHEMA_SQL = `
67
+ CREATE TABLE IF NOT EXISTS agentkit_knowledge_fts (
68
+ chunk_id TEXT PRIMARY KEY,
69
+ source_id TEXT NOT NULL,
70
+ title TEXT,
71
+ section TEXT,
72
+ content TEXT NOT NULL
73
+ );
74
+
75
+ CREATE INDEX IF NOT EXISTS agentkit_knowledge_fts_source_idx
76
+ ON agentkit_knowledge_fts(source_id);
77
+ `;
78
+
79
+ export const KNOWLEDGE_SCHEMA_SQL = `${KNOWLEDGE_BASE_SCHEMA_SQL}\n${KNOWLEDGE_FTS5_SCHEMA_SQL}`;
80
+
81
+ export function applyKnowledgeSchema(db: KnowledgeSchemaDatabase): KnowledgeLexicalIndexMode {
82
+ db.exec(KNOWLEDGE_BASE_SCHEMA_SQL);
83
+
84
+ try {
85
+ db.exec(KNOWLEDGE_FTS5_SCHEMA_SQL);
86
+ return "fts5";
87
+ } catch (error) {
88
+ if (!isMissingFts5ModuleError(error)) {
89
+ throw error;
90
+ }
91
+ }
92
+
93
+ db.exec(KNOWLEDGE_FTS_FALLBACK_SCHEMA_SQL);
94
+ return "fallback";
95
+ }
96
+
97
+ function isMissingFts5ModuleError(error: unknown): boolean {
98
+ const message = error instanceof Error ? error.message : String(error);
99
+ return /no such module:\s*fts5/i.test(message) || /unknown module:\s*fts5/i.test(message);
100
+ }
@@ -0,0 +1,141 @@
1
+ export type RuntimePromptContextOptions = {
2
+ timeZone?: string | null;
3
+ env?: Record<string, string | undefined>;
4
+ now?: Date;
5
+ clock?: RuntimeClockContext;
6
+ };
7
+
8
+ export type RuntimePromptContext = {
9
+ isoTimestamp: string;
10
+ timeZone: string;
11
+ localDate: string;
12
+ localWeekday: string;
13
+ localDateTime: string;
14
+ };
15
+
16
+ export type RuntimeClockContext = RuntimePromptContext & {
17
+ now: Date;
18
+ };
19
+
20
+ const FALLBACK_TIME_ZONE = "UTC";
21
+ const LOCALE = "en-US";
22
+
23
+ export function appendRuntimePromptContext(
24
+ instructions: string,
25
+ options: RuntimePromptContextOptions = {},
26
+ ): string {
27
+ const context = options.clock ?? buildRuntimePromptContext(options);
28
+
29
+ return `${instructions.trimEnd()}
30
+
31
+ Runtime context:
32
+ - Current timestamp: ${context.isoTimestamp}
33
+ - Time zone: ${context.timeZone}
34
+ - Local date: ${context.localDate}
35
+ - Local weekday: ${context.localWeekday}
36
+ - Local date and time: ${context.localDateTime}
37
+
38
+ Use this runtime context to interpret relative dates and times such as today, tomorrow, next Friday, and morning/afternoon. If a scheduling request depends on a different user or business time zone, ask for confirmation before booking.`;
39
+ }
40
+
41
+ export function buildRuntimePromptContext(options: RuntimePromptContextOptions = {}): RuntimePromptContext {
42
+ const clock = buildRuntimeClockContext(options);
43
+
44
+ return {
45
+ isoTimestamp: clock.isoTimestamp,
46
+ timeZone: clock.timeZone,
47
+ localDate: clock.localDate,
48
+ localWeekday: clock.localWeekday,
49
+ localDateTime: clock.localDateTime,
50
+ };
51
+ }
52
+
53
+ export function buildRuntimeClockContext(options: RuntimePromptContextOptions = {}): RuntimeClockContext {
54
+ if (options.clock) {
55
+ return options.clock;
56
+ }
57
+
58
+ const now = new Date((options.now ?? new Date()).getTime());
59
+
60
+ if (Number.isNaN(now.getTime())) {
61
+ throw new Error("runtime prompt context requires a valid Date.");
62
+ }
63
+
64
+ const timeZone = resolveRuntimeTimeZone(options);
65
+
66
+ return {
67
+ now,
68
+ isoTimestamp: now.toISOString(),
69
+ timeZone,
70
+ localDate: formatLocalDate(now, timeZone),
71
+ localWeekday: formatDateTime(now, timeZone, {
72
+ weekday: "long",
73
+ }),
74
+ localDateTime: formatDateTime(now, timeZone, {
75
+ weekday: "long",
76
+ year: "numeric",
77
+ month: "long",
78
+ day: "numeric",
79
+ hour: "numeric",
80
+ minute: "2-digit",
81
+ timeZoneName: "short",
82
+ }),
83
+ };
84
+ }
85
+
86
+ export function resolveRuntimeTimeZone(options: RuntimePromptContextOptions = {}): string {
87
+ if (options.timeZone?.trim()) {
88
+ return normalizeTimeZone(options.timeZone, "timeZone");
89
+ }
90
+
91
+ const explicitEnvTimeZone = options.env?.AGENTKIT_TIME_ZONE?.trim();
92
+
93
+ if (explicitEnvTimeZone) {
94
+ return normalizeTimeZone(explicitEnvTimeZone, "AGENTKIT_TIME_ZONE");
95
+ }
96
+
97
+ const processTimeZone = options.env?.TZ?.trim();
98
+
99
+ if (processTimeZone && isValidTimeZone(processTimeZone)) {
100
+ return processTimeZone;
101
+ }
102
+
103
+ return normalizeTimeZone(Intl.DateTimeFormat().resolvedOptions().timeZone || FALLBACK_TIME_ZONE, "runtime time zone");
104
+ }
105
+
106
+ export function isValidTimeZone(value: string): boolean {
107
+ try {
108
+ new Intl.DateTimeFormat(LOCALE, { timeZone: value }).format(new Date(0));
109
+ return true;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ function normalizeTimeZone(value: string, source: string): string {
116
+ const timeZone = value.trim();
117
+
118
+ if (!isValidTimeZone(timeZone)) {
119
+ throw new Error(`${source} must be a valid IANA time zone such as "UTC" or "America/New_York".`);
120
+ }
121
+
122
+ return timeZone;
123
+ }
124
+
125
+ function formatLocalDate(date: Date, timeZone: string): string {
126
+ const parts = new Intl.DateTimeFormat(LOCALE, {
127
+ timeZone,
128
+ year: "numeric",
129
+ month: "2-digit",
130
+ day: "2-digit",
131
+ }).formatToParts(date);
132
+ const byType = Object.fromEntries(parts.map((part) => [part.type, part.value]));
133
+ return `${byType.year}-${byType.month}-${byType.day}`;
134
+ }
135
+
136
+ function formatDateTime(date: Date, timeZone: string, options: Intl.DateTimeFormatOptions): string {
137
+ return new Intl.DateTimeFormat(LOCALE, {
138
+ ...options,
139
+ timeZone,
140
+ }).format(date);
141
+ }
@@ -7,6 +7,10 @@ export type RuntimeContractTarget = {
7
7
 
8
8
  export type RuntimeContractOptions = {
9
9
  expectedAgentName: string;
10
+ auth?: {
11
+ chat?: HeadersInit;
12
+ admin?: HeadersInit;
13
+ };
10
14
  tool: {
11
15
  name: string;
12
16
  input: unknown;
@@ -20,20 +24,74 @@ export async function assertRuntimeContract(
20
24
  ): Promise<void> {
21
25
  const baseUrl = target.baseUrl.replace(/\/$/, "");
22
26
 
23
- const root = await fetch(`${baseUrl}/`);
27
+ const root = await fetch(`${baseUrl}/`, { headers: options.auth?.chat });
24
28
  expect(root.status).toBe(200);
25
29
 
26
30
  const health = await fetchJson(`${baseUrl}/health`);
27
31
  expect(health.status).toBe(200);
28
32
  expect(health.body).toMatchObject({ status: "ok" });
29
33
 
30
- const inspect = await fetchJson(`${baseUrl}/_agentkit`);
34
+ const inspect = await fetchJson(`${baseUrl}/_agentkit`, { headers: options.auth?.admin });
31
35
  expect(inspect.status).toBe(200);
32
36
  expect(inspect.body).toMatchObject({ agent: options.expectedAgentName });
33
37
 
38
+ if (options.auth) {
39
+ const missingAdmin = await fetchJson(`${baseUrl}/_agentkit`);
40
+ expect(missingAdmin.status).toBe(401);
41
+ expect(missingAdmin.body.error.code).toBe("unauthorized");
42
+
43
+ const invalidAdmin = await fetchJson(`${baseUrl}/_agentkit`, {
44
+ headers: { Authorization: "Bearer invalid-admin-token" },
45
+ });
46
+ expect(invalidAdmin.status).toBe(401);
47
+ expect(invalidAdmin.body.error.code).toBe("unauthorized");
48
+
49
+ const missingChat = await fetchJson(`${baseUrl}/chat`, {
50
+ method: "POST",
51
+ headers: { "content-type": "application/json" },
52
+ body: JSON.stringify({ message: "hello" }),
53
+ });
54
+ expect(missingChat.status).toBe(401);
55
+ expect(missingChat.body.error.code).toBe("unauthorized");
56
+
57
+ const invalidChat = await fetchJson(`${baseUrl}/chat`, {
58
+ method: "POST",
59
+ headers: { "content-type": "application/json", Authorization: "Bearer invalid-chat-token" },
60
+ body: JSON.stringify({ message: "hello" }),
61
+ });
62
+ expect(invalidChat.status).toBe(401);
63
+ expect(invalidChat.body.error.code).toBe("unauthorized");
64
+
65
+ const missingTool = await fetchJson(`${baseUrl}/tools/${encodeURIComponent(options.tool.name)}`, {
66
+ method: "POST",
67
+ headers: { "content-type": "application/json" },
68
+ body: JSON.stringify({ input: options.tool.input }),
69
+ });
70
+ expect(missingTool.status).toBe(401);
71
+ expect(missingTool.body.error.code).toBe("unauthorized");
72
+
73
+ const invalidTool = await fetchJson(`${baseUrl}/tools/${encodeURIComponent(options.tool.name)}`, {
74
+ method: "POST",
75
+ headers: { "content-type": "application/json", Authorization: "Bearer invalid-admin-token" },
76
+ body: JSON.stringify({ input: options.tool.input }),
77
+ });
78
+ expect(invalidTool.status).toBe(401);
79
+ expect(invalidTool.body.error.code).toBe("unauthorized");
80
+
81
+ const missingConversations = await fetchJson(`${baseUrl}/v1/conversations`);
82
+ expect(missingConversations.status).toBe(401);
83
+ expect(missingConversations.body.error.code).toBe("unauthorized");
84
+
85
+ const invalidConversations = await fetchJson(`${baseUrl}/v1/conversations`, {
86
+ headers: { Authorization: "Bearer invalid-admin-token" },
87
+ });
88
+ expect(invalidConversations.status).toBe(401);
89
+ expect(invalidConversations.body.error.code).toBe("unauthorized");
90
+ }
91
+
34
92
  const unauthorized = await fetchJson(`${baseUrl}/chat`, {
35
93
  method: "POST",
36
- headers: { "content-type": "application/json" },
94
+ headers: withHeaders({ "content-type": "application/json" }, options.auth?.chat),
37
95
  body: JSON.stringify({ message: { role: "assistant", content: "not allowed" } }),
38
96
  });
39
97
  expect(unauthorized.status).toBe(400);
@@ -41,7 +99,7 @@ export async function assertRuntimeContract(
41
99
 
42
100
  const chat = await fetchJson(`${baseUrl}/chat`, {
43
101
  method: "POST",
44
- headers: { "content-type": "application/json" },
102
+ headers: withHeaders({ "content-type": "application/json" }, options.auth?.chat),
45
103
  body: JSON.stringify({ message: { role: "user", content: "my name is Ada" } }),
46
104
  });
47
105
  expect(chat.status).toBe(200);
@@ -55,7 +113,7 @@ export async function assertRuntimeContract(
55
113
 
56
114
  const continuedChat = await fetchJson(`${baseUrl}/chat`, {
57
115
  method: "POST",
58
- headers: { "content-type": "application/json" },
116
+ headers: withHeaders({ "content-type": "application/json" }, options.auth?.chat),
59
117
  body: JSON.stringify({ message: "what is my name?", conversationId: chat.body.conversationId }),
60
118
  });
61
119
  expect(continuedChat.status).toBe(200);
@@ -69,7 +127,7 @@ export async function assertRuntimeContract(
69
127
 
70
128
  const tool = await fetchJson(`${baseUrl}/tools/${encodeURIComponent(options.tool.name)}`, {
71
129
  method: "POST",
72
- headers: { "content-type": "application/json" },
130
+ headers: withHeaders({ "content-type": "application/json" }, options.auth?.admin),
73
131
  body: JSON.stringify({ input: options.tool.input }),
74
132
  });
75
133
  expect(tool.status).toBe(200);
@@ -78,7 +136,7 @@ export async function assertRuntimeContract(
78
136
  output: options.tool.expectedOutput,
79
137
  });
80
138
 
81
- const conversations = await fetchJson(`${baseUrl}/v1/conversations`);
139
+ const conversations = await fetchJson(`${baseUrl}/v1/conversations`, { headers: options.auth?.admin });
82
140
  expect(conversations.status).toBe(200);
83
141
  expect(conversations.body.conversations.length).toBeGreaterThanOrEqual(1);
84
142
  expect(conversations.body.conversations.some((conversation: { id: string }) => conversation.id === chat.body.conversationId)).toBe(true);
@@ -91,3 +149,9 @@ async function fetchJson(url: string, init?: RequestInit): Promise<{ status: num
91
149
  body: await response.json(),
92
150
  };
93
151
  }
152
+
153
+ function withHeaders(required: HeadersInit, extra: HeadersInit | undefined): Headers {
154
+ const headers = new Headers(extra);
155
+ new Headers(required).forEach((value, key) => headers.set(key, value));
156
+ return headers;
157
+ }
@@ -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
+ }