@simonfestl/husky-cli 1.27.1 → 1.29.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.
Files changed (34) hide show
  1. package/dist/commands/biz/invoices.d.ts +11 -0
  2. package/dist/commands/biz/invoices.js +661 -0
  3. package/dist/commands/biz/shopify.d.ts +3 -0
  4. package/dist/commands/biz/shopify.js +592 -0
  5. package/dist/commands/biz/supplier-feed.d.ts +3 -0
  6. package/dist/commands/biz/supplier-feed.js +168 -0
  7. package/dist/commands/biz.js +5 -1
  8. package/dist/commands/config.d.ts +3 -2
  9. package/dist/commands/config.js +4 -3
  10. package/dist/commands/sop.d.ts +3 -0
  11. package/dist/commands/sop.js +458 -0
  12. package/dist/commands/task.js +7 -0
  13. package/dist/lib/biz/gcs-upload.d.ts +86 -0
  14. package/dist/lib/biz/gcs-upload.js +189 -0
  15. package/dist/lib/biz/index.d.ts +5 -0
  16. package/dist/lib/biz/index.js +3 -0
  17. package/dist/lib/biz/invoice-extractor-registry.d.ts +22 -0
  18. package/dist/lib/biz/invoice-extractor-registry.js +416 -0
  19. package/dist/lib/biz/invoice-extractor-types.d.ts +127 -0
  20. package/dist/lib/biz/invoice-extractor-types.js +6 -0
  21. package/dist/lib/biz/pattern-detection.d.ts +48 -0
  22. package/dist/lib/biz/pattern-detection.js +205 -0
  23. package/dist/lib/biz/resolved-tickets.d.ts +86 -0
  24. package/dist/lib/biz/resolved-tickets.js +250 -0
  25. package/dist/lib/biz/shopify.d.ts +196 -0
  26. package/dist/lib/biz/shopify.js +429 -0
  27. package/dist/lib/biz/supplier-feed-types.d.ts +96 -0
  28. package/dist/lib/biz/supplier-feed-types.js +46 -0
  29. package/dist/lib/biz/supplier-feed.d.ts +32 -0
  30. package/dist/lib/biz/supplier-feed.js +244 -0
  31. package/dist/lib/permissions.d.ts +2 -1
  32. package/dist/types/roles.d.ts +3 -0
  33. package/dist/types/roles.js +14 -0
  34. package/package.json +1 -1
@@ -0,0 +1,205 @@
1
+ import { QdrantClient } from './qdrant.js';
2
+ import { EmbeddingService } from './embeddings.js';
3
+ import { ResolvedTicketsService } from './resolved-tickets.js';
4
+ import { SOPService } from './sop.js';
5
+ // ============================================================================
6
+ // Pattern Detection Service
7
+ // ============================================================================
8
+ export class PatternDetectionService {
9
+ qdrant;
10
+ embeddings;
11
+ resolvedTickets;
12
+ sopService;
13
+ constructor() {
14
+ this.qdrant = QdrantClient.fromConfig();
15
+ this.embeddings = EmbeddingService.fromConfig();
16
+ this.resolvedTickets = new ResolvedTicketsService();
17
+ this.sopService = new SOPService();
18
+ }
19
+ async detectPatterns(options = {}) {
20
+ const minClusterSize = options.minClusterSize || 3;
21
+ const similarityThreshold = options.similarityThreshold || 0.75;
22
+ const limit = options.limit || 500;
23
+ const tickets = await this.resolvedTickets.list({
24
+ category: options.category,
25
+ outcome: 'positive',
26
+ limit,
27
+ });
28
+ if (tickets.length < minClusterSize) {
29
+ return {
30
+ total_tickets: tickets.length,
31
+ clusters: [],
32
+ unmatched_tickets: tickets,
33
+ suggestions: [],
34
+ };
35
+ }
36
+ const clusters = await this.clusterTickets(tickets, similarityThreshold, minClusterSize);
37
+ const matchedTicketIds = new Set();
38
+ for (const cluster of clusters) {
39
+ matchedTicketIds.add(cluster.representative.id);
40
+ for (const member of cluster.members) {
41
+ matchedTicketIds.add(member.id);
42
+ }
43
+ }
44
+ const unmatched = tickets.filter(t => !matchedTicketIds.has(t.id));
45
+ const suggestions = [];
46
+ for (const cluster of clusters) {
47
+ const suggestion = this.generateSOPSuggestion(cluster);
48
+ cluster.suggested_sop = suggestion;
49
+ suggestions.push(suggestion);
50
+ }
51
+ return {
52
+ total_tickets: tickets.length,
53
+ clusters,
54
+ unmatched_tickets: unmatched,
55
+ suggestions,
56
+ };
57
+ }
58
+ async createSOPFromCluster(clusterId, analysis) {
59
+ const cluster = analysis.clusters.find(c => c.id === clusterId);
60
+ if (!cluster || !cluster.suggested_sop) {
61
+ return null;
62
+ }
63
+ const sop = await this.sopService.create({
64
+ title: cluster.suggested_sop.title,
65
+ description: cluster.suggested_sop.description,
66
+ trigger: cluster.suggested_sop.trigger,
67
+ category: cluster.suggested_sop.category,
68
+ steps: cluster.suggested_sop.steps,
69
+ source_tickets: cluster.suggested_sop.source_tickets,
70
+ tags: ['auto-generated', 'pattern-detected'],
71
+ confidence_score: cluster.suggested_sop.confidence_score,
72
+ });
73
+ return sop.id;
74
+ }
75
+ async findSimilarTickets(ticketId, limit = 5) {
76
+ const ticket = await this.resolvedTickets.get(ticketId);
77
+ if (!ticket)
78
+ return [];
79
+ const results = await this.resolvedTickets.findSimilarProblems(ticket.problem, {
80
+ limit: limit + 1,
81
+ minScore: 0.5,
82
+ });
83
+ return results
84
+ .filter(r => r.ticket.id !== ticketId)
85
+ .slice(0, limit)
86
+ .map(r => ({
87
+ ticket: r.ticket,
88
+ similarity: r.score,
89
+ }));
90
+ }
91
+ async clusterTickets(tickets, threshold, minSize) {
92
+ const clusters = [];
93
+ const assigned = new Set();
94
+ const sortedTickets = [...tickets].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
95
+ for (const ticket of sortedTickets) {
96
+ if (assigned.has(ticket.id))
97
+ continue;
98
+ const similar = await this.resolvedTickets.findSimilarProblems(ticket.problem, {
99
+ limit: 20,
100
+ minScore: threshold,
101
+ onlyPositive: true,
102
+ });
103
+ const clusterMembers = similar
104
+ .filter(r => !assigned.has(r.ticket.id) && r.ticket.id !== ticket.id)
105
+ .map(r => r.ticket);
106
+ if (clusterMembers.length + 1 >= minSize) {
107
+ assigned.add(ticket.id);
108
+ for (const member of clusterMembers) {
109
+ assigned.add(member.id);
110
+ }
111
+ const allMembers = [ticket, ...clusterMembers];
112
+ const avgSimilarity = similar.length > 0
113
+ ? similar.reduce((sum, r) => sum + r.score, 0) / similar.length
114
+ : 1;
115
+ clusters.push({
116
+ id: `cluster_${ticket.ticket_id}_${clusters.length}`,
117
+ representative: ticket,
118
+ members: clusterMembers,
119
+ similarity: avgSimilarity,
120
+ problem_category: ticket.problem_category,
121
+ common_steps: this.findCommonSteps(allMembers),
122
+ });
123
+ }
124
+ }
125
+ return clusters.sort((a, b) => (b.members.length + 1) - (a.members.length + 1));
126
+ }
127
+ findCommonSteps(tickets) {
128
+ if (tickets.length === 0)
129
+ return [];
130
+ const stepCounts = new Map();
131
+ for (const ticket of tickets) {
132
+ for (const step of ticket.resolution.steps) {
133
+ const key = step.action.toLowerCase().trim();
134
+ const existing = stepCounts.get(key);
135
+ if (existing) {
136
+ existing.count++;
137
+ }
138
+ else {
139
+ stepCounts.set(key, { step, count: 1 });
140
+ }
141
+ }
142
+ }
143
+ const threshold = Math.ceil(tickets.length * 0.5);
144
+ const commonSteps = Array.from(stepCounts.values())
145
+ .filter(({ count }) => count >= threshold)
146
+ .sort((a, b) => {
147
+ const orderDiff = (a.step.order || 0) - (b.step.order || 0);
148
+ if (orderDiff !== 0)
149
+ return orderDiff;
150
+ return b.count - a.count;
151
+ })
152
+ .map(({ step }, index) => ({
153
+ ...step,
154
+ order: index + 1,
155
+ }));
156
+ return commonSteps;
157
+ }
158
+ generateSOPSuggestion(cluster) {
159
+ const allTickets = [cluster.representative, ...cluster.members];
160
+ const ticketCount = allTickets.length;
161
+ const title = `Handle: ${cluster.representative.problem.slice(0, 50)}`;
162
+ const description = [
163
+ `Auto-generated from ${ticketCount} similar resolved tickets.`,
164
+ `Common problem pattern in category: ${cluster.problem_category}.`,
165
+ `Average resolution similarity: ${(cluster.similarity * 100).toFixed(0)}%.`,
166
+ ].join(' ');
167
+ const trigger = cluster.representative.problem;
168
+ const steps = cluster.common_steps.map((step, index) => ({
169
+ order: index + 1,
170
+ action: step.action,
171
+ description: step.description,
172
+ tool: step.tool_used,
173
+ required: true,
174
+ }));
175
+ if (steps.length === 0) {
176
+ const repSteps = cluster.representative.resolution.steps;
177
+ for (const step of repSteps) {
178
+ steps.push({
179
+ order: step.order,
180
+ action: step.action,
181
+ description: step.description,
182
+ tool: step.tool_used,
183
+ required: true,
184
+ });
185
+ }
186
+ }
187
+ const confidenceFactors = [
188
+ ticketCount >= 5 ? 0.3 : ticketCount >= 3 ? 0.2 : 0.1,
189
+ cluster.similarity * 0.3,
190
+ steps.length >= 3 ? 0.2 : 0.1,
191
+ cluster.common_steps.length > 0 ? 0.2 : 0,
192
+ ];
193
+ const confidence_score = Math.min(1, confidenceFactors.reduce((a, b) => a + b, 0));
194
+ return {
195
+ title,
196
+ description,
197
+ trigger,
198
+ category: cluster.problem_category,
199
+ steps,
200
+ confidence_score,
201
+ source_tickets: allTickets.map(t => t.ticket_id),
202
+ };
203
+ }
204
+ }
205
+ export default PatternDetectionService;
@@ -0,0 +1,86 @@
1
+ export declare const RESOLUTION_OUTCOMES: readonly ["positive", "negative", "neutral", "escalated"];
2
+ export type ResolutionOutcome = typeof RESOLUTION_OUTCOMES[number];
3
+ export interface ResolutionStep {
4
+ order: number;
5
+ action: string;
6
+ description: string;
7
+ tool_used?: string;
8
+ duration_seconds?: number;
9
+ success?: boolean;
10
+ }
11
+ export interface ResolvedTicket {
12
+ id: string;
13
+ ticket_id: number;
14
+ problem: string;
15
+ problem_category: string;
16
+ resolution: {
17
+ outcome: ResolutionOutcome;
18
+ summary: string;
19
+ steps: ResolutionStep[];
20
+ sop_reference?: string;
21
+ };
22
+ agent_id: string;
23
+ files?: string[];
24
+ tags: string[];
25
+ created_at: string;
26
+ customer_satisfaction?: number;
27
+ metadata?: Record<string, unknown>;
28
+ }
29
+ export interface CreateResolvedTicketInput {
30
+ ticket_id: number;
31
+ problem: string;
32
+ problem_category: string;
33
+ outcome: ResolutionOutcome;
34
+ summary: string;
35
+ steps: ResolutionStep[];
36
+ sop_reference?: string;
37
+ agent_id: string;
38
+ files?: string[];
39
+ tags?: string[];
40
+ customer_satisfaction?: number;
41
+ metadata?: Record<string, unknown>;
42
+ }
43
+ export interface ResolvedTicketSearchResult {
44
+ ticket: ResolvedTicket;
45
+ score: number;
46
+ }
47
+ export declare class ResolvedTicketsService {
48
+ private qdrant;
49
+ private embeddings;
50
+ constructor();
51
+ log(input: CreateResolvedTicketInput): Promise<ResolvedTicket>;
52
+ get(id: string): Promise<ResolvedTicket | null>;
53
+ getByTicketId(ticketId: number): Promise<ResolvedTicket | null>;
54
+ search(query: string, options?: {
55
+ limit?: number;
56
+ category?: string;
57
+ outcome?: ResolutionOutcome;
58
+ minScore?: number;
59
+ }): Promise<ResolvedTicketSearchResult[]>;
60
+ list(options?: {
61
+ category?: string;
62
+ outcome?: ResolutionOutcome;
63
+ agent_id?: string;
64
+ limit?: number;
65
+ }): Promise<ResolvedTicket[]>;
66
+ findSimilarProblems(problem: string, options?: {
67
+ limit?: number;
68
+ minScore?: number;
69
+ onlyPositive?: boolean;
70
+ }): Promise<ResolvedTicketSearchResult[]>;
71
+ getCategories(): Promise<{
72
+ category: string;
73
+ count: number;
74
+ }[]>;
75
+ stats(): Promise<{
76
+ total: number;
77
+ byOutcome: Record<ResolutionOutcome, number>;
78
+ byCategory: Record<string, number>;
79
+ avgSteps: number;
80
+ }>;
81
+ delete(id: string): Promise<void>;
82
+ private buildEmbeddingText;
83
+ private ticketToPayload;
84
+ private payloadToTicket;
85
+ }
86
+ export default ResolvedTicketsService;
@@ -0,0 +1,250 @@
1
+ import { QdrantClient } from './qdrant.js';
2
+ import { EmbeddingService } from './embeddings.js';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+ // ============================================================================
5
+ // Types
6
+ // ============================================================================
7
+ export const RESOLUTION_OUTCOMES = ['positive', 'negative', 'neutral', 'escalated'];
8
+ // ============================================================================
9
+ // Constants
10
+ // ============================================================================
11
+ const RESOLVED_COLLECTION = 'tickets_resolved';
12
+ // ============================================================================
13
+ // Resolved Tickets Service
14
+ // ============================================================================
15
+ export class ResolvedTicketsService {
16
+ qdrant;
17
+ embeddings;
18
+ constructor() {
19
+ this.qdrant = QdrantClient.fromConfig();
20
+ this.embeddings = EmbeddingService.fromConfig();
21
+ }
22
+ async log(input) {
23
+ const id = `rt_${input.ticket_id}_${uuidv4().slice(0, 6)}`;
24
+ const now = new Date().toISOString();
25
+ const ticket = {
26
+ id,
27
+ ticket_id: input.ticket_id,
28
+ problem: input.problem,
29
+ problem_category: input.problem_category,
30
+ resolution: {
31
+ outcome: input.outcome,
32
+ summary: input.summary,
33
+ steps: input.steps.map((s, i) => ({
34
+ ...s,
35
+ order: s.order || i + 1,
36
+ })),
37
+ sop_reference: input.sop_reference,
38
+ },
39
+ agent_id: input.agent_id,
40
+ files: input.files || [],
41
+ tags: input.tags || [],
42
+ created_at: now,
43
+ customer_satisfaction: input.customer_satisfaction,
44
+ metadata: input.metadata,
45
+ };
46
+ const embeddingText = this.buildEmbeddingText(ticket);
47
+ const vector = await this.embeddings.embed(embeddingText);
48
+ await this.qdrant.upsertOne(RESOLVED_COLLECTION, id, vector, {
49
+ type: 'resolved_ticket',
50
+ ...this.ticketToPayload(ticket),
51
+ });
52
+ return ticket;
53
+ }
54
+ async get(id) {
55
+ const point = await this.qdrant.getPoint(RESOLVED_COLLECTION, id);
56
+ if (!point || !point.payload)
57
+ return null;
58
+ const payload = point.payload;
59
+ if (payload.type !== 'resolved_ticket')
60
+ return null;
61
+ return this.payloadToTicket(payload);
62
+ }
63
+ async getByTicketId(ticketId) {
64
+ const results = await this.qdrant.scroll(RESOLVED_COLLECTION, {
65
+ filter: {
66
+ must: [
67
+ { key: 'type', match: { value: 'resolved_ticket' } },
68
+ { key: 'ticket_id', match: { value: ticketId } },
69
+ ],
70
+ },
71
+ limit: 1,
72
+ with_payload: true,
73
+ });
74
+ if (results.length === 0)
75
+ return null;
76
+ return this.payloadToTicket(results[0].payload);
77
+ }
78
+ async search(query, options = {}) {
79
+ const limit = options.limit || 5;
80
+ const minScore = options.minScore || 0.5;
81
+ const vector = await this.embeddings.embed(query);
82
+ const mustConditions = [
83
+ { key: 'type', match: { value: 'resolved_ticket' } }
84
+ ];
85
+ if (options.category) {
86
+ mustConditions.push({ key: 'problem_category', match: { value: options.category } });
87
+ }
88
+ if (options.outcome) {
89
+ mustConditions.push({ key: 'outcome', match: { value: options.outcome } });
90
+ }
91
+ const results = await this.qdrant.search(RESOLVED_COLLECTION, vector, limit, {
92
+ filter: { must: mustConditions },
93
+ scoreThreshold: minScore,
94
+ });
95
+ return results
96
+ .map(r => {
97
+ const ticket = this.payloadToTicket(r.payload);
98
+ if (!ticket)
99
+ return null;
100
+ return { ticket, score: r.score };
101
+ })
102
+ .filter((r) => r !== null);
103
+ }
104
+ async list(options = {}) {
105
+ const limit = options.limit || 50;
106
+ const mustConditions = [
107
+ { key: 'type', match: { value: 'resolved_ticket' } }
108
+ ];
109
+ if (options.category) {
110
+ mustConditions.push({ key: 'problem_category', match: { value: options.category } });
111
+ }
112
+ if (options.outcome) {
113
+ mustConditions.push({ key: 'outcome', match: { value: options.outcome } });
114
+ }
115
+ if (options.agent_id) {
116
+ mustConditions.push({ key: 'agent_id', match: { value: options.agent_id } });
117
+ }
118
+ const results = await this.qdrant.scroll(RESOLVED_COLLECTION, {
119
+ filter: { must: mustConditions },
120
+ limit,
121
+ with_payload: true,
122
+ });
123
+ return results
124
+ .map(r => this.payloadToTicket(r.payload))
125
+ .filter((t) => t !== null);
126
+ }
127
+ async findSimilarProblems(problem, options = {}) {
128
+ const searchOptions = {
129
+ limit: options.limit || 5,
130
+ minScore: options.minScore || 0.6,
131
+ };
132
+ if (options.onlyPositive) {
133
+ searchOptions.outcome = 'positive';
134
+ }
135
+ return this.search(problem, searchOptions);
136
+ }
137
+ async getCategories() {
138
+ const tickets = await this.list({ limit: 1000 });
139
+ const categoryMap = new Map();
140
+ for (const t of tickets) {
141
+ const count = categoryMap.get(t.problem_category) || 0;
142
+ categoryMap.set(t.problem_category, count + 1);
143
+ }
144
+ return Array.from(categoryMap.entries())
145
+ .map(([category, count]) => ({ category, count }))
146
+ .sort((a, b) => b.count - a.count);
147
+ }
148
+ async stats() {
149
+ const tickets = await this.list({ limit: 1000 });
150
+ const byOutcome = {
151
+ positive: 0,
152
+ negative: 0,
153
+ neutral: 0,
154
+ escalated: 0,
155
+ };
156
+ const byCategory = {};
157
+ let totalSteps = 0;
158
+ for (const t of tickets) {
159
+ byOutcome[t.resolution.outcome]++;
160
+ byCategory[t.problem_category] = (byCategory[t.problem_category] || 0) + 1;
161
+ totalSteps += t.resolution.steps.length;
162
+ }
163
+ return {
164
+ total: tickets.length,
165
+ byOutcome,
166
+ byCategory,
167
+ avgSteps: tickets.length > 0 ? totalSteps / tickets.length : 0,
168
+ };
169
+ }
170
+ async delete(id) {
171
+ await this.qdrant.deletePoints(RESOLVED_COLLECTION, [id]);
172
+ }
173
+ buildEmbeddingText(ticket) {
174
+ const stepsText = ticket.resolution.steps
175
+ .map(s => `${s.order}. ${s.action}: ${s.description}`)
176
+ .join('\n');
177
+ return [
178
+ `Problem: ${ticket.problem}`,
179
+ `Category: ${ticket.problem_category}`,
180
+ `Resolution: ${ticket.resolution.summary}`,
181
+ `Outcome: ${ticket.resolution.outcome}`,
182
+ `Steps:\n${stepsText}`,
183
+ ].join('\n');
184
+ }
185
+ ticketToPayload(ticket) {
186
+ return {
187
+ id: ticket.id,
188
+ ticket_id: ticket.ticket_id,
189
+ problem: ticket.problem,
190
+ problem_category: ticket.problem_category,
191
+ outcome: ticket.resolution.outcome,
192
+ summary: ticket.resolution.summary,
193
+ steps: JSON.stringify(ticket.resolution.steps),
194
+ sop_reference: ticket.resolution.sop_reference || null,
195
+ agent_id: ticket.agent_id,
196
+ files: ticket.files || [],
197
+ tags: ticket.tags,
198
+ created_at: ticket.created_at,
199
+ customer_satisfaction: ticket.customer_satisfaction ?? null,
200
+ metadata: ticket.metadata ? JSON.stringify(ticket.metadata) : null,
201
+ };
202
+ }
203
+ payloadToTicket(payload) {
204
+ if (!payload || payload.type !== 'resolved_ticket')
205
+ return null;
206
+ let steps = [];
207
+ try {
208
+ if (typeof payload.steps === 'string') {
209
+ steps = JSON.parse(payload.steps);
210
+ }
211
+ else if (Array.isArray(payload.steps)) {
212
+ steps = payload.steps;
213
+ }
214
+ }
215
+ catch {
216
+ steps = [];
217
+ }
218
+ let metadata;
219
+ try {
220
+ if (typeof payload.metadata === 'string') {
221
+ metadata = JSON.parse(payload.metadata);
222
+ }
223
+ else if (payload.metadata && typeof payload.metadata === 'object') {
224
+ metadata = payload.metadata;
225
+ }
226
+ }
227
+ catch {
228
+ metadata = undefined;
229
+ }
230
+ return {
231
+ id: String(payload.id),
232
+ ticket_id: Number(payload.ticket_id),
233
+ problem: String(payload.problem || ''),
234
+ problem_category: String(payload.problem_category || 'general'),
235
+ resolution: {
236
+ outcome: payload.outcome || 'neutral',
237
+ summary: String(payload.summary || ''),
238
+ steps,
239
+ sop_reference: payload.sop_reference ? String(payload.sop_reference) : undefined,
240
+ },
241
+ agent_id: String(payload.agent_id || ''),
242
+ files: payload.files || [],
243
+ tags: payload.tags || [],
244
+ created_at: String(payload.created_at || new Date().toISOString()),
245
+ customer_satisfaction: payload.customer_satisfaction ? Number(payload.customer_satisfaction) : undefined,
246
+ metadata,
247
+ };
248
+ }
249
+ }
250
+ export default ResolvedTicketsService;