@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.
- package/dist/commands/biz/invoices.d.ts +11 -0
- package/dist/commands/biz/invoices.js +661 -0
- package/dist/commands/biz/shopify.d.ts +3 -0
- package/dist/commands/biz/shopify.js +592 -0
- package/dist/commands/biz/supplier-feed.d.ts +3 -0
- package/dist/commands/biz/supplier-feed.js +168 -0
- package/dist/commands/biz.js +5 -1
- package/dist/commands/config.d.ts +3 -2
- package/dist/commands/config.js +4 -3
- package/dist/commands/sop.d.ts +3 -0
- package/dist/commands/sop.js +458 -0
- package/dist/commands/task.js +7 -0
- package/dist/lib/biz/gcs-upload.d.ts +86 -0
- package/dist/lib/biz/gcs-upload.js +189 -0
- package/dist/lib/biz/index.d.ts +5 -0
- package/dist/lib/biz/index.js +3 -0
- package/dist/lib/biz/invoice-extractor-registry.d.ts +22 -0
- package/dist/lib/biz/invoice-extractor-registry.js +416 -0
- package/dist/lib/biz/invoice-extractor-types.d.ts +127 -0
- package/dist/lib/biz/invoice-extractor-types.js +6 -0
- package/dist/lib/biz/pattern-detection.d.ts +48 -0
- package/dist/lib/biz/pattern-detection.js +205 -0
- package/dist/lib/biz/resolved-tickets.d.ts +86 -0
- package/dist/lib/biz/resolved-tickets.js +250 -0
- package/dist/lib/biz/shopify.d.ts +196 -0
- package/dist/lib/biz/shopify.js +429 -0
- package/dist/lib/biz/supplier-feed-types.d.ts +96 -0
- package/dist/lib/biz/supplier-feed-types.js +46 -0
- package/dist/lib/biz/supplier-feed.d.ts +32 -0
- package/dist/lib/biz/supplier-feed.js +244 -0
- package/dist/lib/permissions.d.ts +2 -1
- package/dist/types/roles.d.ts +3 -0
- package/dist/types/roles.js +14 -0
- 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;
|
package/dist/commands/biz.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/commands/config.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
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
|
|
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,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;
|