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
package/bin.js ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+
3
+ // CLI entrypoint with automatic restart capability.
4
+ // Spawns the main CLI and restarts it on non-zero exit codes with throttling.
5
+ // Exit codes 0, 130 (SIGINT), 143 (SIGTERM), or 64 (EXIT_NO_RESTART) terminate cleanly.
6
+
7
+ import { spawn } from 'node:child_process'
8
+ import { fileURLToPath } from 'node:url'
9
+ import { dirname, join } from 'node:path'
10
+
11
+ const __filename = fileURLToPath(import.meta.url)
12
+ const __dirname = dirname(__filename)
13
+
14
+ const NODE_PATH = process.execPath
15
+ const CLI_PATH = join(__dirname, 'dist', 'cli.js')
16
+
17
+ let lastStart = 0
18
+
19
+ async function sleep(ms) {
20
+ return new Promise(resolve => setTimeout(resolve, ms))
21
+ }
22
+
23
+ async function run() {
24
+ while (true) {
25
+ const now = Date.now()
26
+ const elapsed = now - lastStart
27
+ if (elapsed < 5000) {
28
+ await sleep(5000 - elapsed)
29
+ }
30
+ lastStart = Date.now()
31
+
32
+ try {
33
+ const code = await new Promise((resolve) => {
34
+ const child = spawn(NODE_PATH, [CLI_PATH, ...process.argv.slice(2)], {
35
+ stdio: 'inherit'
36
+ })
37
+
38
+ child.on('exit', (code, signal) => {
39
+ if (signal) {
40
+ // Map signals to exit codes similar to bash
41
+ if (signal === 'SIGINT') resolve(130)
42
+ else if (signal === 'SIGTERM') resolve(143)
43
+ else resolve(1)
44
+ } else {
45
+ resolve(code || 0)
46
+ }
47
+ })
48
+
49
+ child.on('error', (err) => {
50
+ console.error('Failed to start process:', err)
51
+ resolve(1)
52
+ })
53
+ })
54
+
55
+ // Exit cleanly if the app ended OK or via SIGINT/SIGTERM
56
+ if (code === 0 || code === 130 || code === 143 || code === 64) {
57
+ process.exit(code)
58
+ }
59
+ // otherwise loop; the 5s throttle above will apply
60
+ } catch (err) {
61
+ console.error('Unexpected error:', err)
62
+ // Continue looping after error
63
+ }
64
+ }
65
+ }
66
+
67
+ run().catch(err => {
68
+ console.error('Fatal error:', err)
69
+ process.exit(1)
70
+ })
@@ -0,0 +1,210 @@
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
+ import { Type } from '@google/genai';
5
+ import { z, toJSONSchema } from 'zod';
6
+ /**
7
+ * Convert JSON Schema to GenAI Schema format
8
+ * Based on the actual implementation used by the GenAI package:
9
+ * https://github.com/googleapis/js-genai/blob/027f09db662ce6b30f737b10b4d2efcb4282a9b6/src/_transformers.ts#L294
10
+ */
11
+ function jsonSchemaToGenAISchema(jsonSchema) {
12
+ const schema = {};
13
+ // Map JSON Schema type to GenAI Type
14
+ if (jsonSchema.type) {
15
+ switch (jsonSchema.type) {
16
+ case 'string':
17
+ schema.type = Type.STRING;
18
+ break;
19
+ case 'number':
20
+ schema.type = Type.NUMBER;
21
+ schema.format = jsonSchema.format || 'float';
22
+ break;
23
+ case 'integer':
24
+ schema.type = Type.INTEGER;
25
+ schema.format = jsonSchema.format || 'int32';
26
+ break;
27
+ case 'boolean':
28
+ schema.type = Type.BOOLEAN;
29
+ break;
30
+ case 'array':
31
+ schema.type = Type.ARRAY;
32
+ if (jsonSchema.items) {
33
+ schema.items = jsonSchemaToGenAISchema(jsonSchema.items);
34
+ }
35
+ if (jsonSchema.minItems !== undefined) {
36
+ schema.minItems = jsonSchema.minItems;
37
+ }
38
+ if (jsonSchema.maxItems !== undefined) {
39
+ schema.maxItems = jsonSchema.maxItems;
40
+ }
41
+ break;
42
+ case 'object':
43
+ schema.type = Type.OBJECT;
44
+ if (jsonSchema.properties) {
45
+ schema.properties = {};
46
+ for (const [key, value] of Object.entries(jsonSchema.properties)) {
47
+ schema.properties[key] = jsonSchemaToGenAISchema(value);
48
+ }
49
+ }
50
+ if (jsonSchema.required) {
51
+ schema.required = jsonSchema.required;
52
+ }
53
+ // Note: GenAI Schema doesn't have additionalProperties field
54
+ // We skip it for now
55
+ break;
56
+ default:
57
+ // For unknown types, keep as-is
58
+ schema.type = jsonSchema.type;
59
+ }
60
+ }
61
+ // Copy over common properties
62
+ if (jsonSchema.description) {
63
+ schema.description = jsonSchema.description;
64
+ }
65
+ if (jsonSchema.enum) {
66
+ schema.enum = jsonSchema.enum.map(String);
67
+ }
68
+ if (jsonSchema.default !== undefined) {
69
+ schema.default = jsonSchema.default;
70
+ }
71
+ if (jsonSchema.example !== undefined) {
72
+ schema.example = jsonSchema.example;
73
+ }
74
+ if (jsonSchema.nullable) {
75
+ schema.nullable = true;
76
+ }
77
+ // Handle anyOf/oneOf as anyOf in GenAI
78
+ if (jsonSchema.anyOf) {
79
+ schema.anyOf = jsonSchema.anyOf.map((s) => jsonSchemaToGenAISchema(s));
80
+ }
81
+ else if (jsonSchema.oneOf) {
82
+ schema.anyOf = jsonSchema.oneOf.map((s) => jsonSchemaToGenAISchema(s));
83
+ }
84
+ // Handle number/string specific properties
85
+ if (jsonSchema.minimum !== undefined) {
86
+ schema.minimum = jsonSchema.minimum;
87
+ }
88
+ if (jsonSchema.maximum !== undefined) {
89
+ schema.maximum = jsonSchema.maximum;
90
+ }
91
+ if (jsonSchema.minLength !== undefined) {
92
+ schema.minLength = jsonSchema.minLength;
93
+ }
94
+ if (jsonSchema.maxLength !== undefined) {
95
+ schema.maxLength = jsonSchema.maxLength;
96
+ }
97
+ if (jsonSchema.pattern) {
98
+ schema.pattern = jsonSchema.pattern;
99
+ }
100
+ return schema;
101
+ }
102
+ /**
103
+ * Convert AI SDK Tool to GenAI FunctionDeclaration
104
+ */
105
+ export function aiToolToGenAIFunction(tool) {
106
+ // Extract the input schema - assume it's a Zod schema
107
+ const inputSchema = tool.inputSchema;
108
+ // Get the tool name from the schema or generate one
109
+ let toolName = 'tool';
110
+ let jsonSchema = {};
111
+ if (inputSchema) {
112
+ // Convert Zod schema to JSON Schema
113
+ jsonSchema = toJSONSchema(inputSchema);
114
+ // Extract name from Zod description if available
115
+ const description = inputSchema.description;
116
+ if (description) {
117
+ const nameMatch = description.match(/name:\s*(\w+)/);
118
+ if (nameMatch) {
119
+ toolName = nameMatch[1] || '';
120
+ }
121
+ }
122
+ }
123
+ // Convert JSON Schema to GenAI Schema
124
+ const genAISchema = jsonSchemaToGenAISchema(jsonSchema);
125
+ // Create the FunctionDeclaration
126
+ const functionDeclaration = {
127
+ name: toolName,
128
+ description: tool.description || jsonSchema.description || 'Tool function',
129
+ parameters: genAISchema,
130
+ };
131
+ return functionDeclaration;
132
+ }
133
+ /**
134
+ * Convert AI SDK Tool to GenAI CallableTool
135
+ */
136
+ export function aiToolToCallableTool(tool, name) {
137
+ const toolName = name || 'tool';
138
+ return {
139
+ name,
140
+ async tool() {
141
+ const functionDeclaration = aiToolToGenAIFunction(tool);
142
+ if (name) {
143
+ functionDeclaration.name = name;
144
+ }
145
+ return {
146
+ functionDeclarations: [functionDeclaration],
147
+ };
148
+ },
149
+ async callTool(functionCalls) {
150
+ const parts = [];
151
+ for (const functionCall of functionCalls) {
152
+ // Check if this function call matches our tool
153
+ if (functionCall.name !== toolName &&
154
+ name &&
155
+ functionCall.name !== name) {
156
+ continue;
157
+ }
158
+ // Execute the tool if it has an execute function
159
+ if (tool.execute) {
160
+ try {
161
+ const result = await tool.execute(functionCall.args || {}, {
162
+ toolCallId: functionCall.id || '',
163
+ messages: [],
164
+ });
165
+ // Convert the result to a Part
166
+ parts.push({
167
+ functionResponse: {
168
+ id: functionCall.id,
169
+ name: functionCall.name || toolName,
170
+ response: {
171
+ output: result,
172
+ },
173
+ },
174
+ });
175
+ }
176
+ catch (error) {
177
+ // Handle errors
178
+ parts.push({
179
+ functionResponse: {
180
+ id: functionCall.id,
181
+ name: functionCall.name || toolName,
182
+ response: {
183
+ error: error instanceof Error ? error.message : String(error),
184
+ },
185
+ },
186
+ });
187
+ }
188
+ }
189
+ }
190
+ return parts;
191
+ },
192
+ };
193
+ }
194
+ /**
195
+ * Helper to extract schema from AI SDK tool
196
+ */
197
+ export function extractSchemaFromTool(tool) {
198
+ const inputSchema = tool.inputSchema;
199
+ if (!inputSchema) {
200
+ return {};
201
+ }
202
+ // Convert Zod schema to JSON Schema
203
+ return toJSONSchema(inputSchema);
204
+ }
205
+ /**
206
+ * Given an object of tools, creates an array of CallableTool
207
+ */
208
+ export function callableToolsFromObject(tools) {
209
+ return Object.entries(tools).map(([name, tool]) => aiToolToCallableTool(tool, name));
210
+ }
@@ -0,0 +1,267 @@
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 { aiToolToGenAIFunction, aiToolToCallableTool, extractSchemaFromTool, } from './ai-tool-to-genai.js';
6
+ describe('AI Tool to GenAI Conversion', () => {
7
+ it('should convert a simple Zod-based tool', () => {
8
+ const weatherTool = tool({
9
+ description: 'Get the current weather for a location',
10
+ inputSchema: z.object({
11
+ location: z.string().describe('The city name'),
12
+ unit: z.enum(['celsius', 'fahrenheit']).optional(),
13
+ }),
14
+ execute: async ({ location, unit }) => {
15
+ return {
16
+ temperature: 72,
17
+ unit: unit || 'fahrenheit',
18
+ condition: 'sunny',
19
+ };
20
+ },
21
+ });
22
+ const genAIFunction = aiToolToGenAIFunction(weatherTool);
23
+ expect(genAIFunction).toMatchInlineSnapshot(`
24
+ {
25
+ "description": "Get the current weather for a location",
26
+ "name": "tool",
27
+ "parameters": {
28
+ "properties": {
29
+ "location": {
30
+ "description": "The city name",
31
+ "type": "STRING",
32
+ },
33
+ "unit": {
34
+ "enum": [
35
+ "celsius",
36
+ "fahrenheit",
37
+ ],
38
+ "type": "STRING",
39
+ },
40
+ },
41
+ "required": [
42
+ "location",
43
+ ],
44
+ "type": "OBJECT",
45
+ },
46
+ }
47
+ `);
48
+ });
49
+ it('should handle complex nested schemas', () => {
50
+ const complexTool = tool({
51
+ description: 'Process complex data',
52
+ inputSchema: z.object({
53
+ user: z.object({
54
+ name: z.string(),
55
+ age: z.number().int().min(0).max(150),
56
+ email: z.string().email(),
57
+ }),
58
+ preferences: z.array(z.string()),
59
+ metadata: z.record(z.string(), z.unknown()).optional(),
60
+ }),
61
+ execute: async (input) => input,
62
+ });
63
+ const genAIFunction = aiToolToGenAIFunction(complexTool);
64
+ expect(genAIFunction.parameters).toMatchInlineSnapshot(`
65
+ {
66
+ "properties": {
67
+ "metadata": {
68
+ "type": "OBJECT",
69
+ },
70
+ "preferences": {
71
+ "items": {
72
+ "type": "STRING",
73
+ },
74
+ "type": "ARRAY",
75
+ },
76
+ "user": {
77
+ "properties": {
78
+ "age": {
79
+ "format": "int32",
80
+ "maximum": 150,
81
+ "minimum": 0,
82
+ "type": "INTEGER",
83
+ },
84
+ "email": {
85
+ "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
86
+ "type": "STRING",
87
+ },
88
+ "name": {
89
+ "type": "STRING",
90
+ },
91
+ },
92
+ "required": [
93
+ "name",
94
+ "age",
95
+ "email",
96
+ ],
97
+ "type": "OBJECT",
98
+ },
99
+ },
100
+ "required": [
101
+ "user",
102
+ "preferences",
103
+ ],
104
+ "type": "OBJECT",
105
+ }
106
+ `);
107
+ });
108
+ it('should extract schema from tool', () => {
109
+ const testTool = tool({
110
+ inputSchema: z.object({
111
+ test: z.string(),
112
+ }),
113
+ execute: async () => { },
114
+ });
115
+ const schema = extractSchemaFromTool(testTool);
116
+ expect(schema).toMatchInlineSnapshot(`
117
+ {
118
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
119
+ "additionalProperties": false,
120
+ "properties": {
121
+ "test": {
122
+ "type": "string",
123
+ },
124
+ },
125
+ "required": [
126
+ "test",
127
+ ],
128
+ "type": "object",
129
+ }
130
+ `);
131
+ });
132
+ it('should handle tools with no input schema', () => {
133
+ const simpleTool = tool({
134
+ description: 'Simple tool with no inputs',
135
+ inputSchema: z.object({}),
136
+ execute: async () => ({ result: 'done' }),
137
+ });
138
+ const genAIFunction = aiToolToGenAIFunction(simpleTool);
139
+ expect(genAIFunction).toMatchInlineSnapshot(`
140
+ {
141
+ "description": "Simple tool with no inputs",
142
+ "name": "tool",
143
+ "parameters": {
144
+ "properties": {},
145
+ "type": "OBJECT",
146
+ },
147
+ }
148
+ `);
149
+ });
150
+ it('should handle union types', () => {
151
+ const unionTool = tool({
152
+ description: 'Tool with union types',
153
+ inputSchema: z.object({
154
+ value: z.union([z.string(), z.number(), z.boolean()]),
155
+ }),
156
+ execute: async ({ value }) => ({ received: value }),
157
+ });
158
+ const genAIFunction = aiToolToGenAIFunction(unionTool);
159
+ expect(genAIFunction.parameters?.properties?.value).toMatchInlineSnapshot(`
160
+ {
161
+ "anyOf": [
162
+ {
163
+ "type": "STRING",
164
+ },
165
+ {
166
+ "format": "float",
167
+ "type": "NUMBER",
168
+ },
169
+ {
170
+ "type": "BOOLEAN",
171
+ },
172
+ ],
173
+ }
174
+ `);
175
+ });
176
+ it('should create a CallableTool', async () => {
177
+ const weatherTool = tool({
178
+ description: 'Get weather',
179
+ inputSchema: z.object({
180
+ location: z.string(),
181
+ }),
182
+ execute: async ({ location }) => ({
183
+ temperature: 72,
184
+ location,
185
+ }),
186
+ });
187
+ const callableTool = aiToolToCallableTool(weatherTool, 'weather');
188
+ // Test tool() method
189
+ const genAITool = await callableTool.tool();
190
+ expect(genAITool.functionDeclarations).toMatchInlineSnapshot(`
191
+ [
192
+ {
193
+ "description": "Get weather",
194
+ "name": "weather",
195
+ "parameters": {
196
+ "properties": {
197
+ "location": {
198
+ "type": "STRING",
199
+ },
200
+ },
201
+ "required": [
202
+ "location",
203
+ ],
204
+ "type": "OBJECT",
205
+ },
206
+ },
207
+ ]
208
+ `);
209
+ // Test callTool() method
210
+ const functionCall = {
211
+ id: 'call_123',
212
+ name: 'weather',
213
+ args: { location: 'San Francisco' },
214
+ };
215
+ const parts = await callableTool.callTool([functionCall]);
216
+ expect(parts).toMatchInlineSnapshot(`
217
+ [
218
+ {
219
+ "functionResponse": {
220
+ "id": "call_123",
221
+ "name": "weather",
222
+ "response": {
223
+ "output": {
224
+ "location": "San Francisco",
225
+ "temperature": 72,
226
+ },
227
+ },
228
+ },
229
+ },
230
+ ]
231
+ `);
232
+ });
233
+ it('should handle tool execution errors', async () => {
234
+ const errorTool = tool({
235
+ description: 'Tool that throws',
236
+ inputSchema: z.object({
237
+ trigger: z.boolean(),
238
+ }),
239
+ execute: async ({ trigger }) => {
240
+ if (trigger) {
241
+ throw new Error('Tool execution failed');
242
+ }
243
+ return { success: true };
244
+ },
245
+ });
246
+ const callableTool = aiToolToCallableTool(errorTool, 'error_tool');
247
+ const functionCall = {
248
+ id: 'call_error',
249
+ name: 'error_tool',
250
+ args: { trigger: true },
251
+ };
252
+ const parts = await callableTool.callTool([functionCall]);
253
+ expect(parts).toMatchInlineSnapshot(`
254
+ [
255
+ {
256
+ "functionResponse": {
257
+ "id": "call_error",
258
+ "name": "error_tool",
259
+ "response": {
260
+ "error": "Tool execution failed",
261
+ },
262
+ },
263
+ },
264
+ ]
265
+ `);
266
+ });
267
+ });
@@ -0,0 +1,97 @@
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
+ import { ChannelType, } from 'discord.js';
5
+ import path from 'node:path';
6
+ import { getDatabase } from './database.js';
7
+ import { extractTagsArrays } from './xml.js';
8
+ export async function ensureKimakiCategory(guild, botName) {
9
+ const categoryName = botName ? `Kimaki ${botName}` : 'Kimaki';
10
+ const existingCategory = guild.channels.cache.find((channel) => {
11
+ if (channel.type !== ChannelType.GuildCategory) {
12
+ return false;
13
+ }
14
+ return channel.name.toLowerCase() === categoryName.toLowerCase();
15
+ });
16
+ if (existingCategory) {
17
+ return existingCategory;
18
+ }
19
+ return guild.channels.create({
20
+ name: categoryName,
21
+ type: ChannelType.GuildCategory,
22
+ });
23
+ }
24
+ export async function ensureKimakiAudioCategory(guild, botName) {
25
+ const categoryName = botName ? `Kimaki Audio ${botName}` : 'Kimaki Audio';
26
+ const existingCategory = guild.channels.cache.find((channel) => {
27
+ if (channel.type !== ChannelType.GuildCategory) {
28
+ return false;
29
+ }
30
+ return channel.name.toLowerCase() === categoryName.toLowerCase();
31
+ });
32
+ if (existingCategory) {
33
+ return existingCategory;
34
+ }
35
+ return guild.channels.create({
36
+ name: categoryName,
37
+ type: ChannelType.GuildCategory,
38
+ });
39
+ }
40
+ export async function createProjectChannels({ guild, projectDirectory, appId, botName, }) {
41
+ const baseName = path.basename(projectDirectory);
42
+ const channelName = `${baseName}`
43
+ .toLowerCase()
44
+ .replace(/[^a-z0-9-]/g, '-')
45
+ .slice(0, 100);
46
+ const kimakiCategory = await ensureKimakiCategory(guild, botName);
47
+ const kimakiAudioCategory = await ensureKimakiAudioCategory(guild, botName);
48
+ const textChannel = await guild.channels.create({
49
+ name: channelName,
50
+ type: ChannelType.GuildText,
51
+ parent: kimakiCategory,
52
+ topic: `<kimaki><directory>${projectDirectory}</directory><app>${appId}</app></kimaki>`,
53
+ });
54
+ const voiceChannel = await guild.channels.create({
55
+ name: channelName,
56
+ type: ChannelType.GuildVoice,
57
+ parent: kimakiAudioCategory,
58
+ });
59
+ getDatabase()
60
+ .prepare('INSERT OR REPLACE INTO channel_directories (channel_id, directory, channel_type) VALUES (?, ?, ?)')
61
+ .run(textChannel.id, projectDirectory, 'text');
62
+ getDatabase()
63
+ .prepare('INSERT OR REPLACE INTO channel_directories (channel_id, directory, channel_type) VALUES (?, ?, ?)')
64
+ .run(voiceChannel.id, projectDirectory, 'voice');
65
+ return {
66
+ textChannelId: textChannel.id,
67
+ voiceChannelId: voiceChannel.id,
68
+ channelName,
69
+ };
70
+ }
71
+ export async function getChannelsWithDescriptions(guild) {
72
+ const channels = [];
73
+ guild.channels.cache
74
+ .filter((channel) => channel.isTextBased())
75
+ .forEach((channel) => {
76
+ const textChannel = channel;
77
+ const description = textChannel.topic || null;
78
+ let kimakiDirectory;
79
+ let kimakiApp;
80
+ if (description) {
81
+ const extracted = extractTagsArrays({
82
+ xml: description,
83
+ tags: ['kimaki.directory', 'kimaki.app'],
84
+ });
85
+ kimakiDirectory = extracted['kimaki.directory']?.[0]?.trim();
86
+ kimakiApp = extracted['kimaki.app']?.[0]?.trim();
87
+ }
88
+ channels.push({
89
+ id: textChannel.id,
90
+ name: textChannel.name,
91
+ description,
92
+ kimakiDirectory,
93
+ kimakiApp,
94
+ });
95
+ });
96
+ return channels;
97
+ }