@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,168 @@
1
+ import { Command } from "commander";
2
+ import { SupplierFeedService, SUPPLIER_IDS } from "../../lib/biz/supplier-feed.js";
3
+ import { COLLECTION_NAME } from "../../lib/biz/supplier-feed-types.js";
4
+ export const supplierFeedCommand = new Command("supplier-feed")
5
+ .description("Supplier product catalog for semantic search (Wattiz, Skuterzone, Emove)");
6
+ supplierFeedCommand
7
+ .command("init")
8
+ .description("Create/verify Qdrant collection for supplier products")
9
+ .option("--json", "JSON output")
10
+ .action(async (options) => {
11
+ try {
12
+ const service = new SupplierFeedService();
13
+ await service.ensureCollection();
14
+ if (options.json) {
15
+ console.log(JSON.stringify({ success: true, collection: COLLECTION_NAME }));
16
+ }
17
+ else {
18
+ console.log(`✓ Collection "${COLLECTION_NAME}" ready`);
19
+ }
20
+ }
21
+ catch (err) {
22
+ if (options.json) {
23
+ console.log(JSON.stringify({ success: false, error: err.message }));
24
+ }
25
+ else {
26
+ console.error(`✗ ${err.message}`);
27
+ }
28
+ process.exit(1);
29
+ }
30
+ });
31
+ supplierFeedCommand
32
+ .command("sync")
33
+ .description("Sync products from supplier catalogs into Qdrant")
34
+ .option("-s, --supplier <id>", "Specific supplier (wattiz|skuterzone|emove)")
35
+ .option("-q, --query <query>", "Search query for products", "*")
36
+ .option("-l, --limit <num>", "Max products per supplier", "50")
37
+ .option("-v, --verbose", "Verbose output")
38
+ .option("--json", "JSON output")
39
+ .action(async (options) => {
40
+ try {
41
+ const service = new SupplierFeedService();
42
+ const suppliers = options.supplier
43
+ ? [options.supplier]
44
+ : [...SUPPLIER_IDS];
45
+ if (!options.json && !options.verbose) {
46
+ console.log(`\n Syncing supplier products...`);
47
+ console.log(` Suppliers: ${suppliers.join(', ')}`);
48
+ console.log(` Query: "${options.query}"`);
49
+ console.log(` Limit: ${options.limit} per supplier\n`);
50
+ }
51
+ const results = await service.syncAll({
52
+ suppliers,
53
+ query: options.query,
54
+ limit: parseInt(options.limit, 10),
55
+ verbose: options.verbose,
56
+ });
57
+ if (options.json) {
58
+ console.log(JSON.stringify({ success: true, results }));
59
+ }
60
+ else {
61
+ console.log("\n Results:");
62
+ for (const r of results) {
63
+ console.log(` ${r.supplier}: ${r.productsUpserted}/${r.productsFound} products (${r.durationMs}ms)`);
64
+ if (r.errors > 0) {
65
+ console.log(` ⚠ ${r.errors} error(s)`);
66
+ }
67
+ }
68
+ const total = results.reduce((sum, r) => sum + r.productsUpserted, 0);
69
+ console.log(`\n ✓ Total: ${total} products synced to "${COLLECTION_NAME}"\n`);
70
+ }
71
+ }
72
+ catch (err) {
73
+ if (options.json) {
74
+ console.log(JSON.stringify({ success: false, error: err.message }));
75
+ }
76
+ else {
77
+ console.error(`✗ ${err.message}`);
78
+ }
79
+ process.exit(1);
80
+ }
81
+ });
82
+ supplierFeedCommand
83
+ .command("search <query>")
84
+ .description("Semantic search for products across supplier catalogs")
85
+ .option("-s, --supplier <id>", "Filter by supplier")
86
+ .option("-l, --limit <num>", "Max results", "10")
87
+ .option("--min-score <score>", "Min similarity score (0-1)", "0.5")
88
+ .option("--in-stock", "Only in-stock products")
89
+ .option("--json", "JSON output")
90
+ .action(async (query, options) => {
91
+ try {
92
+ const service = new SupplierFeedService();
93
+ const results = await service.search(query, {
94
+ supplier: options.supplier,
95
+ limit: parseInt(options.limit, 10),
96
+ minScore: parseFloat(options.minScore),
97
+ inStockOnly: options.inStock,
98
+ });
99
+ if (options.json) {
100
+ console.log(JSON.stringify({ success: true, query, results }));
101
+ }
102
+ else {
103
+ console.log(`\n 🔍 Search: "${query}"`);
104
+ if (options.supplier)
105
+ console.log(` Filter: ${options.supplier}`);
106
+ console.log(` Found: ${results.length} result(s)\n`);
107
+ if (results.length === 0) {
108
+ console.log(" No matching products found.\n");
109
+ }
110
+ else {
111
+ for (const r of results) {
112
+ console.log(` [${r.supplier}] ${r.name}`);
113
+ console.log(` Score: ${(r.score * 100).toFixed(1)}%`);
114
+ if (r.sku)
115
+ console.log(` SKU: ${r.sku}`);
116
+ if (r.price)
117
+ console.log(` Price: ${r.price}`);
118
+ if (r.stockStatus)
119
+ console.log(` Stock: ${r.stockStatus}`);
120
+ console.log(` URL: ${r.url}`);
121
+ console.log();
122
+ }
123
+ }
124
+ }
125
+ }
126
+ catch (err) {
127
+ if (options.json) {
128
+ console.log(JSON.stringify({ success: false, error: err.message }));
129
+ }
130
+ else {
131
+ console.error(`✗ ${err.message}`);
132
+ }
133
+ process.exit(1);
134
+ }
135
+ });
136
+ supplierFeedCommand
137
+ .command("stats")
138
+ .description("Show collection statistics")
139
+ .option("--json", "JSON output")
140
+ .action(async (options) => {
141
+ try {
142
+ const service = new SupplierFeedService();
143
+ const stats = await service.getStats();
144
+ if (options.json) {
145
+ console.log(JSON.stringify({ success: true, collection: COLLECTION_NAME, ...stats }));
146
+ }
147
+ else {
148
+ console.log(`\n 📊 Supplier Feed Statistics`);
149
+ console.log(` Collection: ${COLLECTION_NAME}`);
150
+ console.log(` Total products: ${stats.total}\n`);
151
+ console.log(` By supplier:`);
152
+ for (const [supplier, count] of Object.entries(stats.bySupplier)) {
153
+ console.log(` ${supplier}: ${count}`);
154
+ }
155
+ console.log();
156
+ }
157
+ }
158
+ catch (err) {
159
+ if (options.json) {
160
+ console.log(JSON.stringify({ success: false, error: err.message }));
161
+ }
162
+ else {
163
+ console.error(`✗ ${err.message}`);
164
+ }
165
+ process.exit(1);
166
+ }
167
+ });
168
+ export default supplierFeedCommand;
@@ -15,6 +15,8 @@ import { gotessCommand } from "./biz/gotess.js";
15
15
  import { skuterzoneCommand } from "./biz/skuterzone.js";
16
16
  import { emoveCommand } from "./biz/emove.js";
17
17
  import { wattizCommand } from "./biz/wattiz.js";
18
+ import { shopifyCommand } from "./biz/shopify.js";
19
+ import { supplierFeedCommand } from "./biz/supplier-feed.js";
18
20
  import { guards } from "../lib/permissions.js";
19
21
  export const bizCommand = new Command("biz")
20
22
  .description("Business operations for autonomous agents")
@@ -28,5 +30,7 @@ export const bizCommand = new Command("biz")
28
30
  .addCommand(gotessCommand)
29
31
  .addCommand(skuterzoneCommand)
30
32
  .addCommand(emoveCommand)
31
- .addCommand(wattizCommand);
33
+ .addCommand(wattizCommand)
34
+ .addCommand(shopifyCommand)
35
+ .addCommand(supplierFeedCommand);
32
36
  export default bizCommand;
@@ -1,6 +1,5 @@
1
1
  import { Command } from "commander";
2
- declare const VALID_ROLES: readonly ["admin", "supervisor", "worker", "reviewer", "e2e_agent", "pr_agent", "support", "devops", "purchasing", "ops"];
3
- type AgentRole = typeof VALID_ROLES[number];
2
+ import { type AgentRole } from "../types/roles.js";
4
3
  interface Config {
5
4
  apiUrl?: string;
6
5
  apiKey?: string;
@@ -44,6 +43,8 @@ interface Config {
44
43
  wattizBaseUrl?: string;
45
44
  wattizLanguage?: string;
46
45
  gcsBucket?: string;
46
+ shopifyDomain?: string;
47
+ shopifyToken?: string;
47
48
  }
48
49
  export declare function getConfig(): Config;
49
50
  export declare function saveConfig(config: Config): void;
@@ -4,15 +4,16 @@ import { join } from "path";
4
4
  import { homedir } from "os";
5
5
  import { ErrorHelpers, errorWithHint, ExplainTopic } from "../lib/error-hints.js";
6
6
  import { getApiClient } from "../lib/api-client.js";
7
- // Valid agent roles - used for runtime validation
8
- const VALID_ROLES = ["admin", "supervisor", "worker", "reviewer", "e2e_agent", "pr_agent", "support", "devops", "purchasing", "ops"];
7
+ import { AGENT_ROLES, isValidAgentRole } from "../types/roles.js";
8
+ // Re-export for backward compatibility
9
+ const VALID_ROLES = AGENT_ROLES;
9
10
  const CONFIG_DIR = join(homedir(), ".husky");
10
11
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
11
12
  /**
12
13
  * Validate if a string is a valid AgentRole
13
14
  */
14
15
  function isValidRole(role) {
15
- return VALID_ROLES.includes(role);
16
+ return isValidAgentRole(role);
16
17
  }
17
18
  // API Key validation - must be at least 16 characters, alphanumeric + common key chars (base64, JWT, etc.)
18
19
  function validateApiKey(key) {
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare const sopCommand: Command;
3
+ export default sopCommand;
@@ -0,0 +1,458 @@
1
+ import { Command } from "commander";
2
+ import { SOPService, SOP_STATUSES } from "../lib/biz/sop.js";
3
+ import { PatternDetectionService } from "../lib/biz/pattern-detection.js";
4
+ const DEFAULT_APPROVER = process.env.HUSKY_AGENT_ID || 'system';
5
+ function validateStatus(status) {
6
+ if (!status)
7
+ return undefined;
8
+ if (!SOP_STATUSES.includes(status)) {
9
+ console.error(`Invalid status: ${status}. Must be one of: ${SOP_STATUSES.join(', ')}`);
10
+ process.exit(1);
11
+ }
12
+ return status;
13
+ }
14
+ export const sopCommand = new Command("sop")
15
+ .description("Standard Operating Procedures management");
16
+ sopCommand
17
+ .command("list")
18
+ .description("List SOPs")
19
+ .option("-s, --status <status>", `Filter by status (${SOP_STATUSES.join(", ")})`)
20
+ .option("-c, --category <category>", "Filter by category")
21
+ .option("-l, --limit <num>", "Max results", "50")
22
+ .option("--json", "Output as JSON")
23
+ .action(async (options) => {
24
+ try {
25
+ const service = new SOPService();
26
+ const sops = await service.list({
27
+ status: options.status,
28
+ category: options.category,
29
+ limit: parseInt(options.limit, 10),
30
+ });
31
+ if (options.json) {
32
+ console.log(JSON.stringify(sops, null, 2));
33
+ return;
34
+ }
35
+ console.log(`\n 📋 SOPs (${sops.length})\n`);
36
+ if (sops.length === 0) {
37
+ console.log(" No SOPs found.");
38
+ return;
39
+ }
40
+ for (const sop of sops) {
41
+ const statusIcon = getStatusIcon(sop.status);
42
+ console.log(` ${statusIcon} ${sop.id.padEnd(12)} │ ${sop.status.padEnd(10)} │ ${sop.category.padEnd(15)} │ ${sop.title.slice(0, 35)}`);
43
+ }
44
+ console.log("");
45
+ }
46
+ catch (error) {
47
+ console.error("Error:", error.message);
48
+ process.exit(1);
49
+ }
50
+ });
51
+ sopCommand
52
+ .command("get <id>")
53
+ .description("Get SOP details")
54
+ .option("--json", "Output as JSON")
55
+ .action(async (id, options) => {
56
+ try {
57
+ const service = new SOPService();
58
+ const sop = await service.get(id);
59
+ if (!sop) {
60
+ console.error(`SOP not found: ${id}`);
61
+ process.exit(1);
62
+ }
63
+ if (options.json) {
64
+ console.log(JSON.stringify(sop, null, 2));
65
+ return;
66
+ }
67
+ console.log(`\n 📋 SOP: ${sop.title}`);
68
+ console.log(" " + "─".repeat(60));
69
+ console.log(` ID: ${sop.id}`);
70
+ console.log(` Status: ${getStatusIcon(sop.status)} ${sop.status}`);
71
+ console.log(` Category: ${sop.category}`);
72
+ console.log(` Version: ${sop.version}`);
73
+ console.log(` Trigger: ${sop.trigger}`);
74
+ console.log(` Created: ${sop.created_at}`);
75
+ console.log(` Updated: ${sop.updated_at}`);
76
+ if (sop.approved_by) {
77
+ console.log(` Approved by: ${sop.approved_by} (${sop.approved_at})`);
78
+ }
79
+ if (sop.deprecated_by) {
80
+ console.log(` Deprecated by: ${sop.deprecated_by} (${sop.deprecated_at})`);
81
+ }
82
+ console.log(`\n Description:\n ${sop.description}`);
83
+ console.log(`\n Steps (${sop.steps.length}):`);
84
+ for (const step of sop.steps) {
85
+ const required = step.required ? "●" : "○";
86
+ console.log(` ${required} ${step.order}. ${step.action}`);
87
+ console.log(` ${step.description}`);
88
+ if (step.tool)
89
+ console.log(` Tool: ${step.tool}`);
90
+ if (step.conditions)
91
+ console.log(` Conditions: ${step.conditions}`);
92
+ }
93
+ if (sop.tags.length > 0) {
94
+ console.log(`\n Tags: ${sop.tags.join(", ")}`);
95
+ }
96
+ if (sop.source_tickets.length > 0) {
97
+ console.log(` Source tickets: ${sop.source_tickets.join(", ")}`);
98
+ }
99
+ console.log("");
100
+ }
101
+ catch (error) {
102
+ console.error("Error:", error.message);
103
+ process.exit(1);
104
+ }
105
+ });
106
+ sopCommand
107
+ .command("search <query>")
108
+ .description("Semantic search for SOPs")
109
+ .option("-l, --limit <num>", "Max results", "5")
110
+ .option("-s, --status <status>", `Filter by status (${SOP_STATUSES.join(", ")})`)
111
+ .option("-m, --min-score <score>", "Minimum similarity score", "0.5")
112
+ .option("--json", "Output as JSON")
113
+ .action(async (query, options) => {
114
+ try {
115
+ const service = new SOPService();
116
+ console.log(` Searching SOPs for: "${query}"...`);
117
+ const results = await service.search(query, {
118
+ limit: parseInt(options.limit, 10),
119
+ status: options.status,
120
+ minScore: parseFloat(options.minScore),
121
+ });
122
+ if (options.json) {
123
+ console.log(JSON.stringify(results, null, 2));
124
+ return;
125
+ }
126
+ console.log(`\n 🔍 Search results (${results.length})\n`);
127
+ if (results.length === 0) {
128
+ console.log(" No matching SOPs found.");
129
+ return;
130
+ }
131
+ for (const { sop, score } of results) {
132
+ const statusIcon = getStatusIcon(sop.status);
133
+ console.log(` [${(score * 100).toFixed(1)}%] ${statusIcon} ${sop.id} │ ${sop.title.slice(0, 40)}`);
134
+ }
135
+ console.log("");
136
+ }
137
+ catch (error) {
138
+ console.error("Error:", error.message);
139
+ process.exit(1);
140
+ }
141
+ });
142
+ sopCommand
143
+ .command("create")
144
+ .description("Create a new SOP (draft)")
145
+ .requiredOption("-t, --title <title>", "SOP title")
146
+ .requiredOption("-d, --description <desc>", "SOP description")
147
+ .requiredOption("--trigger <trigger>", "Trigger phrase/condition")
148
+ .requiredOption("-c, --category <category>", "Category")
149
+ .option("--steps <json>", "Steps as JSON array")
150
+ .option("--tags <tags>", "Comma-separated tags")
151
+ .option("--source-tickets <ids>", "Comma-separated ticket IDs")
152
+ .option("--json", "Output as JSON")
153
+ .action(async (options) => {
154
+ try {
155
+ const service = new SOPService();
156
+ let steps = [];
157
+ if (options.steps) {
158
+ try {
159
+ steps = JSON.parse(options.steps);
160
+ }
161
+ catch {
162
+ console.error("Invalid JSON for --steps");
163
+ process.exit(1);
164
+ }
165
+ }
166
+ const input = {
167
+ title: options.title,
168
+ description: options.description,
169
+ trigger: options.trigger,
170
+ category: options.category,
171
+ steps,
172
+ tags: options.tags ? options.tags.split(",").map((t) => t.trim()) : [],
173
+ source_tickets: options.sourceTickets
174
+ ? options.sourceTickets.split(",").map((id) => parseInt(id.trim(), 10))
175
+ : [],
176
+ };
177
+ console.log(` Creating SOP: "${input.title}"...`);
178
+ const sop = await service.create(input);
179
+ if (options.json) {
180
+ console.log(JSON.stringify(sop, null, 2));
181
+ return;
182
+ }
183
+ console.log(` ✓ SOP created: ${sop.id}`);
184
+ console.log(` Status: ${sop.status} (use 'husky sop approve ${sop.id}' to approve)`);
185
+ }
186
+ catch (error) {
187
+ console.error("Error:", error.message);
188
+ process.exit(1);
189
+ }
190
+ });
191
+ sopCommand
192
+ .command("approve <id>")
193
+ .description("Approve a draft SOP")
194
+ .option("-a, --approver <name>", "Approver name/ID", DEFAULT_APPROVER)
195
+ .option("--json", "Output as JSON")
196
+ .action(async (id, options) => {
197
+ try {
198
+ const service = new SOPService();
199
+ console.log(` Approving SOP: ${id}...`);
200
+ const sop = await service.approve(id, options.approver);
201
+ if (!sop) {
202
+ console.error(`SOP not found: ${id}`);
203
+ process.exit(1);
204
+ }
205
+ if (options.json) {
206
+ console.log(JSON.stringify(sop, null, 2));
207
+ return;
208
+ }
209
+ console.log(` ✓ SOP approved: ${sop.id}`);
210
+ console.log(` Approved by: ${sop.approved_by}`);
211
+ }
212
+ catch (error) {
213
+ console.error("Error:", error.message);
214
+ process.exit(1);
215
+ }
216
+ });
217
+ sopCommand
218
+ .command("deprecate <id>")
219
+ .description("Deprecate an SOP")
220
+ .option("-d, --deprecator <name>", "Deprecator name/ID", DEFAULT_APPROVER)
221
+ .option("--json", "Output as JSON")
222
+ .action(async (id, options) => {
223
+ try {
224
+ const service = new SOPService();
225
+ console.log(` Deprecating SOP: ${id}...`);
226
+ const sop = await service.deprecate(id, options.deprecator);
227
+ if (!sop) {
228
+ console.error(`SOP not found: ${id}`);
229
+ process.exit(1);
230
+ }
231
+ if (options.json) {
232
+ console.log(JSON.stringify(sop, null, 2));
233
+ return;
234
+ }
235
+ console.log(` ✓ SOP deprecated: ${sop.id}`);
236
+ console.log(` Deprecated by: ${sop.deprecated_by}`);
237
+ }
238
+ catch (error) {
239
+ console.error("Error:", error.message);
240
+ process.exit(1);
241
+ }
242
+ });
243
+ sopCommand
244
+ .command("delete <id>")
245
+ .description("Delete an SOP permanently")
246
+ .option("-y, --yes", "Skip confirmation")
247
+ .action(async (id, options) => {
248
+ try {
249
+ const service = new SOPService();
250
+ if (!options.yes) {
251
+ console.log(` Warning: This will permanently delete SOP ${id}`);
252
+ console.log(` Use --yes to confirm.`);
253
+ process.exit(1);
254
+ }
255
+ await service.delete(id);
256
+ console.log(` ✓ SOP deleted: ${id}`);
257
+ }
258
+ catch (error) {
259
+ console.error("Error:", error.message);
260
+ process.exit(1);
261
+ }
262
+ });
263
+ sopCommand
264
+ .command("stats")
265
+ .description("Show SOP statistics")
266
+ .option("--json", "Output as JSON")
267
+ .action(async (options) => {
268
+ try {
269
+ const service = new SOPService();
270
+ const stats = await service.stats();
271
+ if (options.json) {
272
+ console.log(JSON.stringify(stats, null, 2));
273
+ return;
274
+ }
275
+ console.log(`\n 📊 SOP Statistics`);
276
+ console.log(" " + "─".repeat(40));
277
+ console.log(` Total SOPs: ${stats.total}`);
278
+ console.log(`\n By Status:`);
279
+ console.log(` 📝 Draft: ${stats.byStatus.draft}`);
280
+ console.log(` ✅ Approved: ${stats.byStatus.approved}`);
281
+ console.log(` ⛔ Deprecated: ${stats.byStatus.deprecated}`);
282
+ if (Object.keys(stats.byCategory).length > 0) {
283
+ console.log(`\n By Category:`);
284
+ for (const [cat, count] of Object.entries(stats.byCategory).sort((a, b) => b[1] - a[1])) {
285
+ console.log(` ${cat}: ${count}`);
286
+ }
287
+ }
288
+ console.log("");
289
+ }
290
+ catch (error) {
291
+ console.error("Error:", error.message);
292
+ process.exit(1);
293
+ }
294
+ });
295
+ sopCommand
296
+ .command("categories")
297
+ .description("List SOP categories")
298
+ .option("--json", "Output as JSON")
299
+ .action(async (options) => {
300
+ try {
301
+ const service = new SOPService();
302
+ const categories = await service.getCategories();
303
+ if (options.json) {
304
+ console.log(JSON.stringify(categories, null, 2));
305
+ return;
306
+ }
307
+ console.log(`\n 📁 SOP Categories (${categories.length})\n`);
308
+ if (categories.length === 0) {
309
+ console.log(" No categories found.");
310
+ return;
311
+ }
312
+ for (const { category, count } of categories) {
313
+ console.log(` ${category.padEnd(20)} │ ${count} SOP(s)`);
314
+ }
315
+ console.log("");
316
+ }
317
+ catch (error) {
318
+ console.error("Error:", error.message);
319
+ process.exit(1);
320
+ }
321
+ });
322
+ sopCommand
323
+ .command("find-trigger <trigger>")
324
+ .description("Find SOP matching a trigger phrase")
325
+ .option("-m, --min-score <score>", "Minimum similarity score", "0.7")
326
+ .option("--json", "Output as JSON")
327
+ .action(async (trigger, options) => {
328
+ try {
329
+ const service = new SOPService();
330
+ console.log(` Finding SOP for trigger: "${trigger}"...`);
331
+ const result = await service.findByTrigger(trigger, {
332
+ minScore: parseFloat(options.minScore),
333
+ });
334
+ if (options.json) {
335
+ console.log(JSON.stringify(result, null, 2));
336
+ return;
337
+ }
338
+ if (!result) {
339
+ console.log("\n No matching approved SOP found.");
340
+ return;
341
+ }
342
+ console.log(`\n ✓ Found SOP: ${result.sop.id} (${(result.score * 100).toFixed(1)}% match)`);
343
+ console.log(` Title: ${result.sop.title}`);
344
+ console.log(` Steps: ${result.sop.steps.length}`);
345
+ console.log(`\n Run 'husky sop get ${result.sop.id}' for full details.`);
346
+ }
347
+ catch (error) {
348
+ console.error("Error:", error.message);
349
+ process.exit(1);
350
+ }
351
+ });
352
+ function getStatusIcon(status) {
353
+ switch (status) {
354
+ case "draft": return "📝";
355
+ case "approved": return "✅";
356
+ case "deprecated": return "⛔";
357
+ default: return "○";
358
+ }
359
+ }
360
+ const detectCommand = sopCommand
361
+ .command("detect")
362
+ .description("Detect patterns in resolved tickets for SOP suggestions");
363
+ detectCommand
364
+ .command("analyze")
365
+ .description("Analyze resolved tickets for patterns")
366
+ .option("-c, --category <category>", "Filter by category")
367
+ .option("-m, --min-cluster <num>", "Minimum cluster size", "3")
368
+ .option("-s, --similarity <score>", "Similarity threshold", "0.75")
369
+ .option("-l, --limit <num>", "Max tickets to analyze", "500")
370
+ .option("--json", "Output as JSON")
371
+ .action(async (options) => {
372
+ try {
373
+ const service = new PatternDetectionService();
374
+ console.log(" Analyzing resolved tickets for patterns...");
375
+ const analysis = await service.detectPatterns({
376
+ category: options.category,
377
+ minClusterSize: parseInt(options.minCluster, 10),
378
+ similarityThreshold: parseFloat(options.similarity),
379
+ limit: parseInt(options.limit, 10),
380
+ });
381
+ if (options.json) {
382
+ console.log(JSON.stringify(analysis, null, 2));
383
+ return;
384
+ }
385
+ console.log(`\n 🔍 Pattern Analysis Results`);
386
+ console.log(" " + "─".repeat(50));
387
+ console.log(` Total tickets analyzed: ${analysis.total_tickets}`);
388
+ console.log(` Clusters found: ${analysis.clusters.length}`);
389
+ console.log(` Unmatched tickets: ${analysis.unmatched_tickets.length}`);
390
+ if (analysis.clusters.length > 0) {
391
+ console.log(`\n 📊 Detected Patterns:\n`);
392
+ for (const cluster of analysis.clusters) {
393
+ const size = cluster.members.length + 1;
394
+ const confidence = cluster.suggested_sop?.confidence_score || 0;
395
+ console.log(` ${cluster.id}`);
396
+ console.log(` Size: ${size} tickets`);
397
+ console.log(` Category: ${cluster.problem_category}`);
398
+ console.log(` Similarity: ${(cluster.similarity * 100).toFixed(0)}%`);
399
+ console.log(` Confidence: ${(confidence * 100).toFixed(0)}%`);
400
+ console.log(` Problem: ${cluster.representative.problem.slice(0, 50)}...`);
401
+ console.log("");
402
+ }
403
+ console.log(` 💡 To create an SOP from a pattern:`);
404
+ console.log(` husky sop detect create-from <cluster_id>`);
405
+ }
406
+ else {
407
+ console.log("\n No significant patterns detected.");
408
+ console.log(" Try lowering --similarity or --min-cluster thresholds.");
409
+ }
410
+ console.log("");
411
+ }
412
+ catch (error) {
413
+ console.error("Error:", error.message);
414
+ process.exit(1);
415
+ }
416
+ });
417
+ detectCommand
418
+ .command("suggestions")
419
+ .description("List SOP suggestions from detected patterns")
420
+ .option("-c, --category <category>", "Filter by category")
421
+ .option("-l, --limit <num>", "Max suggestions", "10")
422
+ .option("--json", "Output as JSON")
423
+ .action(async (options) => {
424
+ try {
425
+ const service = new PatternDetectionService();
426
+ console.log(" Generating SOP suggestions...");
427
+ const analysis = await service.detectPatterns({
428
+ category: options.category,
429
+ minClusterSize: 3,
430
+ similarityThreshold: 0.7,
431
+ limit: 500,
432
+ });
433
+ const suggestions = analysis.suggestions.slice(0, parseInt(options.limit, 10));
434
+ if (options.json) {
435
+ console.log(JSON.stringify(suggestions, null, 2));
436
+ return;
437
+ }
438
+ console.log(`\n 💡 SOP Suggestions (${suggestions.length})\n`);
439
+ if (suggestions.length === 0) {
440
+ console.log(" No suggestions available.");
441
+ console.log(" Log more resolved tickets with 'husky biz tickets learn log'.");
442
+ return;
443
+ }
444
+ for (const [i, sug] of suggestions.entries()) {
445
+ console.log(` ${i + 1}. ${sug.title.slice(0, 50)}`);
446
+ console.log(` Category: ${sug.category}`);
447
+ console.log(` Steps: ${sug.steps.length}`);
448
+ console.log(` Confidence: ${(sug.confidence_score * 100).toFixed(0)}%`);
449
+ console.log(` Source tickets: ${sug.source_tickets.length}`);
450
+ console.log("");
451
+ }
452
+ }
453
+ catch (error) {
454
+ console.error("Error:", error.message);
455
+ process.exit(1);
456
+ }
457
+ });
458
+ export default sopCommand;