swarm-tickets 1.0.1 → 2.0.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.
@@ -0,0 +1,614 @@
1
+ /**
2
+ * Supabase Storage Adapter
3
+ * Uses Supabase client for cloud PostgreSQL storage
4
+ */
5
+
6
+ const BaseAdapter = require('./base-adapter');
7
+
8
+ class SupabaseAdapter extends BaseAdapter {
9
+ constructor(config) {
10
+ super(config);
11
+ this.supabaseUrl = config.supabaseUrl;
12
+ this.supabaseKey = config.supabaseKey;
13
+ this.supabaseServiceKey = config.supabaseServiceKey;
14
+ this.client = null;
15
+ this.adminClient = null;
16
+ }
17
+
18
+ async initialize() {
19
+ // Dynamic import to make supabase optional
20
+ let createClient;
21
+ try {
22
+ const supabase = require('@supabase/supabase-js');
23
+ createClient = supabase.createClient;
24
+ } catch (error) {
25
+ throw new Error(
26
+ 'Supabase adapter requires @supabase/supabase-js. Install it with: npm install @supabase/supabase-js'
27
+ );
28
+ }
29
+
30
+ this.client = createClient(this.supabaseUrl, this.supabaseKey);
31
+
32
+ // Create admin client if service key is provided (for table creation)
33
+ if (this.supabaseServiceKey) {
34
+ this.adminClient = createClient(this.supabaseUrl, this.supabaseServiceKey);
35
+ }
36
+
37
+ // Try to create tables if we have admin access
38
+ await this._ensureTables();
39
+ }
40
+
41
+ async _ensureTables() {
42
+ // Check if tables exist by trying to query them
43
+ const { error } = await this.client.from('tickets').select('id').limit(1);
44
+
45
+ if (error && error.code === '42P01') {
46
+ // Table doesn't exist
47
+ if (this.adminClient) {
48
+ console.log('Creating Supabase tables...');
49
+ await this._createTables();
50
+ } else {
51
+ throw new Error(
52
+ 'Supabase tables do not exist. Either:\n' +
53
+ '1. Set SUPABASE_SERVICE_ROLE_KEY for auto-creation, or\n' +
54
+ '2. Run the migration script manually. See README for SQL.'
55
+ );
56
+ }
57
+ }
58
+ }
59
+
60
+ async _createTables() {
61
+ // Execute table creation via Supabase SQL
62
+ const { error } = await this.adminClient.rpc('exec_sql', {
63
+ sql: this._getCreateTablesSql()
64
+ });
65
+
66
+ if (error) {
67
+ // RPC might not exist, try alternative approach
68
+ console.warn('Could not auto-create tables via RPC. Please create them manually.');
69
+ console.log('SQL Schema:\n', this._getCreateTablesSql());
70
+ }
71
+ }
72
+
73
+ _getCreateTablesSql() {
74
+ return `
75
+ -- Main tickets table
76
+ CREATE TABLE IF NOT EXISTS tickets (
77
+ id TEXT PRIMARY KEY,
78
+ route TEXT NOT NULL,
79
+ f12_errors TEXT DEFAULT '',
80
+ server_errors TEXT DEFAULT '',
81
+ description TEXT DEFAULT '',
82
+ status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'in-progress', 'fixed', 'closed')),
83
+ priority TEXT CHECK (priority IS NULL OR priority IN ('critical', 'high', 'medium', 'low')),
84
+ namespace TEXT,
85
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
86
+ updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
87
+ );
88
+
89
+ -- Related tickets junction table
90
+ CREATE TABLE IF NOT EXISTS ticket_relations (
91
+ id SERIAL PRIMARY KEY,
92
+ ticket_id TEXT NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
93
+ related_ticket_id TEXT NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
94
+ UNIQUE(ticket_id, related_ticket_id)
95
+ );
96
+
97
+ -- Swarm actions log
98
+ CREATE TABLE IF NOT EXISTS swarm_actions (
99
+ id SERIAL PRIMARY KEY,
100
+ ticket_id TEXT NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
101
+ timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
102
+ action TEXT NOT NULL,
103
+ result TEXT
104
+ );
105
+
106
+ -- Comments table
107
+ CREATE TABLE IF NOT EXISTS comments (
108
+ id TEXT PRIMARY KEY,
109
+ ticket_id TEXT NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
110
+ timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
111
+ type TEXT NOT NULL DEFAULT 'human' CHECK (type IN ('human', 'ai')),
112
+ author TEXT DEFAULT 'anonymous',
113
+ content TEXT DEFAULT '',
114
+ metadata JSONB DEFAULT '{}',
115
+ edited_at TIMESTAMP WITH TIME ZONE
116
+ );
117
+
118
+ -- API Keys for bug report widget
119
+ CREATE TABLE IF NOT EXISTS api_keys (
120
+ id SERIAL PRIMARY KEY,
121
+ key TEXT UNIQUE NOT NULL,
122
+ name TEXT,
123
+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
124
+ last_used TIMESTAMP WITH TIME ZONE,
125
+ rate_limit INTEGER DEFAULT 100,
126
+ enabled BOOLEAN DEFAULT true
127
+ );
128
+
129
+ -- Rate limiting table
130
+ CREATE TABLE IF NOT EXISTS rate_limits (
131
+ id SERIAL PRIMARY KEY,
132
+ identifier TEXT NOT NULL,
133
+ window_start TIMESTAMP WITH TIME ZONE NOT NULL,
134
+ request_count INTEGER DEFAULT 1,
135
+ UNIQUE(identifier, window_start)
136
+ );
137
+
138
+ -- Create indexes
139
+ CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status);
140
+ CREATE INDEX IF NOT EXISTS idx_tickets_priority ON tickets(priority);
141
+ CREATE INDEX IF NOT EXISTS idx_tickets_route ON tickets(route);
142
+ CREATE INDEX IF NOT EXISTS idx_tickets_created_at ON tickets(created_at);
143
+ CREATE INDEX IF NOT EXISTS idx_swarm_actions_ticket_id ON swarm_actions(ticket_id);
144
+ CREATE INDEX IF NOT EXISTS idx_comments_ticket_id ON comments(ticket_id);
145
+
146
+ -- Enable Row Level Security (optional)
147
+ ALTER TABLE tickets ENABLE ROW LEVEL SECURITY;
148
+ ALTER TABLE swarm_actions ENABLE ROW LEVEL SECURITY;
149
+ ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
150
+
151
+ -- Create policies for anonymous access (adjust as needed)
152
+ CREATE POLICY IF NOT EXISTS "Allow all for now" ON tickets FOR ALL USING (true);
153
+ CREATE POLICY IF NOT EXISTS "Allow all for now" ON swarm_actions FOR ALL USING (true);
154
+ CREATE POLICY IF NOT EXISTS "Allow all for now" ON comments FOR ALL USING (true);
155
+ `;
156
+ }
157
+
158
+ async close() {
159
+ // Supabase client doesn't need explicit closing
160
+ this.client = null;
161
+ this.adminClient = null;
162
+ }
163
+
164
+ // Helper to convert DB row to ticket format
165
+ _rowToTicket(row, swarmActions = [], comments = [], relatedTickets = []) {
166
+ return {
167
+ id: row.id,
168
+ route: row.route,
169
+ f12Errors: row.f12_errors,
170
+ serverErrors: row.server_errors,
171
+ description: row.description,
172
+ status: row.status,
173
+ priority: row.priority,
174
+ namespace: row.namespace,
175
+ createdAt: row.created_at,
176
+ updatedAt: row.updated_at,
177
+ swarmActions: swarmActions.map(a => ({
178
+ timestamp: a.timestamp,
179
+ action: a.action,
180
+ result: a.result
181
+ })),
182
+ comments: comments.map(c => ({
183
+ id: c.id,
184
+ timestamp: c.timestamp,
185
+ type: c.type,
186
+ author: c.author,
187
+ content: c.content,
188
+ metadata: c.metadata || {},
189
+ editedAt: c.edited_at
190
+ })),
191
+ relatedTickets: relatedTickets.map(r => r.related_ticket_id)
192
+ };
193
+ }
194
+
195
+ async _getFullTicket(id) {
196
+ const [ticketResult, actionsResult, commentsResult, relationsResult] = await Promise.all([
197
+ this.client.from('tickets').select('*').eq('id', id).single(),
198
+ this.client.from('swarm_actions').select('*').eq('ticket_id', id).order('timestamp'),
199
+ this.client.from('comments').select('*').eq('ticket_id', id).order('timestamp'),
200
+ this.client.from('ticket_relations').select('related_ticket_id').eq('ticket_id', id)
201
+ ]);
202
+
203
+ if (ticketResult.error || !ticketResult.data) return null;
204
+
205
+ return this._rowToTicket(
206
+ ticketResult.data,
207
+ actionsResult.data || [],
208
+ commentsResult.data || [],
209
+ relationsResult.data || []
210
+ );
211
+ }
212
+
213
+ // ==================== TICKET OPERATIONS ====================
214
+
215
+ async getAllTickets(filters = {}) {
216
+ let query = this.client.from('tickets').select('*');
217
+
218
+ if (filters.status) {
219
+ query = query.eq('status', filters.status);
220
+ }
221
+ if (filters.priority) {
222
+ query = query.eq('priority', filters.priority);
223
+ }
224
+ if (filters.route) {
225
+ query = query.ilike('route', `%${filters.route}%`);
226
+ }
227
+
228
+ query = query.order('created_at', { ascending: false });
229
+
230
+ const { data, error } = await query;
231
+ if (error) throw error;
232
+
233
+ // Get all related data for each ticket
234
+ const tickets = await Promise.all(
235
+ (data || []).map(row => this._getFullTicket(row.id))
236
+ );
237
+
238
+ return tickets.filter(t => t !== null);
239
+ }
240
+
241
+ async getTicket(id) {
242
+ return this._getFullTicket(id);
243
+ }
244
+
245
+ async createTicket(ticketData) {
246
+ const id = this.generateTicketId();
247
+ const now = new Date().toISOString();
248
+
249
+ const ticket = {
250
+ id,
251
+ route: ticketData.route || '',
252
+ f12_errors: ticketData.f12Errors || '',
253
+ server_errors: ticketData.serverErrors || '',
254
+ description: ticketData.description || '',
255
+ status: ticketData.status || 'open',
256
+ priority: ticketData.priority || null,
257
+ namespace: ticketData.namespace || null,
258
+ created_at: now,
259
+ updated_at: now
260
+ };
261
+
262
+ const { error } = await this.client.from('tickets').insert(ticket);
263
+ if (error) throw error;
264
+
265
+ // Insert related tickets
266
+ if (ticketData.relatedTickets && ticketData.relatedTickets.length > 0) {
267
+ const relations = ticketData.relatedTickets.map(relatedId => ({
268
+ ticket_id: id,
269
+ related_ticket_id: relatedId
270
+ }));
271
+ await this.client.from('ticket_relations').insert(relations);
272
+ }
273
+
274
+ // Insert swarm actions
275
+ if (ticketData.swarmActions && ticketData.swarmActions.length > 0) {
276
+ const actions = ticketData.swarmActions.map(action => ({
277
+ ticket_id: id,
278
+ timestamp: action.timestamp || now,
279
+ action: action.action,
280
+ result: action.result || null
281
+ }));
282
+ await this.client.from('swarm_actions').insert(actions);
283
+ }
284
+
285
+ // Insert comments
286
+ if (ticketData.comments && ticketData.comments.length > 0) {
287
+ const comments = ticketData.comments.map(comment => ({
288
+ id: comment.id || this.generateCommentId(),
289
+ ticket_id: id,
290
+ timestamp: comment.timestamp || now,
291
+ type: comment.type || 'human',
292
+ author: comment.author || 'anonymous',
293
+ content: comment.content || '',
294
+ metadata: comment.metadata || {}
295
+ }));
296
+ await this.client.from('comments').insert(comments);
297
+ }
298
+
299
+ return this.getTicket(id);
300
+ }
301
+
302
+ async updateTicket(id, updates) {
303
+ const existing = await this.getTicket(id);
304
+ if (!existing) return null;
305
+
306
+ const now = new Date().toISOString();
307
+
308
+ const updateData = { updated_at: now };
309
+ if (updates.route !== undefined) updateData.route = updates.route;
310
+ if (updates.f12Errors !== undefined) updateData.f12_errors = updates.f12Errors;
311
+ if (updates.serverErrors !== undefined) updateData.server_errors = updates.serverErrors;
312
+ if (updates.description !== undefined) updateData.description = updates.description;
313
+ if (updates.status !== undefined) updateData.status = updates.status;
314
+ if (updates.priority !== undefined) updateData.priority = updates.priority;
315
+ if (updates.namespace !== undefined) updateData.namespace = updates.namespace;
316
+
317
+ const { error } = await this.client.from('tickets').update(updateData).eq('id', id);
318
+ if (error) throw error;
319
+
320
+ // Update related tickets if provided
321
+ if (updates.relatedTickets !== undefined) {
322
+ await this.client.from('ticket_relations').delete().eq('ticket_id', id);
323
+ if (updates.relatedTickets.length > 0) {
324
+ const relations = updates.relatedTickets.map(relatedId => ({
325
+ ticket_id: id,
326
+ related_ticket_id: relatedId
327
+ }));
328
+ await this.client.from('ticket_relations').insert(relations);
329
+ }
330
+ }
331
+
332
+ return this.getTicket(id);
333
+ }
334
+
335
+ async deleteTicket(id) {
336
+ const { error, count } = await this.client.from('tickets').delete().eq('id', id);
337
+ if (error) throw error;
338
+ return count > 0;
339
+ }
340
+
341
+ // ==================== SWARM ACTION OPERATIONS ====================
342
+
343
+ async addSwarmAction(ticketId, action) {
344
+ const ticket = await this.getTicket(ticketId);
345
+ if (!ticket) return null;
346
+
347
+ const now = new Date().toISOString();
348
+
349
+ const { error } = await this.client.from('swarm_actions').insert({
350
+ ticket_id: ticketId,
351
+ timestamp: now,
352
+ action: action.action,
353
+ result: action.result || null
354
+ });
355
+ if (error) throw error;
356
+
357
+ await this.client.from('tickets').update({ updated_at: now }).eq('id', ticketId);
358
+
359
+ return this.getTicket(ticketId);
360
+ }
361
+
362
+ // ==================== COMMENT OPERATIONS ====================
363
+
364
+ async addComment(ticketId, commentData) {
365
+ const ticket = await this.getTicket(ticketId);
366
+ if (!ticket) return null;
367
+
368
+ const now = new Date().toISOString();
369
+ const commentId = this.generateCommentId();
370
+
371
+ const comment = {
372
+ id: commentId,
373
+ ticket_id: ticketId,
374
+ timestamp: now,
375
+ type: commentData.type || 'human',
376
+ author: commentData.author || 'anonymous',
377
+ content: commentData.content || '',
378
+ metadata: commentData.metadata || {}
379
+ };
380
+
381
+ const { error } = await this.client.from('comments').insert(comment);
382
+ if (error) throw error;
383
+
384
+ await this.client.from('tickets').update({ updated_at: now }).eq('id', ticketId);
385
+
386
+ return {
387
+ id: commentId,
388
+ timestamp: now,
389
+ type: comment.type,
390
+ author: comment.author,
391
+ content: comment.content,
392
+ metadata: comment.metadata
393
+ };
394
+ }
395
+
396
+ async getComments(ticketId) {
397
+ const { data, error } = await this.client
398
+ .from('comments')
399
+ .select('*')
400
+ .eq('ticket_id', ticketId)
401
+ .order('timestamp');
402
+
403
+ if (error) throw error;
404
+
405
+ return (data || []).map(c => ({
406
+ id: c.id,
407
+ timestamp: c.timestamp,
408
+ type: c.type,
409
+ author: c.author,
410
+ content: c.content,
411
+ metadata: c.metadata || {},
412
+ editedAt: c.edited_at
413
+ }));
414
+ }
415
+
416
+ async updateComment(ticketId, commentId, updates) {
417
+ const { data: existing } = await this.client
418
+ .from('comments')
419
+ .select('*')
420
+ .eq('id', commentId)
421
+ .eq('ticket_id', ticketId)
422
+ .single();
423
+
424
+ if (!existing) return null;
425
+
426
+ const now = new Date().toISOString();
427
+
428
+ const updateData = { edited_at: now };
429
+ if (updates.content !== undefined) updateData.content = updates.content;
430
+ if (updates.metadata !== undefined) updateData.metadata = { ...existing.metadata, ...updates.metadata };
431
+
432
+ const { error } = await this.client
433
+ .from('comments')
434
+ .update(updateData)
435
+ .eq('id', commentId)
436
+ .eq('ticket_id', ticketId);
437
+
438
+ if (error) throw error;
439
+
440
+ await this.client.from('tickets').update({ updated_at: now }).eq('id', ticketId);
441
+
442
+ const { data: updated } = await this.client
443
+ .from('comments')
444
+ .select('*')
445
+ .eq('id', commentId)
446
+ .single();
447
+
448
+ return {
449
+ id: updated.id,
450
+ timestamp: updated.timestamp,
451
+ type: updated.type,
452
+ author: updated.author,
453
+ content: updated.content,
454
+ metadata: updated.metadata || {},
455
+ editedAt: updated.edited_at
456
+ };
457
+ }
458
+
459
+ async deleteComment(ticketId, commentId) {
460
+ const { count, error } = await this.client
461
+ .from('comments')
462
+ .delete()
463
+ .eq('id', commentId)
464
+ .eq('ticket_id', ticketId);
465
+
466
+ if (error) throw error;
467
+
468
+ if (count > 0) {
469
+ await this.client.from('tickets').update({ updated_at: new Date().toISOString() }).eq('id', ticketId);
470
+ return true;
471
+ }
472
+ return false;
473
+ }
474
+
475
+ // ==================== STATS OPERATIONS ====================
476
+
477
+ async getStats() {
478
+ const { data: tickets, error } = await this.client.from('tickets').select('status, priority');
479
+ if (error) throw error;
480
+
481
+ const byStatus = { open: 0, inProgress: 0, fixed: 0, closed: 0 };
482
+ const byPriority = { critical: 0, high: 0, medium: 0, low: 0 };
483
+
484
+ (tickets || []).forEach(t => {
485
+ if (t.status === 'in-progress') byStatus.inProgress++;
486
+ else if (byStatus[t.status] !== undefined) byStatus[t.status]++;
487
+
488
+ if (t.priority && byPriority[t.priority] !== undefined) {
489
+ byPriority[t.priority]++;
490
+ }
491
+ });
492
+
493
+ return {
494
+ total: (tickets || []).length,
495
+ byStatus,
496
+ byPriority
497
+ };
498
+ }
499
+
500
+ // ==================== BUG REPORT OPERATIONS ====================
501
+
502
+ async createBugReport(reportData, apiKey = null) {
503
+ // Validate API key if provided
504
+ if (apiKey) {
505
+ const { data: keyRecord } = await this.client
506
+ .from('api_keys')
507
+ .select('*')
508
+ .eq('key', apiKey)
509
+ .eq('enabled', true)
510
+ .single();
511
+
512
+ if (!keyRecord) {
513
+ throw new Error('Invalid API key');
514
+ }
515
+
516
+ // Update last used
517
+ await this.client.from('api_keys').update({ last_used: new Date().toISOString() }).eq('key', apiKey);
518
+ }
519
+
520
+ // Rate limiting
521
+ const identifier = apiKey || reportData.ip || 'anonymous';
522
+ const windowStart = new Date();
523
+ windowStart.setMinutes(0, 0, 0);
524
+ const windowKey = windowStart.toISOString();
525
+
526
+ // Check current count
527
+ const { data: rateLimit } = await this.client
528
+ .from('rate_limits')
529
+ .select('request_count')
530
+ .eq('identifier', identifier)
531
+ .eq('window_start', windowKey)
532
+ .single();
533
+
534
+ const limit = apiKey ? 1000 : 10;
535
+ if (rateLimit && rateLimit.request_count >= limit) {
536
+ throw new Error('Rate limit exceeded. Please try again later.');
537
+ }
538
+
539
+ // Increment rate limit
540
+ if (rateLimit) {
541
+ await this.client
542
+ .from('rate_limits')
543
+ .update({ request_count: rateLimit.request_count + 1 })
544
+ .eq('identifier', identifier)
545
+ .eq('window_start', windowKey);
546
+ } else {
547
+ await this.client.from('rate_limits').insert({
548
+ identifier,
549
+ window_start: windowKey,
550
+ request_count: 1
551
+ });
552
+ }
553
+
554
+ // Create the ticket
555
+ const ticket = await this.createTicket({
556
+ route: reportData.location || reportData.route || 'unknown',
557
+ f12Errors: reportData.clientError || reportData.f12Errors || '',
558
+ serverErrors: '',
559
+ description: reportData.description || '',
560
+ status: 'open',
561
+ priority: null,
562
+ swarmActions: [{
563
+ timestamp: new Date().toISOString(),
564
+ action: 'bug-report-submitted',
565
+ result: `Bug report submitted via widget. User agent: ${reportData.userAgent || 'unknown'}`
566
+ }]
567
+ });
568
+
569
+ return {
570
+ id: ticket.id,
571
+ status: 'submitted',
572
+ message: 'Bug report received. Thank you!'
573
+ };
574
+ }
575
+
576
+ // ==================== API KEY MANAGEMENT ====================
577
+
578
+ async createApiKey(name = null) {
579
+ const crypto = require('crypto');
580
+ const key = 'stk_' + crypto.randomBytes(24).toString('hex');
581
+ const now = new Date().toISOString();
582
+
583
+ const { error } = await this.client.from('api_keys').insert({
584
+ key,
585
+ name,
586
+ created_at: now
587
+ });
588
+
589
+ if (error) throw error;
590
+
591
+ return { key, name, createdAt: now };
592
+ }
593
+
594
+ async listApiKeys() {
595
+ const { data, error } = await this.client
596
+ .from('api_keys')
597
+ .select('id, name, created_at, last_used, enabled');
598
+
599
+ if (error) throw error;
600
+ return data || [];
601
+ }
602
+
603
+ async revokeApiKey(key) {
604
+ const { count, error } = await this.client
605
+ .from('api_keys')
606
+ .update({ enabled: false })
607
+ .eq('key', key);
608
+
609
+ if (error) throw error;
610
+ return count > 0;
611
+ }
612
+ }
613
+
614
+ module.exports = SupabaseAdapter;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "swarm-tickets",
3
- "version": "1.0.1",
4
- "description": "Lightweight ticket tracking system for AI-powered bug fixing with Claude-flow/Claude Code",
3
+ "version": "2.0.0",
4
+ "description": "Lightweight ticket tracking system for AI-powered bug fixing with Claude-flow/Claude Code. Supports JSON, SQLite, and Supabase storage.",
5
5
  "main": "ticket-server.js",
6
6
  "bin": {
7
7
  "swarm-tickets": "./ticket-server.js"
@@ -15,9 +15,11 @@
15
15
  "ticket-server.js",
16
16
  "ticket-cli.js",
17
17
  "ticket-tracker.html",
18
+ "bug-report-widget.js",
18
19
  "backup-tickets.sh",
19
20
  "tickets.example.json",
20
21
  "setup.js",
22
+ "lib/",
21
23
  "SKILL.md",
22
24
  "README.md",
23
25
  "LICENSE"
@@ -32,21 +34,28 @@
32
34
  "claude-code",
33
35
  "ai",
34
36
  "automation",
35
- "development-tools"
37
+ "development-tools",
38
+ "sqlite",
39
+ "supabase",
40
+ "bug-report"
36
41
  ],
37
42
  "dependencies": {
38
43
  "express": "^4.18.2",
39
44
  "cors": "^2.8.5"
40
45
  },
41
- "repository": {
42
- "type": "git",
43
- "url": "git+https://github.com/AIWhispererGal/swarm-tickets.git"
44
- },
45
- "bugs": {
46
- "url": "https://github.com/AIWhispererGal/swarm-tickets/issues"
47
- },
48
- "homepage": "https://github.com/AIWhispererGal/swarm-tickets#readme",
49
- "author": "AIWhispererGal and Claude Sonnet 4.5",
46
+ "optionalDependencies": {
47
+ "better-sqlite3": "^9.4.0",
48
+ "@supabase/supabase-js": "^2.39.0"
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/AIWhispererGal/swarm-tickets.git"
53
+ },
54
+ "bugs": {
55
+ "url": "https://github.com/AIWhispererGal/swarm-tickets/issues"
56
+ },
57
+ "homepage": "https://github.com/AIWhispererGal/swarm-tickets#readme",
58
+ "author": "AIWhispererGal and Claude",
50
59
  "license": "MIT",
51
60
  "engines": {
52
61
  "node": ">=14.0.0"
package/setup.js CHANGED
File without changes
package/ticket-cli.js CHANGED
File without changes