shuvmaki 0.4.26

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 (94) hide show
  1. package/bin.js +70 -0
  2. package/dist/ai-tool-to-genai.js +210 -0
  3. package/dist/ai-tool-to-genai.test.js +267 -0
  4. package/dist/channel-management.js +97 -0
  5. package/dist/cli.js +709 -0
  6. package/dist/commands/abort.js +78 -0
  7. package/dist/commands/add-project.js +98 -0
  8. package/dist/commands/agent.js +152 -0
  9. package/dist/commands/ask-question.js +183 -0
  10. package/dist/commands/create-new-project.js +78 -0
  11. package/dist/commands/fork.js +186 -0
  12. package/dist/commands/model.js +313 -0
  13. package/dist/commands/permissions.js +126 -0
  14. package/dist/commands/queue.js +129 -0
  15. package/dist/commands/resume.js +145 -0
  16. package/dist/commands/session.js +142 -0
  17. package/dist/commands/share.js +80 -0
  18. package/dist/commands/types.js +2 -0
  19. package/dist/commands/undo-redo.js +161 -0
  20. package/dist/commands/user-command.js +145 -0
  21. package/dist/database.js +184 -0
  22. package/dist/discord-bot.js +384 -0
  23. package/dist/discord-utils.js +217 -0
  24. package/dist/escape-backticks.test.js +410 -0
  25. package/dist/format-tables.js +96 -0
  26. package/dist/format-tables.test.js +418 -0
  27. package/dist/genai-worker-wrapper.js +109 -0
  28. package/dist/genai-worker.js +297 -0
  29. package/dist/genai.js +232 -0
  30. package/dist/interaction-handler.js +144 -0
  31. package/dist/logger.js +51 -0
  32. package/dist/markdown.js +310 -0
  33. package/dist/markdown.test.js +262 -0
  34. package/dist/message-formatting.js +273 -0
  35. package/dist/message-formatting.test.js +73 -0
  36. package/dist/openai-realtime.js +228 -0
  37. package/dist/opencode.js +216 -0
  38. package/dist/session-handler.js +580 -0
  39. package/dist/system-message.js +61 -0
  40. package/dist/tools.js +356 -0
  41. package/dist/utils.js +85 -0
  42. package/dist/voice-handler.js +541 -0
  43. package/dist/voice.js +314 -0
  44. package/dist/worker-types.js +4 -0
  45. package/dist/xml.js +92 -0
  46. package/dist/xml.test.js +32 -0
  47. package/package.json +60 -0
  48. package/src/__snapshots__/compact-session-context-no-system.md +35 -0
  49. package/src/__snapshots__/compact-session-context.md +47 -0
  50. package/src/ai-tool-to-genai.test.ts +296 -0
  51. package/src/ai-tool-to-genai.ts +255 -0
  52. package/src/channel-management.ts +161 -0
  53. package/src/cli.ts +1010 -0
  54. package/src/commands/abort.ts +94 -0
  55. package/src/commands/add-project.ts +139 -0
  56. package/src/commands/agent.ts +201 -0
  57. package/src/commands/ask-question.ts +276 -0
  58. package/src/commands/create-new-project.ts +111 -0
  59. package/src/commands/fork.ts +257 -0
  60. package/src/commands/model.ts +402 -0
  61. package/src/commands/permissions.ts +146 -0
  62. package/src/commands/queue.ts +181 -0
  63. package/src/commands/resume.ts +230 -0
  64. package/src/commands/session.ts +184 -0
  65. package/src/commands/share.ts +96 -0
  66. package/src/commands/types.ts +25 -0
  67. package/src/commands/undo-redo.ts +213 -0
  68. package/src/commands/user-command.ts +178 -0
  69. package/src/database.ts +220 -0
  70. package/src/discord-bot.ts +513 -0
  71. package/src/discord-utils.ts +282 -0
  72. package/src/escape-backticks.test.ts +447 -0
  73. package/src/format-tables.test.ts +440 -0
  74. package/src/format-tables.ts +110 -0
  75. package/src/genai-worker-wrapper.ts +160 -0
  76. package/src/genai-worker.ts +366 -0
  77. package/src/genai.ts +321 -0
  78. package/src/interaction-handler.ts +187 -0
  79. package/src/logger.ts +57 -0
  80. package/src/markdown.test.ts +358 -0
  81. package/src/markdown.ts +365 -0
  82. package/src/message-formatting.test.ts +81 -0
  83. package/src/message-formatting.ts +340 -0
  84. package/src/openai-realtime.ts +363 -0
  85. package/src/opencode.ts +277 -0
  86. package/src/session-handler.ts +758 -0
  87. package/src/system-message.ts +62 -0
  88. package/src/tools.ts +428 -0
  89. package/src/utils.ts +118 -0
  90. package/src/voice-handler.ts +760 -0
  91. package/src/voice.ts +432 -0
  92. package/src/worker-types.ts +66 -0
  93. package/src/xml.test.ts +37 -0
  94. package/src/xml.ts +121 -0
@@ -0,0 +1,296 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { tool } from 'ai'
3
+ import { z } from 'zod'
4
+ import { Type } from '@google/genai'
5
+ import type { FunctionDeclaration, FunctionCall } from '@google/genai'
6
+ import {
7
+ aiToolToGenAIFunction,
8
+ aiToolToCallableTool,
9
+ extractSchemaFromTool,
10
+ } from './ai-tool-to-genai.js'
11
+
12
+ describe('AI Tool to GenAI Conversion', () => {
13
+ it('should convert a simple Zod-based tool', () => {
14
+ const weatherTool = tool({
15
+ description: 'Get the current weather for a location',
16
+ inputSchema: z.object({
17
+ location: z.string().describe('The city name'),
18
+ unit: z.enum(['celsius', 'fahrenheit']).optional(),
19
+ }),
20
+ execute: async ({ location, unit }) => {
21
+ return {
22
+ temperature: 72,
23
+ unit: unit || 'fahrenheit',
24
+ condition: 'sunny',
25
+ }
26
+ },
27
+ })
28
+
29
+ const genAIFunction = aiToolToGenAIFunction(weatherTool)
30
+
31
+ expect(genAIFunction).toMatchInlineSnapshot(`
32
+ {
33
+ "description": "Get the current weather for a location",
34
+ "name": "tool",
35
+ "parameters": {
36
+ "properties": {
37
+ "location": {
38
+ "description": "The city name",
39
+ "type": "STRING",
40
+ },
41
+ "unit": {
42
+ "enum": [
43
+ "celsius",
44
+ "fahrenheit",
45
+ ],
46
+ "type": "STRING",
47
+ },
48
+ },
49
+ "required": [
50
+ "location",
51
+ ],
52
+ "type": "OBJECT",
53
+ },
54
+ }
55
+ `)
56
+ })
57
+
58
+ it('should handle complex nested schemas', () => {
59
+ const complexTool = tool({
60
+ description: 'Process complex data',
61
+ inputSchema: z.object({
62
+ user: z.object({
63
+ name: z.string(),
64
+ age: z.number().int().min(0).max(150),
65
+ email: z.string().email(),
66
+ }),
67
+ preferences: z.array(z.string()),
68
+ metadata: z.record(z.string(), z.unknown()).optional(),
69
+ }),
70
+ execute: async (input) => input,
71
+ })
72
+
73
+ const genAIFunction = aiToolToGenAIFunction(complexTool)
74
+
75
+ expect(genAIFunction.parameters).toMatchInlineSnapshot(`
76
+ {
77
+ "properties": {
78
+ "metadata": {
79
+ "type": "OBJECT",
80
+ },
81
+ "preferences": {
82
+ "items": {
83
+ "type": "STRING",
84
+ },
85
+ "type": "ARRAY",
86
+ },
87
+ "user": {
88
+ "properties": {
89
+ "age": {
90
+ "format": "int32",
91
+ "maximum": 150,
92
+ "minimum": 0,
93
+ "type": "INTEGER",
94
+ },
95
+ "email": {
96
+ "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
97
+ "type": "STRING",
98
+ },
99
+ "name": {
100
+ "type": "STRING",
101
+ },
102
+ },
103
+ "required": [
104
+ "name",
105
+ "age",
106
+ "email",
107
+ ],
108
+ "type": "OBJECT",
109
+ },
110
+ },
111
+ "required": [
112
+ "user",
113
+ "preferences",
114
+ ],
115
+ "type": "OBJECT",
116
+ }
117
+ `)
118
+ })
119
+
120
+ it('should extract schema from tool', () => {
121
+ const testTool = tool({
122
+ inputSchema: z.object({
123
+ test: z.string(),
124
+ }),
125
+ execute: async () => {},
126
+ })
127
+
128
+ const schema = extractSchemaFromTool(testTool)
129
+
130
+ expect(schema).toMatchInlineSnapshot(`
131
+ {
132
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
133
+ "additionalProperties": false,
134
+ "properties": {
135
+ "test": {
136
+ "type": "string",
137
+ },
138
+ },
139
+ "required": [
140
+ "test",
141
+ ],
142
+ "type": "object",
143
+ }
144
+ `)
145
+ })
146
+
147
+ it('should handle tools with no input schema', () => {
148
+ const simpleTool = tool({
149
+ description: 'Simple tool with no inputs',
150
+ inputSchema: z.object({}),
151
+ execute: async () => ({ result: 'done' }),
152
+ })
153
+
154
+ const genAIFunction = aiToolToGenAIFunction(simpleTool)
155
+
156
+ expect(genAIFunction).toMatchInlineSnapshot(`
157
+ {
158
+ "description": "Simple tool with no inputs",
159
+ "name": "tool",
160
+ "parameters": {
161
+ "properties": {},
162
+ "type": "OBJECT",
163
+ },
164
+ }
165
+ `)
166
+ })
167
+
168
+ it('should handle union types', () => {
169
+ const unionTool = tool({
170
+ description: 'Tool with union types',
171
+ inputSchema: z.object({
172
+ value: z.union([z.string(), z.number(), z.boolean()]),
173
+ }),
174
+ execute: async ({ value }) => ({ received: value }),
175
+ })
176
+
177
+ const genAIFunction = aiToolToGenAIFunction(unionTool)
178
+
179
+ expect(genAIFunction.parameters?.properties?.value).toMatchInlineSnapshot(`
180
+ {
181
+ "anyOf": [
182
+ {
183
+ "type": "STRING",
184
+ },
185
+ {
186
+ "format": "float",
187
+ "type": "NUMBER",
188
+ },
189
+ {
190
+ "type": "BOOLEAN",
191
+ },
192
+ ],
193
+ }
194
+ `)
195
+ })
196
+
197
+ it('should create a CallableTool', async () => {
198
+ const weatherTool = tool({
199
+ description: 'Get weather',
200
+ inputSchema: z.object({
201
+ location: z.string(),
202
+ }),
203
+ execute: async ({ location }) => ({
204
+ temperature: 72,
205
+ location,
206
+ }),
207
+ })
208
+
209
+ const callableTool = aiToolToCallableTool(weatherTool, 'weather')
210
+
211
+ // Test tool() method
212
+ const genAITool = await callableTool.tool()
213
+ expect(genAITool.functionDeclarations).toMatchInlineSnapshot(`
214
+ [
215
+ {
216
+ "description": "Get weather",
217
+ "name": "weather",
218
+ "parameters": {
219
+ "properties": {
220
+ "location": {
221
+ "type": "STRING",
222
+ },
223
+ },
224
+ "required": [
225
+ "location",
226
+ ],
227
+ "type": "OBJECT",
228
+ },
229
+ },
230
+ ]
231
+ `)
232
+
233
+ // Test callTool() method
234
+ const functionCall: FunctionCall = {
235
+ id: 'call_123',
236
+ name: 'weather',
237
+ args: { location: 'San Francisco' },
238
+ }
239
+
240
+ const parts = await callableTool.callTool([functionCall])
241
+ expect(parts).toMatchInlineSnapshot(`
242
+ [
243
+ {
244
+ "functionResponse": {
245
+ "id": "call_123",
246
+ "name": "weather",
247
+ "response": {
248
+ "output": {
249
+ "location": "San Francisco",
250
+ "temperature": 72,
251
+ },
252
+ },
253
+ },
254
+ },
255
+ ]
256
+ `)
257
+ })
258
+
259
+ it('should handle tool execution errors', async () => {
260
+ const errorTool = tool({
261
+ description: 'Tool that throws',
262
+ inputSchema: z.object({
263
+ trigger: z.boolean(),
264
+ }),
265
+ execute: async ({ trigger }) => {
266
+ if (trigger) {
267
+ throw new Error('Tool execution failed')
268
+ }
269
+ return { success: true }
270
+ },
271
+ })
272
+
273
+ const callableTool = aiToolToCallableTool(errorTool, 'error_tool')
274
+
275
+ const functionCall: FunctionCall = {
276
+ id: 'call_error',
277
+ name: 'error_tool',
278
+ args: { trigger: true },
279
+ }
280
+
281
+ const parts = await callableTool.callTool([functionCall])
282
+ expect(parts).toMatchInlineSnapshot(`
283
+ [
284
+ {
285
+ "functionResponse": {
286
+ "id": "call_error",
287
+ "name": "error_tool",
288
+ "response": {
289
+ "error": "Tool execution failed",
290
+ },
291
+ },
292
+ },
293
+ ]
294
+ `)
295
+ })
296
+ })
@@ -0,0 +1,255 @@
1
+ // AI SDK to Google GenAI tool converter.
2
+ // Transforms Vercel AI SDK tool definitions into Google GenAI CallableTool format
3
+ // for use with Gemini's function calling in the voice assistant.
4
+
5
+ import type { Tool, jsonSchema as JsonSchemaType } from 'ai'
6
+ import type {
7
+ FunctionDeclaration,
8
+ Schema,
9
+ Type as GenAIType,
10
+ Tool as GenAITool,
11
+ CallableTool,
12
+ FunctionCall,
13
+ Part,
14
+ } from '@google/genai'
15
+ import { Type } from '@google/genai'
16
+ import { z, toJSONSchema } from 'zod'
17
+
18
+ /**
19
+ * Convert JSON Schema to GenAI Schema format
20
+ * Based on the actual implementation used by the GenAI package:
21
+ * https://github.com/googleapis/js-genai/blob/027f09db662ce6b30f737b10b4d2efcb4282a9b6/src/_transformers.ts#L294
22
+ */
23
+ function jsonSchemaToGenAISchema(jsonSchema: any): Schema {
24
+ const schema: Schema = {}
25
+
26
+ // Map JSON Schema type to GenAI Type
27
+ if (jsonSchema.type) {
28
+ switch (jsonSchema.type) {
29
+ case 'string':
30
+ schema.type = Type.STRING
31
+ break
32
+ case 'number':
33
+ schema.type = Type.NUMBER
34
+ schema.format = jsonSchema.format || 'float'
35
+ break
36
+ case 'integer':
37
+ schema.type = Type.INTEGER
38
+ schema.format = jsonSchema.format || 'int32'
39
+ break
40
+ case 'boolean':
41
+ schema.type = Type.BOOLEAN
42
+ break
43
+ case 'array':
44
+ schema.type = Type.ARRAY
45
+ if (jsonSchema.items) {
46
+ schema.items = jsonSchemaToGenAISchema(jsonSchema.items)
47
+ }
48
+ if (jsonSchema.minItems !== undefined) {
49
+ schema.minItems = jsonSchema.minItems
50
+ }
51
+ if (jsonSchema.maxItems !== undefined) {
52
+ schema.maxItems = jsonSchema.maxItems
53
+ }
54
+ break
55
+ case 'object':
56
+ schema.type = Type.OBJECT
57
+ if (jsonSchema.properties) {
58
+ schema.properties = {}
59
+ for (const [key, value] of Object.entries(jsonSchema.properties)) {
60
+ schema.properties[key] = jsonSchemaToGenAISchema(value)
61
+ }
62
+ }
63
+ if (jsonSchema.required) {
64
+ schema.required = jsonSchema.required
65
+ }
66
+ // Note: GenAI Schema doesn't have additionalProperties field
67
+ // We skip it for now
68
+ break
69
+ default:
70
+ // For unknown types, keep as-is
71
+ schema.type = jsonSchema.type
72
+ }
73
+ }
74
+
75
+ // Copy over common properties
76
+ if (jsonSchema.description) {
77
+ schema.description = jsonSchema.description
78
+ }
79
+ if (jsonSchema.enum) {
80
+ schema.enum = jsonSchema.enum.map(String)
81
+ }
82
+ if (jsonSchema.default !== undefined) {
83
+ schema.default = jsonSchema.default
84
+ }
85
+ if (jsonSchema.example !== undefined) {
86
+ schema.example = jsonSchema.example
87
+ }
88
+ if (jsonSchema.nullable) {
89
+ schema.nullable = true
90
+ }
91
+
92
+ // Handle anyOf/oneOf as anyOf in GenAI
93
+ if (jsonSchema.anyOf) {
94
+ schema.anyOf = jsonSchema.anyOf.map((s: any) => jsonSchemaToGenAISchema(s))
95
+ } else if (jsonSchema.oneOf) {
96
+ schema.anyOf = jsonSchema.oneOf.map((s: any) => jsonSchemaToGenAISchema(s))
97
+ }
98
+
99
+ // Handle number/string specific properties
100
+ if (jsonSchema.minimum !== undefined) {
101
+ schema.minimum = jsonSchema.minimum
102
+ }
103
+ if (jsonSchema.maximum !== undefined) {
104
+ schema.maximum = jsonSchema.maximum
105
+ }
106
+ if (jsonSchema.minLength !== undefined) {
107
+ schema.minLength = jsonSchema.minLength
108
+ }
109
+ if (jsonSchema.maxLength !== undefined) {
110
+ schema.maxLength = jsonSchema.maxLength
111
+ }
112
+ if (jsonSchema.pattern) {
113
+ schema.pattern = jsonSchema.pattern
114
+ }
115
+
116
+ return schema
117
+ }
118
+
119
+ /**
120
+ * Convert AI SDK Tool to GenAI FunctionDeclaration
121
+ */
122
+ export function aiToolToGenAIFunction(
123
+ tool: Tool<any, any>,
124
+ ): FunctionDeclaration {
125
+ // Extract the input schema - assume it's a Zod schema
126
+ const inputSchema = tool.inputSchema as z.ZodType<any>
127
+
128
+ // Get the tool name from the schema or generate one
129
+ let toolName = 'tool'
130
+ let jsonSchema: any = {}
131
+
132
+ if (inputSchema) {
133
+ // Convert Zod schema to JSON Schema
134
+ jsonSchema = toJSONSchema(inputSchema)
135
+
136
+ // Extract name from Zod description if available
137
+ const description = inputSchema.description
138
+ if (description) {
139
+ const nameMatch = description.match(/name:\s*(\w+)/)
140
+ if (nameMatch) {
141
+ toolName = nameMatch[1] || ''
142
+ }
143
+ }
144
+ }
145
+
146
+ // Convert JSON Schema to GenAI Schema
147
+ const genAISchema = jsonSchemaToGenAISchema(jsonSchema)
148
+
149
+ // Create the FunctionDeclaration
150
+ const functionDeclaration: FunctionDeclaration = {
151
+ name: toolName,
152
+ description: tool.description || jsonSchema.description || 'Tool function',
153
+ parameters: genAISchema,
154
+ }
155
+
156
+ return functionDeclaration
157
+ }
158
+
159
+ /**
160
+ * Convert AI SDK Tool to GenAI CallableTool
161
+ */
162
+ export function aiToolToCallableTool(
163
+ tool: Tool<any, any>,
164
+ name: string,
165
+ ): CallableTool & { name: string } {
166
+ const toolName = name || 'tool'
167
+
168
+ return {
169
+ name,
170
+ async tool(): Promise<GenAITool> {
171
+ const functionDeclaration = aiToolToGenAIFunction(tool)
172
+ if (name) {
173
+ functionDeclaration.name = name
174
+ }
175
+
176
+ return {
177
+ functionDeclarations: [functionDeclaration],
178
+ }
179
+ },
180
+
181
+ async callTool(functionCalls: FunctionCall[]): Promise<Part[]> {
182
+ const parts: Part[] = []
183
+
184
+ for (const functionCall of functionCalls) {
185
+ // Check if this function call matches our tool
186
+ if (
187
+ functionCall.name !== toolName &&
188
+ name &&
189
+ functionCall.name !== name
190
+ ) {
191
+ continue
192
+ }
193
+
194
+ // Execute the tool if it has an execute function
195
+ if (tool.execute) {
196
+ try {
197
+ const result = await tool.execute(functionCall.args || {}, {
198
+ toolCallId: functionCall.id || '',
199
+ messages: [],
200
+ })
201
+
202
+ // Convert the result to a Part
203
+ parts.push({
204
+ functionResponse: {
205
+ id: functionCall.id,
206
+ name: functionCall.name || toolName,
207
+ response: {
208
+ output: result,
209
+ },
210
+ },
211
+ } as Part)
212
+ } catch (error) {
213
+ // Handle errors
214
+ parts.push({
215
+ functionResponse: {
216
+ id: functionCall.id,
217
+ name: functionCall.name || toolName,
218
+ response: {
219
+ error: error instanceof Error ? error.message : String(error),
220
+ },
221
+ },
222
+ } as Part)
223
+ }
224
+ }
225
+ }
226
+
227
+ return parts
228
+ },
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Helper to extract schema from AI SDK tool
234
+ */
235
+ export function extractSchemaFromTool(tool: Tool<any, any>): any {
236
+ const inputSchema = tool.inputSchema as z.ZodType<any>
237
+
238
+ if (!inputSchema) {
239
+ return {}
240
+ }
241
+
242
+ // Convert Zod schema to JSON Schema
243
+ return toJSONSchema(inputSchema)
244
+ }
245
+
246
+ /**
247
+ * Given an object of tools, creates an array of CallableTool
248
+ */
249
+ export function callableToolsFromObject(
250
+ tools: Record<string, Tool<any, any>>,
251
+ ): Array<CallableTool & { name: string }> {
252
+ return Object.entries(tools).map(([name, tool]) =>
253
+ aiToolToCallableTool(tool, name),
254
+ )
255
+ }
@@ -0,0 +1,161 @@
1
+ // Discord channel and category management.
2
+ // Creates and manages Kimaki project channels (text + voice pairs),
3
+ // extracts channel metadata from topic tags, and ensures category structure.
4
+
5
+ import {
6
+ ChannelType,
7
+ type CategoryChannel,
8
+ type Guild,
9
+ type TextChannel,
10
+ } from 'discord.js'
11
+ import path from 'node:path'
12
+ import { getDatabase } from './database.js'
13
+ import { extractTagsArrays } from './xml.js'
14
+
15
+ export async function ensureKimakiCategory(
16
+ guild: Guild,
17
+ botName?: string,
18
+ ): Promise<CategoryChannel> {
19
+ const categoryName = botName ? `Kimaki ${botName}` : 'Kimaki'
20
+
21
+ const existingCategory = guild.channels.cache.find(
22
+ (channel): channel is CategoryChannel => {
23
+ if (channel.type !== ChannelType.GuildCategory) {
24
+ return false
25
+ }
26
+
27
+ return channel.name.toLowerCase() === categoryName.toLowerCase()
28
+ },
29
+ )
30
+
31
+ if (existingCategory) {
32
+ return existingCategory
33
+ }
34
+
35
+ return guild.channels.create({
36
+ name: categoryName,
37
+ type: ChannelType.GuildCategory,
38
+ })
39
+ }
40
+
41
+ export async function ensureKimakiAudioCategory(
42
+ guild: Guild,
43
+ botName?: string,
44
+ ): Promise<CategoryChannel> {
45
+ const categoryName = botName ? `Kimaki Audio ${botName}` : 'Kimaki Audio'
46
+
47
+ const existingCategory = guild.channels.cache.find(
48
+ (channel): channel is CategoryChannel => {
49
+ if (channel.type !== ChannelType.GuildCategory) {
50
+ return false
51
+ }
52
+
53
+ return channel.name.toLowerCase() === categoryName.toLowerCase()
54
+ },
55
+ )
56
+
57
+ if (existingCategory) {
58
+ return existingCategory
59
+ }
60
+
61
+ return guild.channels.create({
62
+ name: categoryName,
63
+ type: ChannelType.GuildCategory,
64
+ })
65
+ }
66
+
67
+ export async function createProjectChannels({
68
+ guild,
69
+ projectDirectory,
70
+ appId,
71
+ botName,
72
+ }: {
73
+ guild: Guild
74
+ projectDirectory: string
75
+ appId: string
76
+ botName?: string
77
+ }): Promise<{ textChannelId: string; voiceChannelId: string; channelName: string }> {
78
+ const baseName = path.basename(projectDirectory)
79
+ const channelName = `${baseName}`
80
+ .toLowerCase()
81
+ .replace(/[^a-z0-9-]/g, '-')
82
+ .slice(0, 100)
83
+
84
+ const kimakiCategory = await ensureKimakiCategory(guild, botName)
85
+ const kimakiAudioCategory = await ensureKimakiAudioCategory(guild, botName)
86
+
87
+ const textChannel = await guild.channels.create({
88
+ name: channelName,
89
+ type: ChannelType.GuildText,
90
+ parent: kimakiCategory,
91
+ topic: `<kimaki><directory>${projectDirectory}</directory><app>${appId}</app></kimaki>`,
92
+ })
93
+
94
+ const voiceChannel = await guild.channels.create({
95
+ name: channelName,
96
+ type: ChannelType.GuildVoice,
97
+ parent: kimakiAudioCategory,
98
+ })
99
+
100
+ getDatabase()
101
+ .prepare(
102
+ 'INSERT OR REPLACE INTO channel_directories (channel_id, directory, channel_type) VALUES (?, ?, ?)',
103
+ )
104
+ .run(textChannel.id, projectDirectory, 'text')
105
+
106
+ getDatabase()
107
+ .prepare(
108
+ 'INSERT OR REPLACE INTO channel_directories (channel_id, directory, channel_type) VALUES (?, ?, ?)',
109
+ )
110
+ .run(voiceChannel.id, projectDirectory, 'voice')
111
+
112
+ return {
113
+ textChannelId: textChannel.id,
114
+ voiceChannelId: voiceChannel.id,
115
+ channelName,
116
+ }
117
+ }
118
+
119
+ export type ChannelWithTags = {
120
+ id: string
121
+ name: string
122
+ description: string | null
123
+ kimakiDirectory?: string
124
+ kimakiApp?: string
125
+ }
126
+
127
+ export async function getChannelsWithDescriptions(
128
+ guild: Guild,
129
+ ): Promise<ChannelWithTags[]> {
130
+ const channels: ChannelWithTags[] = []
131
+
132
+ guild.channels.cache
133
+ .filter((channel) => channel.isTextBased())
134
+ .forEach((channel) => {
135
+ const textChannel = channel as TextChannel
136
+ const description = textChannel.topic || null
137
+
138
+ let kimakiDirectory: string | undefined
139
+ let kimakiApp: string | undefined
140
+
141
+ if (description) {
142
+ const extracted = extractTagsArrays({
143
+ xml: description,
144
+ tags: ['kimaki.directory', 'kimaki.app'],
145
+ })
146
+
147
+ kimakiDirectory = extracted['kimaki.directory']?.[0]?.trim()
148
+ kimakiApp = extracted['kimaki.app']?.[0]?.trim()
149
+ }
150
+
151
+ channels.push({
152
+ id: textChannel.id,
153
+ name: textChannel.name,
154
+ description,
155
+ kimakiDirectory,
156
+ kimakiApp,
157
+ })
158
+ })
159
+
160
+ return channels
161
+ }