lazy-gravity 0.0.2 → 0.0.3
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.
- package/LICENSE +21 -0
- package/README.md +224 -0
- package/dist/bin/cli.js +79 -0
- package/dist/bin/commands/doctor.js +156 -0
- package/dist/bin/commands/open.js +145 -0
- package/dist/bin/commands/setup.js +366 -0
- package/dist/bin/commands/start.js +15 -0
- package/dist/bot/index.js +914 -0
- package/dist/commands/chatCommandHandler.js +145 -0
- package/dist/commands/cleanupCommandHandler.js +396 -0
- package/dist/commands/messageParser.js +28 -0
- package/dist/commands/registerSlashCommands.js +149 -0
- package/dist/commands/slashCommandHandler.js +104 -0
- package/dist/commands/workspaceCommandHandler.js +230 -0
- package/dist/database/chatSessionRepository.js +88 -0
- package/dist/database/scheduleRepository.js +119 -0
- package/dist/database/templateRepository.js +103 -0
- package/dist/database/workspaceBindingRepository.js +109 -0
- package/dist/events/interactionCreateHandler.js +286 -0
- package/dist/events/messageCreateHandler.js +154 -0
- package/dist/index.js +10 -0
- package/dist/middleware/auth.js +10 -0
- package/dist/middleware/sanitize.js +20 -0
- package/dist/services/antigravityLauncher.js +89 -0
- package/dist/services/approvalDetector.js +384 -0
- package/dist/services/autoAcceptService.js +80 -0
- package/dist/services/cdpBridgeManager.js +204 -0
- package/dist/services/cdpConnectionPool.js +157 -0
- package/dist/services/cdpService.js +1311 -0
- package/dist/services/channelManager.js +118 -0
- package/dist/services/chatSessionService.js +516 -0
- package/dist/services/modeService.js +73 -0
- package/dist/services/modelService.js +63 -0
- package/dist/services/processManager.js +61 -0
- package/dist/services/progressSender.js +61 -0
- package/dist/services/promptDispatcher.js +17 -0
- package/dist/services/quotaService.js +185 -0
- package/dist/services/responseMonitor.js +645 -0
- package/dist/services/scheduleService.js +134 -0
- package/dist/services/screenshotService.js +85 -0
- package/dist/services/titleGeneratorService.js +113 -0
- package/dist/services/workspaceService.js +64 -0
- package/dist/ui/autoAcceptUi.js +34 -0
- package/dist/ui/modeUi.js +34 -0
- package/dist/ui/modelsUi.js +97 -0
- package/dist/ui/screenshotUi.js +51 -0
- package/dist/ui/templateUi.js +67 -0
- package/dist/utils/cdpPorts.js +5 -0
- package/dist/utils/config.js +20 -0
- package/dist/utils/configLoader.js +160 -0
- package/dist/utils/discordFormatter.js +167 -0
- package/dist/utils/i18n.js +77 -0
- package/dist/utils/imageHandler.js +154 -0
- package/dist/utils/lockfile.js +113 -0
- package/dist/utils/logger.js +32 -0
- package/dist/utils/logo.js +13 -0
- package/dist/utils/metadataExtractor.js +15 -0
- package/dist/utils/processLogBuffer.js +98 -0
- package/dist/utils/streamMessageFormatter.js +90 -0
- package/package.json +73 -5
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.slashCommands = void 0;
|
|
4
|
+
exports.registerSlashCommands = registerSlashCommands;
|
|
5
|
+
const logger_1 = require("../utils/logger");
|
|
6
|
+
const discord_js_1 = require("discord.js");
|
|
7
|
+
const i18n_1 = require("../utils/i18n");
|
|
8
|
+
/**
|
|
9
|
+
* Slash command definitions for the Discord Interactions API.
|
|
10
|
+
* Registers bot slash commands to the application.
|
|
11
|
+
*/
|
|
12
|
+
/** /mode command definition */
|
|
13
|
+
const modeCommand = new discord_js_1.SlashCommandBuilder()
|
|
14
|
+
.setName('mode')
|
|
15
|
+
.setDescription((0, i18n_1.t)('Display and change execution mode via a dropdown'));
|
|
16
|
+
/** /model command definition (formerly /models, unified to singular) */
|
|
17
|
+
const modelCommand = new discord_js_1.SlashCommandBuilder()
|
|
18
|
+
.setName('model')
|
|
19
|
+
.setDescription((0, i18n_1.t)('Display and change available LLM models'))
|
|
20
|
+
.addStringOption((option) => option
|
|
21
|
+
.setName('name')
|
|
22
|
+
.setDescription((0, i18n_1.t)('Name of the model to change to'))
|
|
23
|
+
.setRequired(false));
|
|
24
|
+
/** /template command definition (formerly /templates, unified to singular) */
|
|
25
|
+
const templateCommand = new discord_js_1.SlashCommandBuilder()
|
|
26
|
+
.setName('template')
|
|
27
|
+
.setDescription((0, i18n_1.t)('List, register, or delete templates'))
|
|
28
|
+
.addSubcommand((sub) => sub
|
|
29
|
+
.setName('list')
|
|
30
|
+
.setDescription((0, i18n_1.t)('Display registered template list with execute buttons')))
|
|
31
|
+
.addSubcommand((sub) => sub
|
|
32
|
+
.setName('add')
|
|
33
|
+
.setDescription((0, i18n_1.t)('Register a new template'))
|
|
34
|
+
.addStringOption((option) => option
|
|
35
|
+
.setName('name')
|
|
36
|
+
.setDescription((0, i18n_1.t)('Template name'))
|
|
37
|
+
.setRequired(true))
|
|
38
|
+
.addStringOption((option) => option
|
|
39
|
+
.setName('prompt')
|
|
40
|
+
.setDescription((0, i18n_1.t)('Prompt content of the template'))
|
|
41
|
+
.setRequired(true)))
|
|
42
|
+
.addSubcommand((sub) => sub
|
|
43
|
+
.setName('delete')
|
|
44
|
+
.setDescription((0, i18n_1.t)('Delete a template'))
|
|
45
|
+
.addStringOption((option) => option
|
|
46
|
+
.setName('name')
|
|
47
|
+
.setDescription((0, i18n_1.t)('Name of the template to delete'))
|
|
48
|
+
.setRequired(true)));
|
|
49
|
+
/** /stop command definition */
|
|
50
|
+
const stopCommand = new discord_js_1.SlashCommandBuilder()
|
|
51
|
+
.setName('stop')
|
|
52
|
+
.setDescription((0, i18n_1.t)('Interrupt active LLM generation'));
|
|
53
|
+
/** /screenshot command definition */
|
|
54
|
+
const screenshotCommand = new discord_js_1.SlashCommandBuilder()
|
|
55
|
+
.setName('screenshot')
|
|
56
|
+
.setDescription((0, i18n_1.t)('Capture current Antigravity screen'));
|
|
57
|
+
/** /status command definition (formerly /cdp status, extended to overall bot status) */
|
|
58
|
+
const statusCommand = new discord_js_1.SlashCommandBuilder()
|
|
59
|
+
.setName('status')
|
|
60
|
+
.setDescription((0, i18n_1.t)('Display overall bot status including connection, model, mode'));
|
|
61
|
+
/** /autoaccept command definition */
|
|
62
|
+
const autoAcceptCommand = new discord_js_1.SlashCommandBuilder()
|
|
63
|
+
.setName('autoaccept')
|
|
64
|
+
.setDescription((0, i18n_1.t)('Display and toggle auto-allow mode for approval dialogs'))
|
|
65
|
+
.addStringOption((option) => option
|
|
66
|
+
.setName('mode')
|
|
67
|
+
.setDescription((0, i18n_1.t)('on / off (optional direct switch)'))
|
|
68
|
+
.setRequired(false));
|
|
69
|
+
/** /project command definition (formerly /workspace, renamed to project) */
|
|
70
|
+
const projectCommand = new discord_js_1.SlashCommandBuilder()
|
|
71
|
+
.setName('project')
|
|
72
|
+
.setDescription((0, i18n_1.t)('List projects, on select auto-create channel and bind'))
|
|
73
|
+
.addSubcommand((sub) => sub
|
|
74
|
+
.setName('list')
|
|
75
|
+
.setDescription((0, i18n_1.t)('Display project list')))
|
|
76
|
+
.addSubcommand((sub) => sub
|
|
77
|
+
.setName('create')
|
|
78
|
+
.setDescription((0, i18n_1.t)('Create a new project'))
|
|
79
|
+
.addStringOption((option) => option
|
|
80
|
+
.setName('name')
|
|
81
|
+
.setDescription((0, i18n_1.t)('Name of the project to create'))
|
|
82
|
+
.setRequired(true)));
|
|
83
|
+
/** /new command definition (formerly /chat new, made into a standalone command) */
|
|
84
|
+
const newCommand = new discord_js_1.SlashCommandBuilder()
|
|
85
|
+
.setName('new')
|
|
86
|
+
.setDescription((0, i18n_1.t)('Start a new chat session in the current project'));
|
|
87
|
+
/** /chat command definition (merged status + list) */
|
|
88
|
+
const chatCommand = new discord_js_1.SlashCommandBuilder()
|
|
89
|
+
.setName('chat')
|
|
90
|
+
.setDescription((0, i18n_1.t)('Display current chat session info and session list'));
|
|
91
|
+
/** /cleanup command definition */
|
|
92
|
+
const cleanupCommand = new discord_js_1.SlashCommandBuilder()
|
|
93
|
+
.setName('cleanup')
|
|
94
|
+
.setDescription((0, i18n_1.t)('Scan and clean up inactive session channels and categories'))
|
|
95
|
+
.addIntegerOption((option) => option
|
|
96
|
+
.setName('days')
|
|
97
|
+
.setDescription((0, i18n_1.t)('Number of days of inactivity (default: 7)'))
|
|
98
|
+
.setRequired(false)
|
|
99
|
+
.setMinValue(1)
|
|
100
|
+
.setMaxValue(365));
|
|
101
|
+
/** /help command definition */
|
|
102
|
+
const helpCommand = new discord_js_1.SlashCommandBuilder()
|
|
103
|
+
.setName('help')
|
|
104
|
+
.setDescription((0, i18n_1.t)('Display list of available commands'));
|
|
105
|
+
/** Array of commands to register */
|
|
106
|
+
exports.slashCommands = [
|
|
107
|
+
helpCommand,
|
|
108
|
+
modeCommand,
|
|
109
|
+
modelCommand,
|
|
110
|
+
templateCommand,
|
|
111
|
+
stopCommand,
|
|
112
|
+
screenshotCommand,
|
|
113
|
+
statusCommand,
|
|
114
|
+
autoAcceptCommand,
|
|
115
|
+
projectCommand,
|
|
116
|
+
newCommand,
|
|
117
|
+
chatCommand,
|
|
118
|
+
cleanupCommand,
|
|
119
|
+
];
|
|
120
|
+
/**
|
|
121
|
+
* Register slash commands with Discord
|
|
122
|
+
* @param token Bot token
|
|
123
|
+
* @param clientId Bot application ID
|
|
124
|
+
* @param guildId Target guild (server) ID (global registration if omitted)
|
|
125
|
+
*/
|
|
126
|
+
async function registerSlashCommands(token, clientId, guildId) {
|
|
127
|
+
const rest = new discord_js_1.REST({ version: '10' }).setToken(token);
|
|
128
|
+
const commandData = exports.slashCommands.map((cmd) => cmd.toJSON());
|
|
129
|
+
try {
|
|
130
|
+
if (guildId) {
|
|
131
|
+
// Guild-specific registration (takes effect immediately)
|
|
132
|
+
await rest.put(discord_js_1.Routes.applicationGuildCommands(clientId, guildId), { body: commandData });
|
|
133
|
+
logger_1.logger.info(`Registered ${commandData.length} slash commands to guild ${guildId}.`);
|
|
134
|
+
// Clear global commands to avoid duplicate suggestions such as old legacy commands.
|
|
135
|
+
// This bot is expected to run primarily in guild scope when guildId is provided.
|
|
136
|
+
await rest.put(discord_js_1.Routes.applicationCommands(clientId), { body: [] });
|
|
137
|
+
logger_1.logger.info('Cleared global slash commands to prevent duplicate command listings.');
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
// Global registration (may take up to 1 hour to take effect)
|
|
141
|
+
await rest.put(discord_js_1.Routes.applicationCommands(clientId), { body: commandData });
|
|
142
|
+
logger_1.logger.info(`Registered ${commandData.length} slash commands globally.`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
logger_1.logger.error((0, i18n_1.t)('❌ Failed to register slash commands:'), error);
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SlashCommandHandler = void 0;
|
|
4
|
+
const i18n_1 = require("../utils/i18n");
|
|
5
|
+
class SlashCommandHandler {
|
|
6
|
+
templateRepo;
|
|
7
|
+
constructor(templateRepo) {
|
|
8
|
+
this.templateRepo = templateRepo;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Parse the slash command name and arguments, then route to the appropriate handler
|
|
12
|
+
*/
|
|
13
|
+
async handleCommand(commandName, args) {
|
|
14
|
+
switch (commandName.toLowerCase()) {
|
|
15
|
+
case 'template':
|
|
16
|
+
return this.handleTemplateCommand(args);
|
|
17
|
+
default:
|
|
18
|
+
return {
|
|
19
|
+
success: false,
|
|
20
|
+
message: (0, i18n_1.t)(`⚠️ Unknown command: /${commandName}`),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
handleTemplateCommand(args) {
|
|
25
|
+
if (args.length === 0) {
|
|
26
|
+
const templates = this.templateRepo.findAll();
|
|
27
|
+
if (templates.length === 0) {
|
|
28
|
+
return {
|
|
29
|
+
success: true,
|
|
30
|
+
message: (0, i18n_1.t)('📝 No templates registered.'),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const list = templates.map((t) => `- **${t.name}**`).join('\n');
|
|
34
|
+
return {
|
|
35
|
+
success: true,
|
|
36
|
+
message: (0, i18n_1.t)(`📝 Registered Templates:\n${list}\n\nTo use: \`/template [name]\``),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const subCommandOrName = args[0];
|
|
40
|
+
// add: register new template
|
|
41
|
+
if (subCommandOrName.toLowerCase() === 'add') {
|
|
42
|
+
if (args.length < 3) {
|
|
43
|
+
return {
|
|
44
|
+
success: false,
|
|
45
|
+
message: (0, i18n_1.t)('⚠️ Missing arguments.\nUsage: `/template add "name" "prompt"`'),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const name = args[1];
|
|
49
|
+
// Quotes already stripped by messageParser. Join remaining args as the prompt
|
|
50
|
+
const prompt = args.slice(2).join(' ');
|
|
51
|
+
try {
|
|
52
|
+
this.templateRepo.create({ name, prompt });
|
|
53
|
+
return {
|
|
54
|
+
success: true,
|
|
55
|
+
message: (0, i18n_1.t)(`✅ Template **${name}** registered.`),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
message: (0, i18n_1.t)(`⚠️ Failed to register template. Name might be duplicated.`),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// delete: remove template
|
|
66
|
+
if (subCommandOrName.toLowerCase() === 'delete') {
|
|
67
|
+
if (args.length < 2) {
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
message: (0, i18n_1.t)('⚠️ Specify a template name to delete.\nUsage: `/template delete "name"`'),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const name = args[1];
|
|
74
|
+
const deleted = this.templateRepo.deleteByName(name);
|
|
75
|
+
if (deleted) {
|
|
76
|
+
return {
|
|
77
|
+
success: true,
|
|
78
|
+
message: (0, i18n_1.t)(`🗑️ Template **${name}** deleted.`),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
message: (0, i18n_1.t)(`⚠️ Template **${name}** not found.`),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Otherwise treat as template invocation
|
|
89
|
+
const templateName = subCommandOrName;
|
|
90
|
+
const template = this.templateRepo.findByName(templateName);
|
|
91
|
+
if (!template) {
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
message: (0, i18n_1.t)(`⚠️ Template **${templateName}** not found.`),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
success: true,
|
|
99
|
+
message: (0, i18n_1.t)(`📝 Invoked template **${templateName}**.\nStarting process with this prompt.`),
|
|
100
|
+
prompt: template.prompt,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
exports.SlashCommandHandler = SlashCommandHandler;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.WorkspaceCommandHandler = exports.WORKSPACE_SELECT_ID = exports.PROJECT_SELECT_ID = void 0;
|
|
7
|
+
const i18n_1 = require("../utils/i18n");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const discord_js_1 = require("discord.js");
|
|
10
|
+
/** Select menu custom ID */
|
|
11
|
+
exports.PROJECT_SELECT_ID = 'project_select';
|
|
12
|
+
/** Backward compatibility: also accept old ID */
|
|
13
|
+
exports.WORKSPACE_SELECT_ID = 'workspace_select';
|
|
14
|
+
/**
|
|
15
|
+
* Handler for the /project slash command.
|
|
16
|
+
* When a project is selected, auto-creates a Discord category + session-1 channel and binds them.
|
|
17
|
+
*/
|
|
18
|
+
class WorkspaceCommandHandler {
|
|
19
|
+
bindingRepo;
|
|
20
|
+
chatSessionRepo;
|
|
21
|
+
workspaceService;
|
|
22
|
+
channelManager;
|
|
23
|
+
processingWorkspaces = new Set();
|
|
24
|
+
constructor(bindingRepo, chatSessionRepo, workspaceService, channelManager) {
|
|
25
|
+
this.bindingRepo = bindingRepo;
|
|
26
|
+
this.chatSessionRepo = chatSessionRepo;
|
|
27
|
+
this.workspaceService = workspaceService;
|
|
28
|
+
this.channelManager = channelManager;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* /project list -- Display project list via select menu
|
|
32
|
+
*/
|
|
33
|
+
async handleShow(interaction) {
|
|
34
|
+
const embed = new discord_js_1.EmbedBuilder()
|
|
35
|
+
.setTitle('📁 Projects')
|
|
36
|
+
.setColor(0x5865F2)
|
|
37
|
+
.setDescription((0, i18n_1.t)('Select a project to auto-create a category and session channel'))
|
|
38
|
+
.setTimestamp();
|
|
39
|
+
const components = [];
|
|
40
|
+
const workspaces = this.workspaceService.scanWorkspaces();
|
|
41
|
+
if (workspaces.length > 0) {
|
|
42
|
+
const options = workspaces.slice(0, 25).map((ws) => ({
|
|
43
|
+
label: ws,
|
|
44
|
+
value: ws,
|
|
45
|
+
}));
|
|
46
|
+
const selectMenu = new discord_js_1.StringSelectMenuBuilder()
|
|
47
|
+
.setCustomId(exports.PROJECT_SELECT_ID)
|
|
48
|
+
.setPlaceholder((0, i18n_1.t)('Select a project...'))
|
|
49
|
+
.addOptions(options);
|
|
50
|
+
if (workspaces.length > 25) {
|
|
51
|
+
selectMenu.setPlaceholder((0, i18n_1.t)(`Select a project... (Showing 25 of ${workspaces.length})`));
|
|
52
|
+
}
|
|
53
|
+
components.push(new discord_js_1.ActionRowBuilder().addComponents(selectMenu));
|
|
54
|
+
}
|
|
55
|
+
await interaction.editReply({
|
|
56
|
+
embeds: [embed],
|
|
57
|
+
components,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Handler for when a project is selected from the select menu.
|
|
62
|
+
* Creates a category + session-1 channel and binds them.
|
|
63
|
+
*/
|
|
64
|
+
async handleSelectMenu(interaction, guild) {
|
|
65
|
+
const workspacePath = interaction.values[0];
|
|
66
|
+
if (!this.workspaceService.exists(workspacePath)) {
|
|
67
|
+
await interaction.update({
|
|
68
|
+
content: (0, i18n_1.t)(`❌ Project \`${workspacePath}\` not found.`),
|
|
69
|
+
embeds: [],
|
|
70
|
+
components: [],
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Check if the same project is already bound (prevent duplicates)
|
|
75
|
+
const existingBindings = this.bindingRepo.findByWorkspacePathAndGuildId(workspacePath, guild.id);
|
|
76
|
+
if (existingBindings.length > 0) {
|
|
77
|
+
const channelLinks = existingBindings.map(b => `<#${b.channelId}>`).join(', ');
|
|
78
|
+
const fullPath = this.workspaceService.getWorkspacePath(workspacePath);
|
|
79
|
+
const embed = new discord_js_1.EmbedBuilder()
|
|
80
|
+
.setTitle('📁 Projects')
|
|
81
|
+
.setColor(0xFFA500)
|
|
82
|
+
.setDescription((0, i18n_1.t)(`⚠️ Project **${workspacePath}** already exists\n`) +
|
|
83
|
+
`→ ${channelLinks}`)
|
|
84
|
+
.addFields({ name: (0, i18n_1.t)('Full Path'), value: `\`${fullPath}\`` })
|
|
85
|
+
.setTimestamp();
|
|
86
|
+
await interaction.update({
|
|
87
|
+
embeds: [embed],
|
|
88
|
+
components: [],
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// Lock project being processed (prevent rapid repeated clicks)
|
|
93
|
+
if (this.processingWorkspaces.has(workspacePath)) {
|
|
94
|
+
await interaction.update({
|
|
95
|
+
content: (0, i18n_1.t)(`⏳ **${workspacePath}** is being created. Please wait.`),
|
|
96
|
+
embeds: [],
|
|
97
|
+
components: [],
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.processingWorkspaces.add(workspacePath);
|
|
102
|
+
try {
|
|
103
|
+
// Ensure category exists
|
|
104
|
+
const categoryResult = await this.channelManager.ensureCategory(guild, workspacePath);
|
|
105
|
+
const categoryId = categoryResult.categoryId;
|
|
106
|
+
// Get session number (usually 1)
|
|
107
|
+
const sessionNumber = this.chatSessionRepo.getNextSessionNumber(categoryId);
|
|
108
|
+
const channelName = `session-${sessionNumber}`;
|
|
109
|
+
// Create session channel
|
|
110
|
+
const sessionResult = await this.channelManager.createSessionChannel(guild, categoryId, channelName);
|
|
111
|
+
const channelId = sessionResult.channelId;
|
|
112
|
+
// Register binding and session
|
|
113
|
+
this.bindingRepo.upsert({
|
|
114
|
+
channelId,
|
|
115
|
+
workspacePath,
|
|
116
|
+
guildId: guild.id,
|
|
117
|
+
});
|
|
118
|
+
this.chatSessionRepo.create({
|
|
119
|
+
channelId,
|
|
120
|
+
categoryId,
|
|
121
|
+
workspacePath,
|
|
122
|
+
sessionNumber,
|
|
123
|
+
guildId: guild.id,
|
|
124
|
+
});
|
|
125
|
+
const fullPath = this.workspaceService.getWorkspacePath(workspacePath);
|
|
126
|
+
const embed = new discord_js_1.EmbedBuilder()
|
|
127
|
+
.setTitle('📁 Projects')
|
|
128
|
+
.setColor(0x00AA00)
|
|
129
|
+
.setDescription((0, i18n_1.t)(`✅ Project **${workspacePath}** created\n`) +
|
|
130
|
+
`→ <#${channelId}>`)
|
|
131
|
+
.addFields({ name: (0, i18n_1.t)('Full Path'), value: `\`${fullPath}\`` })
|
|
132
|
+
.setTimestamp();
|
|
133
|
+
await interaction.update({
|
|
134
|
+
embeds: [embed],
|
|
135
|
+
components: [],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
this.processingWorkspaces.delete(workspacePath);
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* /project create <name> -- Create a new project directory,
|
|
145
|
+
* auto-create a category + session-1 channel and bind them.
|
|
146
|
+
*/
|
|
147
|
+
async handleCreate(interaction, guild) {
|
|
148
|
+
const name = interaction.options.getString('name', true);
|
|
149
|
+
// Path traversal check
|
|
150
|
+
let fullPath;
|
|
151
|
+
try {
|
|
152
|
+
fullPath = this.workspaceService.validatePath(name);
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
await interaction.editReply({
|
|
156
|
+
content: (0, i18n_1.t)(`❌ Invalid project name: ${e.message}`),
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// Check for existing project
|
|
161
|
+
if (this.workspaceService.exists(name)) {
|
|
162
|
+
const existingBindings = this.bindingRepo.findByWorkspacePathAndGuildId(name, guild.id);
|
|
163
|
+
if (existingBindings.length > 0) {
|
|
164
|
+
const channelLinks = existingBindings.map(b => `<#${b.channelId}>`).join(', ');
|
|
165
|
+
await interaction.editReply({
|
|
166
|
+
content: (0, i18n_1.t)(`⚠️ Project **${name}** already exists → ${channelLinks}`),
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Directory exists but not bound -- continue
|
|
171
|
+
}
|
|
172
|
+
// Lock project being processed
|
|
173
|
+
if (this.processingWorkspaces.has(name)) {
|
|
174
|
+
await interaction.editReply({
|
|
175
|
+
content: (0, i18n_1.t)(`⏳ **${name}** is being created.`),
|
|
176
|
+
});
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
this.processingWorkspaces.add(name);
|
|
180
|
+
try {
|
|
181
|
+
if (!this.workspaceService.exists(name)) {
|
|
182
|
+
// Create directory
|
|
183
|
+
fs_1.default.mkdirSync(fullPath, { recursive: true });
|
|
184
|
+
}
|
|
185
|
+
// Ensure category exists
|
|
186
|
+
const categoryResult = await this.channelManager.ensureCategory(guild, name);
|
|
187
|
+
const categoryId = categoryResult.categoryId;
|
|
188
|
+
// Get session number (usually 1)
|
|
189
|
+
const sessionNumber = this.chatSessionRepo.getNextSessionNumber(categoryId);
|
|
190
|
+
const channelName = `session-${sessionNumber}`;
|
|
191
|
+
// Create session channel
|
|
192
|
+
const sessionResult = await this.channelManager.createSessionChannel(guild, categoryId, channelName);
|
|
193
|
+
const channelId = sessionResult.channelId;
|
|
194
|
+
// Register binding and session
|
|
195
|
+
this.bindingRepo.upsert({
|
|
196
|
+
channelId,
|
|
197
|
+
workspacePath: name,
|
|
198
|
+
guildId: guild.id,
|
|
199
|
+
});
|
|
200
|
+
this.chatSessionRepo.create({
|
|
201
|
+
channelId,
|
|
202
|
+
categoryId,
|
|
203
|
+
workspacePath: name,
|
|
204
|
+
sessionNumber,
|
|
205
|
+
guildId: guild.id,
|
|
206
|
+
});
|
|
207
|
+
const embed = new discord_js_1.EmbedBuilder()
|
|
208
|
+
.setTitle('📁 Project Created')
|
|
209
|
+
.setColor(0x00AA00)
|
|
210
|
+
.setDescription((0, i18n_1.t)(`✅ Project **${name}** created\n`) +
|
|
211
|
+
`→ <#${channelId}>`)
|
|
212
|
+
.addFields({ name: (0, i18n_1.t)('Full Path'), value: `\`${fullPath}\`` })
|
|
213
|
+
.setTimestamp();
|
|
214
|
+
await interaction.editReply({ embeds: [embed] });
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
this.processingWorkspaces.delete(name);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Get the bound project path from a channel ID
|
|
222
|
+
*/
|
|
223
|
+
getWorkspaceForChannel(channelId) {
|
|
224
|
+
const binding = this.bindingRepo.findByChannelId(channelId);
|
|
225
|
+
if (!binding)
|
|
226
|
+
return undefined;
|
|
227
|
+
return this.workspaceService.getWorkspacePath(binding.workspacePath);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
exports.WorkspaceCommandHandler = WorkspaceCommandHandler;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ChatSessionRepository = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Repository for persisting Discord channel to chat session mapping in SQLite.
|
|
6
|
+
* One session per channel (UNIQUE constraint).
|
|
7
|
+
*/
|
|
8
|
+
class ChatSessionRepository {
|
|
9
|
+
db;
|
|
10
|
+
constructor(db) {
|
|
11
|
+
this.db = db;
|
|
12
|
+
this.initialize();
|
|
13
|
+
}
|
|
14
|
+
initialize() {
|
|
15
|
+
this.db.exec(`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS chat_sessions (
|
|
17
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
18
|
+
channel_id TEXT NOT NULL UNIQUE,
|
|
19
|
+
category_id TEXT NOT NULL,
|
|
20
|
+
workspace_path TEXT NOT NULL,
|
|
21
|
+
session_number INTEGER NOT NULL,
|
|
22
|
+
display_name TEXT,
|
|
23
|
+
is_renamed INTEGER NOT NULL DEFAULT 0,
|
|
24
|
+
guild_id TEXT NOT NULL,
|
|
25
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
26
|
+
)
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
create(input) {
|
|
30
|
+
const stmt = this.db.prepare(`
|
|
31
|
+
INSERT INTO chat_sessions (channel_id, category_id, workspace_path, session_number, guild_id)
|
|
32
|
+
VALUES (?, ?, ?, ?, ?)
|
|
33
|
+
`);
|
|
34
|
+
const result = stmt.run(input.channelId, input.categoryId, input.workspacePath, input.sessionNumber, input.guildId);
|
|
35
|
+
return {
|
|
36
|
+
id: result.lastInsertRowid,
|
|
37
|
+
channelId: input.channelId,
|
|
38
|
+
categoryId: input.categoryId,
|
|
39
|
+
workspacePath: input.workspacePath,
|
|
40
|
+
sessionNumber: input.sessionNumber,
|
|
41
|
+
displayName: null,
|
|
42
|
+
isRenamed: false,
|
|
43
|
+
guildId: input.guildId,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
findByChannelId(channelId) {
|
|
47
|
+
const row = this.db.prepare('SELECT * FROM chat_sessions WHERE channel_id = ?').get(channelId);
|
|
48
|
+
if (!row)
|
|
49
|
+
return undefined;
|
|
50
|
+
return this.mapRow(row);
|
|
51
|
+
}
|
|
52
|
+
findByCategoryId(categoryId) {
|
|
53
|
+
const rows = this.db.prepare('SELECT * FROM chat_sessions WHERE category_id = ? ORDER BY session_number ASC').all(categoryId);
|
|
54
|
+
return rows.map(this.mapRow);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get the next session number within a category (MAX + 1, or 1 if none)
|
|
58
|
+
*/
|
|
59
|
+
getNextSessionNumber(categoryId) {
|
|
60
|
+
const row = this.db.prepare('SELECT MAX(session_number) as max_num FROM chat_sessions WHERE category_id = ?').get(categoryId);
|
|
61
|
+
return (row?.max_num ?? 0) + 1;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Update session display name and set is_renamed to true
|
|
65
|
+
*/
|
|
66
|
+
updateDisplayName(channelId, displayName) {
|
|
67
|
+
const result = this.db.prepare('UPDATE chat_sessions SET display_name = ?, is_renamed = 1 WHERE channel_id = ?').run(displayName, channelId);
|
|
68
|
+
return result.changes > 0;
|
|
69
|
+
}
|
|
70
|
+
deleteByChannelId(channelId) {
|
|
71
|
+
const result = this.db.prepare('DELETE FROM chat_sessions WHERE channel_id = ?').run(channelId);
|
|
72
|
+
return result.changes > 0;
|
|
73
|
+
}
|
|
74
|
+
mapRow(row) {
|
|
75
|
+
return {
|
|
76
|
+
id: row.id,
|
|
77
|
+
channelId: row.channel_id,
|
|
78
|
+
categoryId: row.category_id,
|
|
79
|
+
workspacePath: row.workspace_path,
|
|
80
|
+
sessionNumber: row.session_number,
|
|
81
|
+
displayName: row.display_name,
|
|
82
|
+
isRenamed: row.is_renamed === 1,
|
|
83
|
+
guildId: row.guild_id,
|
|
84
|
+
createdAt: row.created_at,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
exports.ChatSessionRepository = ChatSessionRepository;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ScheduleRepository = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Repository class for SQLite persistence of scheduled jobs.
|
|
6
|
+
* Handles saving, retrieving, updating, and deleting cron expressions and prompts.
|
|
7
|
+
*/
|
|
8
|
+
class ScheduleRepository {
|
|
9
|
+
db;
|
|
10
|
+
constructor(db) {
|
|
11
|
+
this.db = db;
|
|
12
|
+
this.initialize();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Initialize table (create if not exists)
|
|
16
|
+
*/
|
|
17
|
+
initialize() {
|
|
18
|
+
this.db.exec(`
|
|
19
|
+
CREATE TABLE IF NOT EXISTS schedules (
|
|
20
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
21
|
+
cron_expression TEXT NOT NULL,
|
|
22
|
+
prompt TEXT NOT NULL,
|
|
23
|
+
workspace_path TEXT NOT NULL,
|
|
24
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
25
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
26
|
+
)
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Create a new schedule
|
|
31
|
+
*/
|
|
32
|
+
create(input) {
|
|
33
|
+
const stmt = this.db.prepare(`
|
|
34
|
+
INSERT INTO schedules (cron_expression, prompt, workspace_path, enabled)
|
|
35
|
+
VALUES (?, ?, ?, ?)
|
|
36
|
+
`);
|
|
37
|
+
const result = stmt.run(input.cronExpression, input.prompt, input.workspacePath, input.enabled ? 1 : 0);
|
|
38
|
+
return {
|
|
39
|
+
id: result.lastInsertRowid,
|
|
40
|
+
cronExpression: input.cronExpression,
|
|
41
|
+
prompt: input.prompt,
|
|
42
|
+
workspacePath: input.workspacePath,
|
|
43
|
+
enabled: input.enabled,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get all schedules
|
|
48
|
+
*/
|
|
49
|
+
findAll() {
|
|
50
|
+
const rows = this.db.prepare('SELECT * FROM schedules ORDER BY id ASC').all();
|
|
51
|
+
return rows.map(this.mapRow);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get a schedule by ID
|
|
55
|
+
*/
|
|
56
|
+
findById(id) {
|
|
57
|
+
const row = this.db.prepare('SELECT * FROM schedules WHERE id = ?').get(id);
|
|
58
|
+
if (!row)
|
|
59
|
+
return undefined;
|
|
60
|
+
return this.mapRow(row);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get only enabled schedules (for re-registration on bot startup)
|
|
64
|
+
*/
|
|
65
|
+
findEnabled() {
|
|
66
|
+
const rows = this.db.prepare('SELECT * FROM schedules WHERE enabled = 1 ORDER BY id ASC').all();
|
|
67
|
+
return rows.map(this.mapRow);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Delete a schedule
|
|
71
|
+
*/
|
|
72
|
+
delete(id) {
|
|
73
|
+
const result = this.db.prepare('DELETE FROM schedules WHERE id = ?').run(id);
|
|
74
|
+
return result.changes > 0;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Partially update a schedule
|
|
78
|
+
*/
|
|
79
|
+
update(id, input) {
|
|
80
|
+
const sets = [];
|
|
81
|
+
const values = [];
|
|
82
|
+
if (input.cronExpression !== undefined) {
|
|
83
|
+
sets.push('cron_expression = ?');
|
|
84
|
+
values.push(input.cronExpression);
|
|
85
|
+
}
|
|
86
|
+
if (input.prompt !== undefined) {
|
|
87
|
+
sets.push('prompt = ?');
|
|
88
|
+
values.push(input.prompt);
|
|
89
|
+
}
|
|
90
|
+
if (input.workspacePath !== undefined) {
|
|
91
|
+
sets.push('workspace_path = ?');
|
|
92
|
+
values.push(input.workspacePath);
|
|
93
|
+
}
|
|
94
|
+
if (input.enabled !== undefined) {
|
|
95
|
+
sets.push('enabled = ?');
|
|
96
|
+
values.push(input.enabled ? 1 : 0);
|
|
97
|
+
}
|
|
98
|
+
if (sets.length === 0)
|
|
99
|
+
return false;
|
|
100
|
+
values.push(id);
|
|
101
|
+
const sql = `UPDATE schedules SET ${sets.join(', ')} WHERE id = ?`;
|
|
102
|
+
const result = this.db.prepare(sql).run(...values);
|
|
103
|
+
return result.changes > 0;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Map a DB row to ScheduleRecord
|
|
107
|
+
*/
|
|
108
|
+
mapRow(row) {
|
|
109
|
+
return {
|
|
110
|
+
id: row.id,
|
|
111
|
+
cronExpression: row.cron_expression,
|
|
112
|
+
prompt: row.prompt,
|
|
113
|
+
workspacePath: row.workspace_path,
|
|
114
|
+
enabled: row.enabled === 1,
|
|
115
|
+
createdAt: row.created_at,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.ScheduleRepository = ScheduleRepository;
|