lazy-gravity 0.7.1 โ†’ 0.8.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.
package/dist/bot/index.js CHANGED
@@ -58,6 +58,7 @@ const accountPreferenceRepository_1 = require("../database/accountPreferenceRepo
58
58
  const workspaceBindingRepository_1 = require("../database/workspaceBindingRepository");
59
59
  const channelPreferenceRepository_1 = require("../database/channelPreferenceRepository");
60
60
  const chatSessionRepository_1 = require("../database/chatSessionRepository");
61
+ const artifactThreadRepository_1 = require("../database/artifactThreadRepository");
61
62
  const workspaceService_1 = require("../services/workspaceService");
62
63
  const workspaceCommandHandler_1 = require("../commands/workspaceCommandHandler");
63
64
  const chatCommandHandler_1 = require("../commands/chatCommandHandler");
@@ -84,6 +85,8 @@ const autoAcceptUi_1 = require("../ui/autoAcceptUi");
84
85
  const accountUi_1 = require("../ui/accountUi");
85
86
  const outputUi_1 = require("../ui/outputUi");
86
87
  const screenshotUi_1 = require("../ui/screenshotUi");
88
+ const artifactsUi_1 = require("../ui/artifactsUi");
89
+ const artifactService_1 = require("../services/artifactService");
87
90
  const userPreferenceRepository_1 = require("../database/userPreferenceRepository");
88
91
  const accountUtils_1 = require("../utils/accountUtils");
89
92
  const plainTextFormatter_1 = require("../utils/plainTextFormatter");
@@ -706,6 +709,13 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
706
709
  await options.channelManager.renameChannel(message.guild, message.channelId, formattedName);
707
710
  options.chatSessionRepo.updateDisplayName(message.channelId, sessionInfo.title);
708
711
  }
712
+ // Persist conversation_id for artifact picker resolution
713
+ if (options.artifactService) {
714
+ const convId = options.artifactService.findConversationByTitle(sessionInfo.title);
715
+ if (convId) {
716
+ options.chatSessionRepo.setConversationId(message.channelId, convId);
717
+ }
718
+ }
709
719
  }
710
720
  }
711
721
  catch (e) {
@@ -812,6 +822,8 @@ const startBot = async (cliLogLevel) => {
812
822
  }
813
823
  const workspaceBindingRepo = new workspaceBindingRepository_1.WorkspaceBindingRepository(db);
814
824
  const chatSessionRepo = new chatSessionRepository_1.ChatSessionRepository(db);
825
+ const artifactThreadRepo = new artifactThreadRepository_1.ArtifactThreadRepository(db);
826
+ const artifactService = new artifactService_1.ArtifactService();
815
827
  const workspaceService = new workspaceService_1.WorkspaceService(config.workspaceBaseDir);
816
828
  const channelManager = new channelManager_1.ChannelManager();
817
829
  // Auto-launch Antigravity with CDP port if not already running
@@ -967,8 +979,10 @@ const startBot = async (cliLogLevel) => {
967
979
  channelPrefRepo,
968
980
  chatSessionRepo,
969
981
  chatSessionService,
982
+ artifactThreadRepo,
983
+ artifactService,
970
984
  antigravityAccounts: config.antigravityAccounts,
971
- handleSlashInteraction: async (interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, accountPrefRepoArg, channelPrefRepoArg, antigravityAccountsArg) => handleSlashInteraction(interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, chatSessionService, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, promptDispatcher, templateRepo, joinHandler, userPrefRepo, accountPrefRepoArg, channelPrefRepoArg, antigravityAccountsArg, chatSessionRepo),
985
+ handleSlashInteraction: async (interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, accountPrefRepoArg, channelPrefRepoArg, antigravityAccountsArg) => handleSlashInteraction(interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, chatSessionService, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, promptDispatcher, templateRepo, joinHandler, userPrefRepo, accountPrefRepoArg, channelPrefRepoArg, antigravityAccountsArg, chatSessionRepo, artifactService),
972
986
  handleTemplateUse: async (interaction, templateId) => {
973
987
  const template = templateRepo.findById(templateId);
974
988
  if (!template) {
@@ -1055,6 +1069,7 @@ const startBot = async (cliLogLevel) => {
1055
1069
  channelManager,
1056
1070
  titleGenerator,
1057
1071
  userPrefRepo,
1072
+ artifactService,
1058
1073
  extractionMode: config.extractionMode,
1059
1074
  },
1060
1075
  });
@@ -1073,6 +1088,7 @@ const startBot = async (cliLogLevel) => {
1073
1088
  chatSessionRepo,
1074
1089
  channelManager,
1075
1090
  titleGenerator,
1091
+ artifactService,
1076
1092
  client,
1077
1093
  sendPromptToAntigravity: async (_bridge, message, prompt, cdp, _modeService, _modelService, inboundImages = [], options) => promptDispatcher.send({
1078
1094
  message,
@@ -1261,6 +1277,7 @@ const startBot = async (cliLogLevel) => {
1261
1277
  { command: 'stop', description: 'Interrupt active LLM generation' },
1262
1278
  { command: 'help', description: 'Show available commands' },
1263
1279
  { command: 'ping', description: 'Check bot latency' },
1280
+ { command: 'artifacts', description: 'Browse session artifacts' },
1264
1281
  ]).catch((e) => {
1265
1282
  logger_1.logger.warn('Failed to register Telegram commands:', e instanceof Error ? e.message : e);
1266
1283
  });
@@ -1352,7 +1369,7 @@ async function autoRenameChannel(message, chatSessionRepo, titleGenerator, chann
1352
1369
  /**
1353
1370
  * Handle Discord Interactions API slash commands
1354
1371
  */
1355
- async function handleSlashInteraction(interaction, handler, bridge, wsHandler, chatHandler, cleanupHandler, chatSessionService, modeService, modelService, autoAcceptService, _client, promptDispatcher, templateRepo, joinHandler, userPrefRepo, accountPrefRepo, channelPrefRepo, antigravityAccounts = [{ name: 'default', cdpPort: 9222 }], chatSessionRepo) {
1372
+ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, chatHandler, cleanupHandler, chatSessionService, modeService, modelService, autoAcceptService, _client, promptDispatcher, templateRepo, joinHandler, userPrefRepo, accountPrefRepo, channelPrefRepo, antigravityAccounts = [{ name: 'default', cdpPort: 9222 }], chatSessionRepo, artifactService) {
1356
1373
  const commandName = interaction.commandName;
1357
1374
  const getAccountPort = (accountName) => {
1358
1375
  const match = antigravityAccounts.find((account) => account.name === accountName);
@@ -1476,6 +1493,11 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1476
1493
  '`/help` โ€” Show this help',
1477
1494
  ].join('\n')
1478
1495
  },
1496
+ {
1497
+ name: '๐Ÿ“‚ Artifacts', value: [
1498
+ '`/artifacts` โ€” Browse and render generated artifacts from the active session',
1499
+ ].join('\n')
1500
+ },
1479
1501
  ];
1480
1502
  const helpOutputFormat = userPrefRepo?.getOutputFormat(interaction.user.id) ?? 'embed';
1481
1503
  if (helpOutputFormat === 'plain') {
@@ -1828,6 +1850,14 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1828
1850
  await interaction.editReply({ content: codeBlock });
1829
1851
  break;
1830
1852
  }
1853
+ case 'artifacts': {
1854
+ await (0, artifactsUi_1.sendArtifactPickerUI)(interaction, {
1855
+ userPrefRepo,
1856
+ chatSessionRepo,
1857
+ artifactService
1858
+ });
1859
+ break;
1860
+ }
1831
1861
  default:
1832
1862
  await interaction.editReply({
1833
1863
  content: `Unknown command: /${commandName}`,
@@ -148,6 +148,10 @@ const logsCommand = new discord_js_1.SlashCommandBuilder()
148
148
  const pingCommand = new discord_js_1.SlashCommandBuilder()
149
149
  .setName('ping')
150
150
  .setDescription((0, i18n_1.t)('Check bot latency'));
151
+ /** /artifacts command definition */
152
+ const artifactsCommand = new discord_js_1.SlashCommandBuilder()
153
+ .setName('artifacts')
154
+ .setDescription((0, i18n_1.t)('Browse and view generated artifacts from the active session'));
151
155
  /** Array of commands to register */
152
156
  exports.slashCommands = [
153
157
  helpCommand,
@@ -168,6 +172,7 @@ exports.slashCommands = [
168
172
  outputCommand,
169
173
  pingCommand,
170
174
  logsCommand,
175
+ artifactsCommand,
171
176
  ];
172
177
  /**
173
178
  * Register slash commands with Discord
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ArtifactThreadRepository = void 0;
4
+ /**
5
+ * Repository for mapping (channel_id, conversation_id, filename) to Discord thread ID.
6
+ * This allows reusing the same thread for multiple renders of the same file.
7
+ */
8
+ class ArtifactThreadRepository {
9
+ db;
10
+ constructor(db) {
11
+ this.db = db;
12
+ this.initialize();
13
+ }
14
+ initialize() {
15
+ const tableInfo = this.db.prepare("PRAGMA table_info('artifact_threads')").all();
16
+ if (tableInfo.length === 0) {
17
+ this.db.exec(`
18
+ CREATE TABLE artifact_threads (
19
+ channel_id TEXT NOT NULL,
20
+ conversation_id TEXT NOT NULL,
21
+ filename TEXT NOT NULL,
22
+ thread_id TEXT NOT NULL,
23
+ PRIMARY KEY (channel_id, conversation_id, filename)
24
+ )
25
+ `);
26
+ }
27
+ else {
28
+ const hasConvId = tableInfo.some(col => col.name === 'conversation_id');
29
+ if (!hasConvId) {
30
+ // To safely migrate in sqlite, we recreate the table, since we need to change the primary key
31
+ this.db.exec(`
32
+ CREATE TABLE artifact_threads_new (
33
+ channel_id TEXT NOT NULL,
34
+ conversation_id TEXT NOT NULL DEFAULT '',
35
+ filename TEXT NOT NULL,
36
+ thread_id TEXT NOT NULL,
37
+ PRIMARY KEY (channel_id, conversation_id, filename)
38
+ );
39
+ INSERT INTO artifact_threads_new (channel_id, conversation_id, filename, thread_id)
40
+ SELECT channel_id, '', filename, thread_id FROM artifact_threads;
41
+ DROP TABLE artifact_threads;
42
+ ALTER TABLE artifact_threads_new RENAME TO artifact_threads;
43
+ `);
44
+ }
45
+ }
46
+ }
47
+ /**
48
+ * Get the stored thread ID for a specific file in a channel and conversation.
49
+ */
50
+ getThreadId(channelId, conversationId, filename) {
51
+ const row = this.db.prepare('SELECT thread_id FROM artifact_threads WHERE channel_id = ? AND conversation_id = ? AND filename = ?').get(channelId, conversationId, filename);
52
+ return row?.thread_id ?? null;
53
+ }
54
+ /**
55
+ * Set the thread ID for a specific file in a channel and conversation.
56
+ */
57
+ setThreadId(channelId, conversationId, filename, threadId) {
58
+ this.db.prepare(`
59
+ INSERT OR REPLACE INTO artifact_threads (channel_id, conversation_id, filename, thread_id)
60
+ VALUES (?, ?, ?, ?)
61
+ `).run(channelId, conversationId, filename, threadId);
62
+ }
63
+ /**
64
+ * Remove a stored thread ID.
65
+ */
66
+ deleteThreadId(channelId, conversationId, filename) {
67
+ this.db.prepare('DELETE FROM artifact_threads WHERE channel_id = ? AND conversation_id = ? AND filename = ?').run(channelId, conversationId, filename);
68
+ }
69
+ }
70
+ exports.ArtifactThreadRepository = ArtifactThreadRepository;
@@ -25,6 +25,7 @@ class UserPreferenceRepository {
25
25
  )
26
26
  `);
27
27
  this.migrateDefaultModel();
28
+ this.migrateArtifactRenderMode();
28
29
  }
29
30
  /**
30
31
  * Safe migration: add default_model column if it does not exist.
@@ -48,6 +49,16 @@ class UserPreferenceRepository {
48
49
  }
49
50
  }
50
51
  }
52
+ /**
53
+ * Migration: add artifact_render_mode column.
54
+ * Default is 'thread'.
55
+ */
56
+ migrateArtifactRenderMode() {
57
+ const columns = this.db.pragma('table_info(user_preferences)');
58
+ if (!columns.some(c => c.name === 'artifact_render_mode')) {
59
+ this.db.exec("ALTER TABLE user_preferences ADD COLUMN artifact_render_mode TEXT DEFAULT 'thread'");
60
+ }
61
+ }
51
62
  /**
52
63
  * Get the output format preference for a user.
53
64
  * Returns 'embed' as default if no preference is stored.
@@ -78,6 +89,28 @@ class UserPreferenceRepository {
78
89
  const row = this.db.prepare('SELECT default_model FROM user_preferences WHERE user_id = ?').get(userId);
79
90
  return row?.default_model ?? null;
80
91
  }
92
+ /**
93
+ * Get the artifact render mode preference for a user.
94
+ * Returns 'thread' as default.
95
+ */
96
+ getArtifactRenderMode(userId) {
97
+ const row = this.db.prepare('SELECT artifact_render_mode FROM user_preferences WHERE user_id = ?').get(userId);
98
+ if (!row || row.artifact_render_mode === 'thread')
99
+ return 'thread';
100
+ return 'inline';
101
+ }
102
+ /**
103
+ * Set the artifact render mode preference (upsert).
104
+ */
105
+ setArtifactRenderMode(userId, mode) {
106
+ this.db.prepare(`
107
+ INSERT INTO user_preferences (user_id, artifact_render_mode)
108
+ VALUES (?, ?)
109
+ ON CONFLICT(user_id)
110
+ DO UPDATE SET artifact_render_mode = excluded.artifact_render_mode,
111
+ updated_at = datetime('now')
112
+ `).run(userId, mode);
113
+ }
81
114
  /**
82
115
  * Set the default model for a user (upsert).
83
116
  * Pass null to clear the default.
@@ -111,6 +144,7 @@ class UserPreferenceRepository {
111
144
  defaultModel: row.default_model ?? null,
112
145
  createdAt: row.created_at,
113
146
  updatedAt: row.updated_at,
147
+ artifactRenderMode: row.artifact_render_mode ?? 'thread',
114
148
  };
115
149
  }
116
150
  }
@@ -12,10 +12,20 @@ const cleanupCommandHandler_1 = require("../commands/cleanupCommandHandler");
12
12
  const projectListUi_1 = require("../ui/projectListUi");
13
13
  const modeService_1 = require("../services/modeService");
14
14
  const sessionPickerUi_1 = require("../ui/sessionPickerUi");
15
+ const artifactsUi_1 = require("../ui/artifactsUi");
16
+ const artifactService_1 = require("../services/artifactService");
15
17
  const accountUtils_1 = require("../utils/accountUtils");
16
18
  const accountUi_1 = require("../ui/accountUi");
17
19
  function createInteractionCreateHandler(deps) {
18
- const getParentChannelId = (interaction) => (0, accountUtils_1.inferParentScopeChannelId)(interaction.channelId, interaction.channel?.parentId ?? null);
20
+ const getParentChannelId = (interaction) => {
21
+ const channelId = interaction.channelId;
22
+ if (!channelId)
23
+ return null;
24
+ const parentId = 'channel' in interaction && interaction.channel && 'parentId' in interaction.channel
25
+ ? interaction.channel.parentId
26
+ : null;
27
+ return (0, accountUtils_1.inferParentScopeChannelId)(channelId, parentId);
28
+ };
19
29
  const getSessionAccountName = (channelId) => deps.chatSessionRepo?.findByChannelId(channelId)?.activeAccountName ?? null;
20
30
  const resolveSelectedAccount = (channelId, userId, parentChannelId) => (0, accountUtils_1.resolveScopedAccountName)({
21
31
  channelId,
@@ -664,6 +674,20 @@ function createInteractionCreateHandler(deps) {
664
674
  }
665
675
  return;
666
676
  }
677
+ if (interaction.customId.startsWith(`${artifactsUi_1.ARTIFACT_THREAD_BTN}:`) || interaction.customId.startsWith(`${artifactsUi_1.ARTIFACT_INLINE_BTN}:`)) {
678
+ const newMode = interaction.customId.startsWith(`${artifactsUi_1.ARTIFACT_THREAD_BTN}:`) ? 'thread' : 'inline';
679
+ const passedConvId = interaction.customId.split(':')[1] || null;
680
+ if (deps.userPrefRepo) {
681
+ deps.userPrefRepo.setArtifactRenderMode(interaction.user.id, newMode);
682
+ }
683
+ await interaction.deferUpdate().catch(logger_1.logger.error);
684
+ await (0, artifactsUi_1.sendArtifactPickerUI)(interaction, {
685
+ userPrefRepo: deps.userPrefRepo,
686
+ chatSessionRepo: deps.chatSessionRepo,
687
+ artifactService: deps.artifactService
688
+ }, true, passedConvId);
689
+ return;
690
+ }
667
691
  }
668
692
  catch (error) {
669
693
  logger_1.logger.error('Error during button interaction handling:', error);
@@ -817,6 +841,140 @@ function createInteractionCreateHandler(deps) {
817
841
  }
818
842
  return;
819
843
  }
844
+ if (interaction.isStringSelectMenu() && interaction.customId === artifactsUi_1.ARTIFACT_SELECT_ID) {
845
+ if (!deps.config.allowedUserIds.includes(interaction.user.id)) {
846
+ await interaction.reply({ content: (0, i18n_1.t)('You do not have permission.'), flags: discord_js_1.MessageFlags.Ephemeral }).catch(logger_1.logger.error);
847
+ return;
848
+ }
849
+ try {
850
+ await interaction.deferUpdate();
851
+ }
852
+ catch (deferError) {
853
+ if (deferError?.code === 10062 || deferError?.code === 40060) {
854
+ logger_1.logger.warn('[ArtifactSelect] deferUpdate expired. Skipping.');
855
+ return;
856
+ }
857
+ logger_1.logger.error('[ArtifactSelect] deferUpdate failed:', deferError);
858
+ return;
859
+ }
860
+ const selectedValue = interaction.values[0];
861
+ if (!selectedValue) {
862
+ await interaction.editReply({ content: 'No artifact selected.', components: [], allowedMentions: { parse: [] } }).catch(logger_1.logger.error);
863
+ return;
864
+ }
865
+ try {
866
+ const artifactService = new artifactService_1.ArtifactService();
867
+ // Resolve the selected artifact by rescanning and matching the encoded value
868
+ const channelId = interaction.channelId;
869
+ const sessionTitle = deps.chatSessionRepo?.findByChannelId(channelId)?.displayName?.trim() ?? '';
870
+ let conversationId = sessionTitle ? artifactService.findConversationByTitle(sessionTitle) : null;
871
+ if (!conversationId)
872
+ conversationId = artifactService.getLatestConversationWithArtifacts();
873
+ if (!conversationId) {
874
+ await interaction.editReply({ content: '๐Ÿ“‚ No artifacts found.', components: [], allowedMentions: { parse: [] } }).catch(logger_1.logger.error);
875
+ return;
876
+ }
877
+ const artifacts = artifactService.listArtifacts(conversationId);
878
+ const decoded = artifactService.decodeSelectValue(selectedValue, artifacts);
879
+ if (!decoded) {
880
+ logger_1.logger.warn(`[ArtifactSelect] Could not decode value: ${selectedValue}`);
881
+ await interaction.editReply({ content: 'โš ๏ธ Could not identify the selected artifact.', components: [], allowedMentions: { parse: [] } }).catch(logger_1.logger.error);
882
+ return;
883
+ }
884
+ const content = artifactService.getArtifactContent(decoded.conversationId, decoded.filename);
885
+ if (!content) {
886
+ await interaction.editReply({ content: 'โš ๏ธ Could not read artifact content.', components: [], allowedMentions: { parse: [] } }).catch(logger_1.logger.error);
887
+ return;
888
+ }
889
+ // Get user render mode
890
+ const renderMode = deps.userPrefRepo?.getArtifactRenderMode(interaction.user.id) ?? 'thread';
891
+ let targetChannel = interaction.channel;
892
+ if (renderMode === 'thread' && deps.artifactThreadRepo && interaction.channel?.threads) {
893
+ try {
894
+ const existingThreadId = deps.artifactThreadRepo.getThreadId(channelId, conversationId, decoded.filename);
895
+ let thread = null;
896
+ if (existingThreadId) {
897
+ try {
898
+ thread = await interaction.client.channels.fetch(existingThreadId);
899
+ if (thread && thread.archived) {
900
+ await thread.setArchived(false);
901
+ }
902
+ }
903
+ catch (fetchError) {
904
+ logger_1.logger.warn(`[ArtifactSelect] Could not fetch existing thread ${existingThreadId}:`, fetchError);
905
+ deps.artifactThreadRepo.deleteThreadId(channelId, conversationId, decoded.filename);
906
+ }
907
+ }
908
+ if (!thread) {
909
+ // Create new thread from the prompt message
910
+ const message = await interaction.editReply({
911
+ content: `๐Ÿ“‚ **Artifact: ${decoded.filename}**\n*Creating thread for persistent viewing...*`,
912
+ components: [],
913
+ allowedMentions: { parse: [] }
914
+ });
915
+ thread = await message.startThread({
916
+ name: `artifact: ${decoded.filename}`,
917
+ autoArchiveDuration: 10080,
918
+ });
919
+ deps.artifactThreadRepo.setThreadId(channelId, conversationId, decoded.filename, thread.id);
920
+ // Update the main message to link to the thread
921
+ await interaction.editReply({
922
+ content: `๐Ÿ“‚ **Artifact: ${decoded.filename}**\n*Thread created: <#${thread.id}>*`,
923
+ components: [],
924
+ allowedMentions: { parse: [] }
925
+ }).catch(logger_1.logger.error);
926
+ }
927
+ else {
928
+ // Reuse existing thread
929
+ await interaction.editReply({
930
+ content: `๐Ÿ“‚ **Artifact: ${decoded.filename}**\n*Updated in existing thread: <#${thread.id}>*`,
931
+ components: [],
932
+ allowedMentions: { parse: [] }
933
+ }).catch(logger_1.logger.error);
934
+ // Send a visual spacer to separate content
935
+ const now = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
936
+ await thread.send({ content: `โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n๐Ÿ”„ **Artifact Re-rendered at ${now}**`, allowedMentions: { parse: [] } }).catch(logger_1.logger.error);
937
+ }
938
+ targetChannel = thread;
939
+ }
940
+ catch (threadError) {
941
+ logger_1.logger.error('[ArtifactSelect] Thread creation/resume failed, falling back to inline:', threadError);
942
+ await interaction.editReply({ content: `๐Ÿ“‚ **${decoded.filename}**`, components: [], allowedMentions: { parse: [] } }).catch(logger_1.logger.error);
943
+ }
944
+ }
945
+ else {
946
+ // Inline rendering
947
+ await interaction.editReply({ content: `๐Ÿ“‚ **${decoded.filename}**`, components: [], allowedMentions: { parse: [] } }).catch(logger_1.logger.error);
948
+ }
949
+ // Split into 2000-char chunks and send as follow-ups or thread messages
950
+ const MAX_CHUNK = 1990;
951
+ let remaining = content;
952
+ while (remaining.length > 0) {
953
+ let chunk;
954
+ if (remaining.length <= MAX_CHUNK) {
955
+ chunk = remaining;
956
+ remaining = '';
957
+ }
958
+ else {
959
+ const splitAt = remaining.lastIndexOf('\n', MAX_CHUNK);
960
+ chunk = splitAt > 0 ? remaining.slice(0, splitAt) : remaining.slice(0, MAX_CHUNK);
961
+ remaining = remaining.slice(chunk.length).replace(/^\n/, '');
962
+ }
963
+ if (targetChannel && targetChannel.send) {
964
+ await targetChannel.send({ content: chunk, allowedMentions: { parse: [] } }).catch(logger_1.logger.error);
965
+ }
966
+ else {
967
+ await interaction.followUp({ content: chunk, allowedMentions: { parse: [] } }).catch(logger_1.logger.error);
968
+ }
969
+ }
970
+ logger_1.logger.info(`[ArtifactSelect] Served artifact ${decoded.conversationId}/${decoded.filename} to channel=${channelId} (mode=${renderMode})`);
971
+ }
972
+ catch (error) {
973
+ logger_1.logger.error('[ArtifactSelect] Error serving artifact:', error);
974
+ await interaction.editReply({ content: 'โŒ Error reading artifact.', components: [], allowedMentions: { parse: [] } }).catch(logger_1.logger.error);
975
+ }
976
+ return;
977
+ }
820
978
  if (interaction.isStringSelectMenu() && (0, projectListUi_1.isProjectSelectId)(interaction.customId)) {
821
979
  if (!deps.config.allowedUserIds.includes(interaction.user.id)) {
822
980
  await interaction.reply({ content: (0, i18n_1.t)('You do not have permission.'), flags: discord_js_1.MessageFlags.Ephemeral }).catch(logger_1.logger.error);
@@ -179,6 +179,7 @@ function createMessageCreateHandler(deps) {
179
179
  channelManager: deps.channelManager,
180
180
  titleGenerator: deps.titleGenerator,
181
181
  userPrefRepo: deps.userPrefRepo,
182
+ artifactService: deps.artifactService,
182
183
  extractionMode: deps.config.extractionMode,
183
184
  responseTimeoutMs: deps.config.responseTimeoutMs,
184
185
  });
@@ -358,6 +359,7 @@ function createMessageCreateHandler(deps) {
358
359
  channelManager: deps.channelManager,
359
360
  titleGenerator: deps.titleGenerator,
360
361
  userPrefRepo: deps.userPrefRepo,
362
+ artifactService: deps.artifactService,
361
363
  extractionMode: deps.config.extractionMode,
362
364
  responseTimeoutMs: deps.config.responseTimeoutMs,
363
365
  onFullCompletion: settle,
@@ -0,0 +1,282 @@
1
+ "use strict";
2
+ /**
3
+ * Artifact Service โ€” reads Antigravity artifacts from the local filesystem.
4
+ *
5
+ * Antigravity persists artifacts (implementation plans, tasks, walkthroughs)
6
+ * as Markdown files with companion `.metadata.json` files in:
7
+ * %USERPROFILE%/.gemini/antigravity/brain/<conversation-id>/
8
+ *
9
+ * This service locates relevant conversations and surfaces their artifacts
10
+ * for the /artifacts Discord command.
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.ArtifactService = void 0;
47
+ exports.artifactTypeLabel = artifactTypeLabel;
48
+ const fs = __importStar(require("fs"));
49
+ const path = __importStar(require("path"));
50
+ const os = __importStar(require("os"));
51
+ const logger_1 = require("../utils/logger");
52
+ // ---------------------------------------------------------------------------
53
+ // Helpers
54
+ // ---------------------------------------------------------------------------
55
+ const ARTIFACT_TYPE_LABELS = {
56
+ ARTIFACT_TYPE_IMPLEMENTATION_PLAN: '๐Ÿ“‹ Plan',
57
+ ARTIFACT_TYPE_TASK: 'โœ… Task',
58
+ ARTIFACT_TYPE_WALKTHROUGH: '๐Ÿšถ Walkthrough',
59
+ ARTIFACT_TYPE_OTHER: '๐Ÿ“„ Other',
60
+ };
61
+ function artifactTypeLabel(type) {
62
+ return ARTIFACT_TYPE_LABELS[type] ?? '๐Ÿ“„ Artifact';
63
+ }
64
+ // ---------------------------------------------------------------------------
65
+ // ArtifactService
66
+ // ---------------------------------------------------------------------------
67
+ class ArtifactService {
68
+ brainBasePath;
69
+ constructor(brainBasePath) {
70
+ this.brainBasePath =
71
+ brainBasePath ??
72
+ path.join(os.homedir(), '.gemini', 'antigravity', 'brain');
73
+ }
74
+ /**
75
+ * List all conversations in the brain directory (UUIDs).
76
+ */
77
+ listConversationIds() {
78
+ try {
79
+ if (!fs.existsSync(this.brainBasePath))
80
+ return [];
81
+ return fs.readdirSync(this.brainBasePath).filter((entry) => {
82
+ const full = path.join(this.brainBasePath, entry);
83
+ return (fs.statSync(full).isDirectory() &&
84
+ /^[0-9a-f-]{36}$/.test(entry));
85
+ });
86
+ }
87
+ catch (err) {
88
+ logger_1.logger.warn(`[ArtifactService] Failed to list brain directory: ${err}`);
89
+ return [];
90
+ }
91
+ }
92
+ /**
93
+ * Read the .metadata.json file for a given artifact in a conversation.
94
+ */
95
+ readMetadata(conversationId, mdFilename) {
96
+ const metaPath = path.join(this.brainBasePath, conversationId, `${mdFilename}.metadata.json`);
97
+ try {
98
+ if (!fs.existsSync(metaPath))
99
+ return null;
100
+ const raw = fs.readFileSync(metaPath, 'utf-8');
101
+ return JSON.parse(raw);
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ }
107
+ /**
108
+ * List all artifacts in a given conversation directory.
109
+ * An artifact must be a .md file with a companion .metadata.json.
110
+ */
111
+ listArtifacts(conversationId) {
112
+ const convDir = path.join(this.brainBasePath, conversationId);
113
+ try {
114
+ if (!fs.existsSync(convDir))
115
+ return [];
116
+ const entries = fs.readdirSync(convDir);
117
+ const artifacts = [];
118
+ for (const entry of entries) {
119
+ // Only look at .md files (not .metadata.json, .resolved, etc.)
120
+ if (!entry.endsWith('.md'))
121
+ continue;
122
+ const meta = this.readMetadata(conversationId, entry);
123
+ if (!meta)
124
+ continue; // Not a tracked artifact โ€” skip
125
+ artifacts.push({
126
+ conversationId,
127
+ filename: entry,
128
+ artifactType: meta.artifactType ?? 'ARTIFACT_TYPE_OTHER',
129
+ summary: meta.summary,
130
+ updatedAt: meta.updatedAt,
131
+ version: meta.version,
132
+ absolutePath: path.join(convDir, entry),
133
+ });
134
+ }
135
+ // Sort by updatedAt descending (most recent first)
136
+ artifacts.sort((a, b) => {
137
+ const ta = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
138
+ const tb = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
139
+ return tb - ta;
140
+ });
141
+ return artifacts;
142
+ }
143
+ catch (err) {
144
+ logger_1.logger.warn(`[ArtifactService] Failed to list artifacts for ${conversationId}: ${err}`);
145
+ return [];
146
+ }
147
+ }
148
+ /**
149
+ * Try to find a conversation UUID whose overview.txt contains the given session title.
150
+ * Returns the UUID or null if not found.
151
+ */
152
+ findConversationByTitle(title) {
153
+ if (!title || !title.trim())
154
+ return null;
155
+ const needle = title.trim().toLowerCase();
156
+ const ids = this.listConversationIds();
157
+ // Sort by directory mtime descending (most recent first)
158
+ const sortedIds = ids
159
+ .map((id) => {
160
+ const full = path.join(this.brainBasePath, id);
161
+ try {
162
+ return { id, mtime: fs.statSync(full).mtimeMs };
163
+ }
164
+ catch {
165
+ return { id, mtime: 0 };
166
+ }
167
+ })
168
+ .sort((a, b) => b.mtime - a.mtime)
169
+ .map(x => x.id);
170
+ for (const id of sortedIds) {
171
+ const overviewPath = path.join(this.brainBasePath, id, '.system_generated', 'logs', 'overview.txt');
172
+ try {
173
+ if (!fs.existsSync(overviewPath))
174
+ continue;
175
+ // Only read the first 4KB to avoid huge files
176
+ let fd = null;
177
+ try {
178
+ fd = fs.openSync(overviewPath, 'r');
179
+ const buf = Buffer.alloc(4096);
180
+ const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
181
+ const header = buf.slice(0, bytesRead).toString('utf-8').toLowerCase();
182
+ if (header.includes(needle)) {
183
+ return id;
184
+ }
185
+ }
186
+ finally {
187
+ if (fd !== null)
188
+ fs.closeSync(fd);
189
+ }
190
+ }
191
+ catch {
192
+ // Skip unreadable files
193
+ }
194
+ }
195
+ return null;
196
+ }
197
+ /**
198
+ * Return the conversation ID of the most recently modified conversation
199
+ * that has at least one artifact. Falls back to the most recent conversation
200
+ * overall if none have artifacts.
201
+ */
202
+ getLatestConversationWithArtifacts() {
203
+ const ids = this.listConversationIds();
204
+ if (ids.length === 0)
205
+ return null;
206
+ // Sort by directory mtime descending
207
+ const sorted = ids
208
+ .map((id) => {
209
+ const full = path.join(this.brainBasePath, id);
210
+ try {
211
+ return { id, mtime: fs.statSync(full).mtimeMs };
212
+ }
213
+ catch {
214
+ return { id, mtime: 0 };
215
+ }
216
+ })
217
+ .sort((a, b) => b.mtime - a.mtime);
218
+ // Find the first one that has artifacts
219
+ for (const { id } of sorted) {
220
+ if (this.listArtifacts(id).length > 0)
221
+ return id;
222
+ }
223
+ // Nothing has artifacts โ€” return most recent anyway (caller handles empty list)
224
+ return sorted[0]?.id ?? null;
225
+ }
226
+ /**
227
+ * Read the markdown content of a specific artifact file.
228
+ * Returns null if the file cannot be read.
229
+ */
230
+ getArtifactContent(conversationId, filename) {
231
+ // Sanitize: prevent path traversal
232
+ const safe = path.basename(filename);
233
+ if (!safe.endsWith('.md'))
234
+ return null;
235
+ const filePath = path.join(this.brainBasePath, conversationId, safe);
236
+ try {
237
+ if (!fs.existsSync(filePath))
238
+ return null;
239
+ return fs.readFileSync(filePath, 'utf-8');
240
+ }
241
+ catch (err) {
242
+ logger_1.logger.warn(`[ArtifactService] Failed to read artifact ${conversationId}/${safe}: ${err}`);
243
+ return null;
244
+ }
245
+ }
246
+ /**
247
+ * Encode conversationId and filename into a single string for Discord select menu values.
248
+ * We use a prefix 'art_', followed by a short slice of the conv ID, and the filename.
249
+ * Added a short hash of the filename to prevent collisions on long-filename truncation.
250
+ */
251
+ static encodeSelectValue(conversationId, filename) {
252
+ const shortConv = conversationId.replace(/-/g, '').slice(0, 12);
253
+ // Simple hash of the filename
254
+ let hash = 0;
255
+ for (let i = 0; i < filename.length; i++) {
256
+ hash = ((hash << 5) - hash) + filename.charCodeAt(i);
257
+ hash |= 0; // Convert to 32bit integer
258
+ }
259
+ const shortHash = Math.abs(hash).toString(36).slice(0, 4);
260
+ return `art_${shortConv}_${shortHash}_${filename}`;
261
+ }
262
+ /**
263
+ * Decode a select menu value back into conversationId and filename.
264
+ * Since we only have a slice of the conversationId, we must look it up
265
+ * in the provided list of artifacts.
266
+ */
267
+ decodeSelectValue(value, artifacts) {
268
+ if (!value.startsWith('art_'))
269
+ return null;
270
+ // Format: art_CONV_HASH_FILENAME
271
+ const parts = value.split('_');
272
+ if (parts.length < 4)
273
+ return null;
274
+ const shortConv = parts[1];
275
+ const filename = parts.slice(3).join('_'); // Filename might contain underscores
276
+ // Find the matching artifact in the current list
277
+ const found = artifacts.find(a => a.filename === filename &&
278
+ a.conversationId.replace(/-/g, '').startsWith(shortConv));
279
+ return found ? { conversationId: found.conversationId, filename: found.filename } : null;
280
+ }
281
+ }
282
+ exports.ArtifactService = ArtifactService;
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ /**
3
+ * Artifacts UI โ€” Discord embed + select menu for the /artifacts command.
4
+ *
5
+ * Follows the same pattern as sessionPickerUi.ts.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.ARTIFACT_INLINE_BTN = exports.ARTIFACT_THREAD_BTN = exports.ARTIFACT_SELECT_ID = void 0;
9
+ exports.buildArtifactPickerUI = buildArtifactPickerUI;
10
+ exports.sendArtifactsUI = sendArtifactsUI;
11
+ exports.sendArtifactPickerUI = sendArtifactPickerUI;
12
+ const discord_js_1 = require("discord.js");
13
+ const artifactService_1 = require("../services/artifactService");
14
+ // ---------------------------------------------------------------------------
15
+ // Constants
16
+ // ---------------------------------------------------------------------------
17
+ /** Custom ID for the artifact select menu */
18
+ exports.ARTIFACT_SELECT_ID = 'artifact_select';
19
+ /** Custom ID for the toggle buttons */
20
+ exports.ARTIFACT_THREAD_BTN = 'artifact_mode_thread';
21
+ exports.ARTIFACT_INLINE_BTN = 'artifact_mode_inline';
22
+ /** Discord select menu option limit */
23
+ const MAX_SELECT_OPTIONS = 25;
24
+ // ---------------------------------------------------------------------------
25
+ // Helpers
26
+ // ---------------------------------------------------------------------------
27
+ function truncate(str, max) {
28
+ if (str.length <= max)
29
+ return str;
30
+ return str.slice(0, max - 1) + 'โ€ฆ';
31
+ }
32
+ function formatOptionDescription(artifact) {
33
+ const parts = [];
34
+ parts.push((0, artifactService_1.artifactTypeLabel)(artifact.artifactType));
35
+ if (artifact.summary) {
36
+ parts.push(truncate(artifact.summary, 60));
37
+ }
38
+ const combined = parts.join(' ยท ');
39
+ // Discord limits descriptions to 100 chars
40
+ return combined.length > 0 ? truncate(combined, 100) : undefined;
41
+ }
42
+ function formatUpdatedAt(iso) {
43
+ if (!iso)
44
+ return '';
45
+ try {
46
+ return new Date(iso).toLocaleString(undefined, {
47
+ month: 'short',
48
+ day: 'numeric',
49
+ hour: '2-digit',
50
+ minute: '2-digit',
51
+ });
52
+ }
53
+ catch {
54
+ return '';
55
+ }
56
+ }
57
+ // ---------------------------------------------------------------------------
58
+ // Discord-specific builder
59
+ // ---------------------------------------------------------------------------
60
+ /**
61
+ * Build the artifact picker embed + select menu components.
62
+ */
63
+ function buildArtifactPickerUI(artifacts, conversationId, renderMode = 'thread') {
64
+ const embed = new discord_js_1.EmbedBuilder()
65
+ .setTitle('๐Ÿ“‚ Artifacts')
66
+ .setColor(0x5865F2)
67
+ .setTimestamp();
68
+ if (artifacts.length === 0) {
69
+ embed.setDescription('No artifacts found for the active session.');
70
+ return { embeds: [embed], components: [] };
71
+ }
72
+ const displayId = conversationId
73
+ ? conversationId.slice(0, 8) + 'โ€ฆ'
74
+ : 'current session';
75
+ embed.setDescription(`**${artifacts.length}** artifact(s) found (conversation \`${displayId}\`)\n` +
76
+ 'Select one to render its content below.');
77
+ const fields = artifacts.map((a) => ({
78
+ name: a.filename,
79
+ value: [
80
+ (0, artifactService_1.artifactTypeLabel)(a.artifactType),
81
+ a.summary ? `*${truncate(a.summary, 120)}*` : '',
82
+ a.updatedAt ? `๐Ÿ• v${a.version ?? '?'} ยท ${formatUpdatedAt(a.updatedAt)}` : '',
83
+ ].filter(Boolean).join('\n'),
84
+ inline: false,
85
+ }));
86
+ // Add summary fields (max 10 to avoid embed limit)
87
+ embed.addFields(...fields.slice(0, 10));
88
+ const pageItems = artifacts.slice(0, MAX_SELECT_OPTIONS);
89
+ const options = pageItems.map((a) => ({
90
+ label: truncate(a.filename, 100),
91
+ value: artifactService_1.ArtifactService.encodeSelectValue(a.conversationId, a.filename),
92
+ description: formatOptionDescription(a),
93
+ }));
94
+ const selectMenu = new discord_js_1.StringSelectMenuBuilder()
95
+ .setCustomId(exports.ARTIFACT_SELECT_ID)
96
+ .setPlaceholder('Select an artifact to viewโ€ฆ')
97
+ .addOptions(options);
98
+ const components = [
99
+ new discord_js_1.ActionRowBuilder().addComponents(selectMenu),
100
+ ];
101
+ // Add toggle button row
102
+ const toggleButton = new discord_js_1.ButtonBuilder();
103
+ if (renderMode === 'thread') {
104
+ toggleButton
105
+ .setCustomId(`${exports.ARTIFACT_INLINE_BTN}:${conversationId || ''}`)
106
+ .setLabel('๐Ÿ’ฌ Switch to Inline')
107
+ .setStyle(discord_js_1.ButtonStyle.Secondary)
108
+ .setEmoji('๐Ÿ’ฌ');
109
+ embed.setFooter({ text: 'Output: Thread (one thread per file)' });
110
+ }
111
+ else {
112
+ toggleButton
113
+ .setCustomId(`${exports.ARTIFACT_THREAD_BTN}:${conversationId || ''}`)
114
+ .setLabel('๐Ÿ“Œ Switch to Thread')
115
+ .setStyle(discord_js_1.ButtonStyle.Primary)
116
+ .setEmoji('๐Ÿ“Œ');
117
+ embed.setFooter({ text: 'Output: Inline' });
118
+ }
119
+ components.push(new discord_js_1.ActionRowBuilder().addComponents(toggleButton));
120
+ return { embeds: [embed], components };
121
+ }
122
+ // ---------------------------------------------------------------------------
123
+ // Interaction sender
124
+ // ---------------------------------------------------------------------------
125
+ /**
126
+ * Send the artifacts picker UI as an editReply to a slash command interaction.
127
+ */
128
+ async function sendArtifactsUI(interaction, artifacts, conversationId, renderMode = 'thread') {
129
+ const { embeds, components } = buildArtifactPickerUI(artifacts, conversationId, renderMode);
130
+ await interaction.editReply({ embeds, components });
131
+ }
132
+ /**
133
+ * Higher-level helper to send the artifact picker UI,
134
+ * automatically discovering artifacts for the current channel.
135
+ */
136
+ async function sendArtifactPickerUI(interaction, deps, edit = false, resolvedConversationId) {
137
+ const artifactService = deps.artifactService || new artifactService_1.ArtifactService();
138
+ let conversationId = resolvedConversationId;
139
+ if (!conversationId) {
140
+ const session = deps.chatSessionRepo?.findByChannelId(interaction.channelId);
141
+ if (session) {
142
+ conversationId = session.conversationId ?? null;
143
+ // Fallback to title matching if ID is not in DB or doesn't match a real folder
144
+ if (!conversationId || !artifactService.listArtifacts(conversationId).length) {
145
+ const sessionTitle = session.displayName?.trim() ?? '';
146
+ const matchedId = sessionTitle ? artifactService.findConversationByTitle(sessionTitle) : null;
147
+ if (matchedId)
148
+ conversationId = matchedId;
149
+ }
150
+ // Note: We do NOT fallback to getLatestConversationWithArtifacts() for managed sessions
151
+ // to avoid showing artifacts from other projects/chats.
152
+ }
153
+ else {
154
+ // Final fallback: latest overall (only for non-session channels)
155
+ conversationId = artifactService.getLatestConversationWithArtifacts();
156
+ }
157
+ }
158
+ if (!conversationId) {
159
+ const payload = { content: '๐Ÿ“‚ No artifacts found for this session.', components: [], ephemeral: true };
160
+ if (edit || interaction.deferred || interaction.replied)
161
+ await interaction.editReply(payload);
162
+ else
163
+ await interaction.reply(payload);
164
+ return;
165
+ }
166
+ // 2. List artifacts
167
+ const artifacts = artifactService.listArtifacts(conversationId);
168
+ if (artifacts.length === 0) {
169
+ const payload = { content: '๐Ÿ“‚ No artifacts found in this conversation.', components: [], ephemeral: true };
170
+ if (edit || interaction.deferred || interaction.replied)
171
+ await interaction.editReply(payload);
172
+ else
173
+ await interaction.reply(payload);
174
+ return;
175
+ }
176
+ // 3. Get user render mode pref
177
+ const renderMode = deps.userPrefRepo?.getArtifactRenderMode(interaction.user.id) ?? 'thread';
178
+ // 4. Build and send
179
+ const { embeds, components } = buildArtifactPickerUI(artifacts, conversationId, renderMode);
180
+ const payload = { embeds, components, ephemeral: true };
181
+ if (edit || interaction.deferred || interaction.replied)
182
+ await interaction.editReply(payload);
183
+ else
184
+ await interaction.reply(payload);
185
+ }
@@ -36,7 +36,7 @@ function acquireLock() {
36
36
  if (typeof process.getuid === 'function' && dirStat.uid !== process.getuid()) {
37
37
  throw new Error(`Lock directory is not owned by current user: ${LOCK_DIR}`);
38
38
  }
39
- if ((dirStat.mode & 0o077) !== 0) {
39
+ if (process.platform !== 'win32' && (dirStat.mode & 0o077) !== 0) {
40
40
  throw new Error(`Lock directory has overly permissive permissions: ${LOCK_DIR}`);
41
41
  }
42
42
  // Check existing lock file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazy-gravity",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Control Antigravity from anywhere โ€” a local, secure bot (Discord + Telegram) that lets you remotely operate Antigravity on your home PC from your smartphone.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -70,9 +70,10 @@
70
70
  "minimatch": "^10.2.1",
71
71
  "semantic-release": "^25.0.3",
72
72
  "ts-jest": "^29.4.6",
73
- "ts-morph": "^27.0.2",
73
+ "ts-morph": "^28.0.0",
74
74
  "ts-node": "^10.9.2",
75
+ "ts-node-dev": "^2.0.0",
75
76
  "typescript": "^5.9.3",
76
- "undici": "^7.22.0"
77
+ "undici": "^8.0.3"
77
78
  }
78
79
  }