@simonfestl/husky-cli 1.6.5 → 1.8.2
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/README.md +46 -0
- package/dist/commands/agent.js +43 -0
- package/dist/commands/biz/tickets.js +31 -1
- package/dist/commands/brain.js +279 -8
- package/dist/commands/chat.js +124 -1
- package/dist/commands/config.d.ts +4 -0
- package/dist/commands/config.js +9 -1
- package/dist/commands/e2e.js +361 -9
- package/dist/commands/image.d.ts +2 -0
- package/dist/commands/image.js +141 -0
- package/dist/commands/llm-context.js +69 -2
- package/dist/commands/task.js +109 -5
- package/dist/commands/vm.js +272 -0
- package/dist/commands/youtube.d.ts +2 -0
- package/dist/commands/youtube.js +178 -0
- package/dist/index.js +4 -0
- package/dist/lib/agent-identity.d.ts +25 -0
- package/dist/lib/agent-identity.js +73 -0
- package/dist/lib/biz/agent-brain.d.ts +63 -1
- package/dist/lib/biz/agent-brain.js +316 -4
- package/dist/lib/biz/learning-capture.d.ts +42 -0
- package/dist/lib/biz/learning-capture.js +107 -0
- package/dist/lib/biz/pii-filter.d.ts +34 -0
- package/dist/lib/biz/pii-filter.js +125 -0
- package/dist/lib/biz/qdrant.d.ts +5 -1
- package/dist/lib/biz/qdrant.js +20 -6
- package/dist/lib/biz/sop-generator.d.ts +39 -0
- package/dist/lib/biz/sop-generator.js +131 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -111,6 +111,38 @@ husky vm update <session-id> --status approved
|
|
|
111
111
|
husky vm delete <session-id>
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
### E2E Testing (E2E Agent)
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Run E2E tests for a task
|
|
118
|
+
husky e2e run <task-id>
|
|
119
|
+
husky e2e run <task-id> --secret ADMIN_PASSWORD --retries 2
|
|
120
|
+
husky e2e run <task-id> --env TEST_USER=admin --headed
|
|
121
|
+
|
|
122
|
+
# E2E Inbox (pending test requests)
|
|
123
|
+
husky e2e inbox # List all inbox messages
|
|
124
|
+
husky e2e inbox --status pending # Filter by status
|
|
125
|
+
husky e2e inbox --task <task-id> # Filter by task
|
|
126
|
+
|
|
127
|
+
# Watch for new E2E requests (auto-processing)
|
|
128
|
+
husky e2e watch --interval 30
|
|
129
|
+
husky e2e watch --once # Process once and exit
|
|
130
|
+
|
|
131
|
+
# Complete E2E test request (used by e2e-bridge)
|
|
132
|
+
husky e2e done <inbox-id> --passed # Mark as passed
|
|
133
|
+
husky e2e done <inbox-id> --failed --notes "reason"
|
|
134
|
+
|
|
135
|
+
# Browser automation utilities
|
|
136
|
+
husky e2e screenshot <url> # Take screenshot
|
|
137
|
+
husky e2e screenshot <url> --upload # Upload to GCS
|
|
138
|
+
husky e2e record <url> # Record browser session
|
|
139
|
+
|
|
140
|
+
# Artifact management
|
|
141
|
+
husky e2e upload <file> --task <id> # Upload to GCS
|
|
142
|
+
husky e2e list --task <id> # List artifacts
|
|
143
|
+
husky e2e clean --older-than 7 # Clean old artifacts
|
|
144
|
+
```
|
|
145
|
+
|
|
114
146
|
### Business Strategy
|
|
115
147
|
|
|
116
148
|
```bash
|
|
@@ -296,6 +328,20 @@ husky --version
|
|
|
296
328
|
|
|
297
329
|
## Changelog
|
|
298
330
|
|
|
331
|
+
### v1.7.0 (2026-01-11) - E2E Agent Production Ready
|
|
332
|
+
|
|
333
|
+
**New Features:**
|
|
334
|
+
- `husky e2e inbox` - List E2E test requests from API
|
|
335
|
+
- `husky e2e watch` - Watch for and auto-process E2E requests
|
|
336
|
+
- `husky e2e run --secret` - Inject secrets from GCP Secret Manager
|
|
337
|
+
- `husky e2e run --env` - Set environment variables for tests
|
|
338
|
+
- `husky e2e run --retries` - Retry failed tests automatically
|
|
339
|
+
|
|
340
|
+
**Improvements:**
|
|
341
|
+
- All artifact URLs now use HTTPS
|
|
342
|
+
- Better error handling in E2E commands
|
|
343
|
+
- Updated permissions for e2e_agent role
|
|
344
|
+
|
|
299
345
|
### v1.1.0 (2026-01-09) - Unified Reply System
|
|
300
346
|
|
|
301
347
|
**New Features:**
|
package/dist/commands/agent.js
CHANGED
|
@@ -376,6 +376,49 @@ agentCommand
|
|
|
376
376
|
process.exit(1);
|
|
377
377
|
}
|
|
378
378
|
});
|
|
379
|
+
// husky agent qa-review
|
|
380
|
+
agentCommand
|
|
381
|
+
.command("qa-review <taskId>")
|
|
382
|
+
.description("Trigger QA review for a task")
|
|
383
|
+
.option("--pr <url>", "PR URL to review")
|
|
384
|
+
.option("--json", "Output as JSON")
|
|
385
|
+
.action(async (taskId, options) => {
|
|
386
|
+
const config = getConfig();
|
|
387
|
+
if (!config.apiUrl) {
|
|
388
|
+
console.error("Error: API URL not configured. Run: husky config set api-url <url>");
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
const res = await fetch(`${config.apiUrl}/api/tasks/${taskId}/assign-reviewer`, {
|
|
393
|
+
method: "POST",
|
|
394
|
+
headers: {
|
|
395
|
+
"Content-Type": "application/json",
|
|
396
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
397
|
+
},
|
|
398
|
+
body: JSON.stringify({
|
|
399
|
+
prUrl: options.pr,
|
|
400
|
+
}),
|
|
401
|
+
});
|
|
402
|
+
if (!res.ok) {
|
|
403
|
+
const error = await res.text();
|
|
404
|
+
console.error(`Error assigning reviewer: ${res.status} - ${error}`);
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
const data = await res.json();
|
|
408
|
+
if (options.json) {
|
|
409
|
+
console.log(JSON.stringify(data, null, 2));
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
console.log(`\n✓ Task ${taskId} assigned to reviewer`);
|
|
413
|
+
console.log(` Inbox ID: ${data.inboxId}`);
|
|
414
|
+
console.log(` Message: ${data.message}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
console.error("Error triggering QA review:", error);
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
379
422
|
// husky agent register
|
|
380
423
|
agentCommand
|
|
381
424
|
.command("register")
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { ZendeskClient } from "../../lib/biz/index.js";
|
|
8
|
+
import { AgentBrain } from "../../lib/biz/agent-brain.js";
|
|
8
9
|
import * as fs from "fs";
|
|
9
10
|
import * as path from "path";
|
|
10
11
|
export const ticketsCommand = new Command("tickets")
|
|
@@ -181,9 +182,38 @@ ticketsCommand
|
|
|
181
182
|
ticketsCommand
|
|
182
183
|
.command("close <id>")
|
|
183
184
|
.description("Close/solve a ticket")
|
|
184
|
-
.
|
|
185
|
+
.option("--learning", "Capture learning from ticket before closing")
|
|
186
|
+
.option("-a, --agent <id>", "Agent ID for learning capture")
|
|
187
|
+
.action(async (id, options) => {
|
|
185
188
|
try {
|
|
186
189
|
const client = ZendeskClient.fromConfig();
|
|
190
|
+
// Capture learning if flag is set
|
|
191
|
+
if (options.learning) {
|
|
192
|
+
const ticketId = parseInt(id, 10);
|
|
193
|
+
const ticket = await client.getTicket(ticketId);
|
|
194
|
+
const comments = await client.getTicketComments(ticketId);
|
|
195
|
+
// Build learning content from ticket and resolution
|
|
196
|
+
const learningContent = [
|
|
197
|
+
`Ticket #${ticket.id}: ${ticket.subject}`,
|
|
198
|
+
`Status: ${ticket.status} → solved`,
|
|
199
|
+
`Priority: ${ticket.priority}`,
|
|
200
|
+
`Tags: ${ticket.tags?.join(', ') || 'none'}`,
|
|
201
|
+
'',
|
|
202
|
+
'Resolution:',
|
|
203
|
+
comments.slice(-3).map((c) => `- ${c.body.substring(0, 200)}${c.body.length > 200 ? '...' : ''}`).join('\n')
|
|
204
|
+
].join('\n');
|
|
205
|
+
// Capture to Brain
|
|
206
|
+
const agentId = options.agent || process.env.HUSKY_AGENT_ID || 'support-agent';
|
|
207
|
+
const brain = new AgentBrain(agentId, 'support');
|
|
208
|
+
await brain.remember(learningContent, ['ticket-resolution', 'support', ...(ticket.tags || [])], {
|
|
209
|
+
ticketId: ticket.id,
|
|
210
|
+
subject: ticket.subject,
|
|
211
|
+
priority: ticket.priority,
|
|
212
|
+
closedAt: new Date().toISOString()
|
|
213
|
+
}, 'private' // visibility
|
|
214
|
+
);
|
|
215
|
+
console.log(` 💡 Captured learning from ticket #${ticket.id}`);
|
|
216
|
+
}
|
|
187
217
|
const ticket = await client.closeTicket(parseInt(id, 10));
|
|
188
218
|
console.log(`✓ Ticket #${ticket.id} marked as solved`);
|
|
189
219
|
}
|
package/dist/commands/brain.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { AgentBrain, AGENT_TYPES, isValidAgentType } from "../lib/biz/agent-brain.js";
|
|
3
|
+
import { generateSOP, formatSOPAsMarkdown } from "../lib/biz/sop-generator.js";
|
|
3
4
|
const DEFAULT_AGENT = process.env.HUSKY_AGENT_ID || 'default';
|
|
4
5
|
function createBrain(agentId, agentType) {
|
|
5
6
|
const validAgentType = isValidAgentType(agentType) ? agentType : undefined;
|
|
@@ -13,14 +14,21 @@ brainCommand
|
|
|
13
14
|
.option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
|
|
14
15
|
.option("-t, --tags <tags>", "Comma-separated tags")
|
|
15
16
|
.option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
|
|
17
|
+
.option("--visibility <level>", "Visibility level (private, team, public)", "private")
|
|
18
|
+
.option("--allow-pii", "Skip PII filtering (use only for technical/internal content)")
|
|
16
19
|
.option("--json", "Output as JSON")
|
|
17
20
|
.action(async (content, options) => {
|
|
18
21
|
try {
|
|
19
22
|
const brain = createBrain(options.agent, options.agentType);
|
|
20
23
|
const tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : [];
|
|
24
|
+
const visibility = options.visibility;
|
|
21
25
|
const dbInfo = brain.getDatabaseInfo();
|
|
26
|
+
if (!["private", "team", "public"].includes(visibility)) {
|
|
27
|
+
console.error("Error: Visibility must be 'private', 'team', or 'public'");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
22
30
|
console.log(` Storing memory for agent: ${options.agent} (db: ${dbInfo.databaseName})...`);
|
|
23
|
-
const id = await brain.remember(content, tags);
|
|
31
|
+
const id = await brain.remember(content, tags, undefined, visibility, options.allowPii);
|
|
24
32
|
if (options.json) {
|
|
25
33
|
console.log(JSON.stringify({ success: true, id, agent: options.agent, database: dbInfo.databaseName }));
|
|
26
34
|
}
|
|
@@ -40,25 +48,39 @@ brainCommand
|
|
|
40
48
|
.option("-l, --limit <num>", "Max results", "5")
|
|
41
49
|
.option("-m, --min-score <score>", "Minimum similarity score (0-1)", "0.5")
|
|
42
50
|
.option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
|
|
51
|
+
.option("--shared", "Search shared memories from other agents")
|
|
52
|
+
.option("--public-only", "Search only public memories (requires --shared)")
|
|
43
53
|
.option("--json", "Output as JSON")
|
|
44
54
|
.action(async (query, options) => {
|
|
45
55
|
try {
|
|
46
56
|
const brain = createBrain(options.agent, options.agentType);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
let results;
|
|
58
|
+
if (options.shared) {
|
|
59
|
+
// Search shared memories
|
|
60
|
+
results = await brain.recallShared(query, parseInt(options.limit, 10), parseFloat(options.minScore), options.publicOnly);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// Search personal memories
|
|
64
|
+
const dbInfo = brain.getDatabaseInfo();
|
|
65
|
+
console.log(` Searching memories for: "${query}" (db: ${dbInfo.databaseName})...`);
|
|
66
|
+
results = await brain.recall(query, parseInt(options.limit, 10), parseFloat(options.minScore));
|
|
67
|
+
}
|
|
50
68
|
if (options.json) {
|
|
51
|
-
|
|
69
|
+
const dbInfo = brain.getDatabaseInfo();
|
|
70
|
+
console.log(JSON.stringify({ success: true, query, database: dbInfo.databaseName, shared: options.shared || false, results }));
|
|
52
71
|
return;
|
|
53
72
|
}
|
|
54
|
-
|
|
73
|
+
const icon = options.shared ? '🌐' : '🧠';
|
|
74
|
+
const label = options.shared ? 'Shared Memories' : 'Memories';
|
|
75
|
+
console.log(`\n ${icon} ${label} for "${query}" (${results.length} found)\n`);
|
|
55
76
|
if (results.length === 0) {
|
|
56
|
-
console.log(
|
|
77
|
+
console.log(` No relevant ${options.shared ? 'shared ' : ''}memories found.`);
|
|
57
78
|
return;
|
|
58
79
|
}
|
|
59
80
|
for (const r of results) {
|
|
60
81
|
const tags = r.memory.tags.length > 0 ? ` [${r.memory.tags.join(", ")}]` : "";
|
|
61
|
-
|
|
82
|
+
const visibility = options.shared && r.memory.visibility ? ` [${r.memory.visibility}]` : "";
|
|
83
|
+
console.log(` [${(r.score * 100).toFixed(1)}%]${visibility} ${r.memory.content.slice(0, 80)}${tags}`);
|
|
62
84
|
}
|
|
63
85
|
console.log("");
|
|
64
86
|
}
|
|
@@ -212,4 +234,253 @@ brainCommand
|
|
|
212
234
|
process.exit(1);
|
|
213
235
|
}
|
|
214
236
|
});
|
|
237
|
+
// ============================================================================
|
|
238
|
+
// Phase 2: Cross-Agent Sharing
|
|
239
|
+
// ============================================================================
|
|
240
|
+
brainCommand
|
|
241
|
+
.command("publish <id>")
|
|
242
|
+
.description("Publish a memory for sharing")
|
|
243
|
+
.option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
|
|
244
|
+
.option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
|
|
245
|
+
.option("--visibility <level>", "Visibility level (team, public)", "team")
|
|
246
|
+
.action(async (memoryId, options) => {
|
|
247
|
+
try {
|
|
248
|
+
const brain = createBrain(options.agent, options.agentType);
|
|
249
|
+
const visibility = options.visibility;
|
|
250
|
+
if (visibility !== "team" && visibility !== "public") {
|
|
251
|
+
console.error("Error: Visibility must be 'team' or 'public'");
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
await brain.publish(memoryId, visibility);
|
|
255
|
+
console.log(` ✓ Memory published as ${visibility}`);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
console.error("Error:", error.message);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
brainCommand
|
|
263
|
+
.command("unpublish <id>")
|
|
264
|
+
.description("Unpublish a memory (set to private)")
|
|
265
|
+
.option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
|
|
266
|
+
.option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
|
|
267
|
+
.action(async (memoryId, options) => {
|
|
268
|
+
try {
|
|
269
|
+
const brain = createBrain(options.agent, options.agentType);
|
|
270
|
+
await brain.unpublish(memoryId);
|
|
271
|
+
console.log(` ✓ Memory unpublished (set to private)`);
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
console.error("Error:", error.message);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
brainCommand
|
|
279
|
+
.command("shared")
|
|
280
|
+
.description("List shared memories")
|
|
281
|
+
.option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
|
|
282
|
+
.option("-l, --limit <num>", "Max results", "20")
|
|
283
|
+
.option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
|
|
284
|
+
.option("--public-only", "Show only public memories")
|
|
285
|
+
.option("--json", "Output as JSON")
|
|
286
|
+
.action(async (options) => {
|
|
287
|
+
try {
|
|
288
|
+
const brain = createBrain(options.agent, options.agentType);
|
|
289
|
+
const memories = await brain.listShared(parseInt(options.limit, 10), options.publicOnly);
|
|
290
|
+
if (options.json) {
|
|
291
|
+
console.log(JSON.stringify({ success: true, memories }));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
console.log(`\n 🌐 Shared Memories (${memories.length})\n`);
|
|
295
|
+
if (memories.length === 0) {
|
|
296
|
+
console.log(" No shared memories found.");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
for (const m of memories) {
|
|
300
|
+
const visibility = m.visibility || 'private';
|
|
301
|
+
const endorsements = m.endorsements || 0;
|
|
302
|
+
console.log(` [${visibility}] ${m.content.slice(0, 70)}... (${endorsements} 👍)`);
|
|
303
|
+
}
|
|
304
|
+
console.log("");
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
console.error("Error:", error.message);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
// ============================================================================
|
|
312
|
+
// Phase 3: Quality & Decay
|
|
313
|
+
// ============================================================================
|
|
314
|
+
brainCommand
|
|
315
|
+
.command("boost <id>")
|
|
316
|
+
.description("Boost a memory (positive feedback)")
|
|
317
|
+
.option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
|
|
318
|
+
.option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
|
|
319
|
+
.action(async (memoryId, options) => {
|
|
320
|
+
try {
|
|
321
|
+
const brain = createBrain(options.agent, options.agentType);
|
|
322
|
+
await brain.boost(memoryId);
|
|
323
|
+
console.log(` ✓ Memory boosted: ${memoryId}`);
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
console.error("Error:", error.message);
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
brainCommand
|
|
331
|
+
.command("downvote <id>")
|
|
332
|
+
.description("Downvote a memory (negative feedback)")
|
|
333
|
+
.option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
|
|
334
|
+
.option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
|
|
335
|
+
.action(async (memoryId, options) => {
|
|
336
|
+
try {
|
|
337
|
+
const brain = createBrain(options.agent, options.agentType);
|
|
338
|
+
await brain.downvote(memoryId);
|
|
339
|
+
console.log(` ✓ Memory downvoted: ${memoryId}`);
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
console.error("Error:", error.message);
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
brainCommand
|
|
347
|
+
.command("quality <id>")
|
|
348
|
+
.description("Show quality metrics for a memory")
|
|
349
|
+
.option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
|
|
350
|
+
.option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
|
|
351
|
+
.option("--json", "Output as JSON")
|
|
352
|
+
.action(async (memoryId, options) => {
|
|
353
|
+
try {
|
|
354
|
+
const brain = createBrain(options.agent, options.agentType);
|
|
355
|
+
const quality = await brain.getQuality(memoryId);
|
|
356
|
+
if (options.json) {
|
|
357
|
+
console.log(JSON.stringify({ success: true, ...quality }));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
console.log(`\n 📊 Quality Metrics: ${memoryId}`);
|
|
361
|
+
console.log(` ────────────────────────────────`);
|
|
362
|
+
console.log(` Recall Count: ${quality.recallCount}`);
|
|
363
|
+
console.log(` Boost Count: ${quality.boostCount}`);
|
|
364
|
+
console.log(` Downvote Count: ${quality.downvoteCount}`);
|
|
365
|
+
console.log(` Quality Score: ${quality.qualityScore.toFixed(2)}`);
|
|
366
|
+
console.log(` Status: ${quality.status}`);
|
|
367
|
+
console.log("");
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
console.error("Error:", error.message);
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
brainCommand
|
|
375
|
+
.command("cleanup")
|
|
376
|
+
.description("Archive low-quality memories")
|
|
377
|
+
.option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
|
|
378
|
+
.option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
|
|
379
|
+
.option("--dry-run", "Show what would be archived without doing it", true)
|
|
380
|
+
.option("--execute", "Actually perform the cleanup (removes dry-run)")
|
|
381
|
+
.option("--threshold <score>", "Quality threshold for archiving", "0.1")
|
|
382
|
+
.option("--min-age-days <days>", "Minimum age in days", "90")
|
|
383
|
+
.option("-t, --tag <tags...>", "Filter by tags (for system migrations)")
|
|
384
|
+
.option("--json", "Output as JSON")
|
|
385
|
+
.action(async (options) => {
|
|
386
|
+
try {
|
|
387
|
+
const brain = createBrain(options.agent, options.agentType);
|
|
388
|
+
const dryRun = !options.execute;
|
|
389
|
+
const tags = options.tag;
|
|
390
|
+
const toArchive = await brain.cleanup(dryRun, parseFloat(options.threshold), parseInt(options.minAgeDays, 10), tags);
|
|
391
|
+
if (options.json) {
|
|
392
|
+
console.log(JSON.stringify({ success: true, dryRun, count: toArchive.length, memories: toArchive }));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
console.log(`\n 🧹 Cleanup ${dryRun ? '(DRY RUN)' : ''}`);
|
|
396
|
+
console.log(` ────────────────────────────────`);
|
|
397
|
+
console.log(` Memories to archive: ${toArchive.length}`);
|
|
398
|
+
if (toArchive.length > 0) {
|
|
399
|
+
console.log(`\n Memories:`);
|
|
400
|
+
for (const m of toArchive.slice(0, 10)) {
|
|
401
|
+
const age = Math.floor((Date.now() - m.createdAt.getTime()) / (1000 * 60 * 60 * 24));
|
|
402
|
+
console.log(` ${m.id.slice(0, 8)} │ ${m.content.slice(0, 50)}... (${age}d old, Q: ${m.qualityScore?.toFixed(2)})`);
|
|
403
|
+
}
|
|
404
|
+
if (toArchive.length > 10) {
|
|
405
|
+
console.log(` ... and ${toArchive.length - 10} more`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (dryRun && toArchive.length > 0) {
|
|
409
|
+
console.log(`\n 💡 Use --execute to actually perform the cleanup`);
|
|
410
|
+
}
|
|
411
|
+
console.log("");
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
console.error("Error:", error.message);
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
brainCommand
|
|
419
|
+
.command("purge")
|
|
420
|
+
.description("Permanently delete archived memories")
|
|
421
|
+
.option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
|
|
422
|
+
.option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
|
|
423
|
+
.option("--retention-days <days>", "Retention period in days", "365")
|
|
424
|
+
.option("--json", "Output as JSON")
|
|
425
|
+
.action(async (options) => {
|
|
426
|
+
try {
|
|
427
|
+
const brain = createBrain(options.agent, options.agentType);
|
|
428
|
+
const count = await brain.purge(parseInt(options.retentionDays, 10));
|
|
429
|
+
if (options.json) {
|
|
430
|
+
console.log(JSON.stringify({ success: true, deleted: count }));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
console.log(` ✓ Purged ${count} archived memory(ies)`);
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
console.error("Error:", error.message);
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
// ============================================================================
|
|
441
|
+
// Phase 5: SOP Generation
|
|
442
|
+
// ============================================================================
|
|
443
|
+
brainCommand
|
|
444
|
+
.command("generate-sop <topic>")
|
|
445
|
+
.description("Generate SOP from learnings")
|
|
446
|
+
.option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
|
|
447
|
+
.option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
|
|
448
|
+
.option("--min-memories <num>", "Minimum learnings required", "5")
|
|
449
|
+
.option("--json", "Output as JSON")
|
|
450
|
+
.option("-o, --output <file>", "Save SOP to file")
|
|
451
|
+
.action(async (topic, options) => {
|
|
452
|
+
try {
|
|
453
|
+
console.log(` Generating SOP for topic: ${topic}...`);
|
|
454
|
+
const sop = await generateSOP(options.agent, {
|
|
455
|
+
topic,
|
|
456
|
+
agentType: isValidAgentType(options.agentType) ? options.agentType : undefined,
|
|
457
|
+
minMemories: parseInt(options.minMemories, 10),
|
|
458
|
+
});
|
|
459
|
+
if (options.json) {
|
|
460
|
+
const output = JSON.stringify(sop, null, 2);
|
|
461
|
+
if (options.output) {
|
|
462
|
+
const fs = await import("fs");
|
|
463
|
+
fs.writeFileSync(options.output, output);
|
|
464
|
+
console.log(` ✓ SOP saved to ${options.output}`);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
console.log(output);
|
|
468
|
+
}
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const markdown = formatSOPAsMarkdown(sop);
|
|
472
|
+
if (options.output) {
|
|
473
|
+
const fs = await import("fs");
|
|
474
|
+
fs.writeFileSync(options.output, markdown);
|
|
475
|
+
console.log(` ✓ SOP saved to ${options.output}`);
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
console.log(markdown);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
catch (error) {
|
|
482
|
+
console.error("Error:", error.message);
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
215
486
|
export default brainCommand;
|
package/dist/commands/chat.js
CHANGED
|
@@ -377,6 +377,114 @@ chatCommand
|
|
|
377
377
|
process.exit(1);
|
|
378
378
|
}
|
|
379
379
|
});
|
|
380
|
+
chatCommand
|
|
381
|
+
.command("send-file <filePath>")
|
|
382
|
+
.description("Send a file attachment to Google Chat (images auto-compressed)")
|
|
383
|
+
.option("--space <name>", "Target space (e.g., spaces/ABC123)")
|
|
384
|
+
.option("--thread <name>", "Reply in thread")
|
|
385
|
+
.option("--text <message>", "Optional message text to accompany the file")
|
|
386
|
+
.option("--no-compress", "Skip image compression")
|
|
387
|
+
.action(async (filePath, options) => {
|
|
388
|
+
const config = getConfig();
|
|
389
|
+
const huskyApiUrl = getHuskyApiUrl();
|
|
390
|
+
if (!huskyApiUrl) {
|
|
391
|
+
console.error("Error: API URL not configured. Set husky-api-url or api-url.");
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
const fs = await import("fs");
|
|
395
|
+
const path = await import("path");
|
|
396
|
+
// Check if file exists
|
|
397
|
+
if (!fs.existsSync(filePath)) {
|
|
398
|
+
console.error(`Error: File not found: ${filePath}`);
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
const fileName = path.basename(filePath);
|
|
402
|
+
// Determine MIME type from extension
|
|
403
|
+
const ext = path.extname(filePath).toLowerCase().slice(1);
|
|
404
|
+
const mimeTypes = {
|
|
405
|
+
// Images
|
|
406
|
+
png: "image/png",
|
|
407
|
+
jpg: "image/jpeg",
|
|
408
|
+
jpeg: "image/jpeg",
|
|
409
|
+
gif: "image/gif",
|
|
410
|
+
webp: "image/webp",
|
|
411
|
+
svg: "image/svg+xml",
|
|
412
|
+
// Documents
|
|
413
|
+
pdf: "application/pdf",
|
|
414
|
+
txt: "text/plain",
|
|
415
|
+
md: "text/markdown",
|
|
416
|
+
// Data
|
|
417
|
+
json: "application/json",
|
|
418
|
+
xml: "application/xml",
|
|
419
|
+
csv: "text/csv",
|
|
420
|
+
yaml: "application/x-yaml",
|
|
421
|
+
yml: "application/x-yaml",
|
|
422
|
+
// Code
|
|
423
|
+
js: "text/javascript",
|
|
424
|
+
ts: "text/typescript",
|
|
425
|
+
py: "text/x-python",
|
|
426
|
+
html: "text/html",
|
|
427
|
+
css: "text/css",
|
|
428
|
+
sh: "text/x-sh",
|
|
429
|
+
sql: "text/x-sql",
|
|
430
|
+
};
|
|
431
|
+
const mimeType = mimeTypes[ext] || "application/octet-stream";
|
|
432
|
+
// Read file
|
|
433
|
+
let fileBuffer = fs.readFileSync(filePath);
|
|
434
|
+
const originalSize = fileBuffer.length;
|
|
435
|
+
console.log(`📤 Preparing ${fileName} (${(originalSize / 1024).toFixed(1)} KB, ${mimeType})...`);
|
|
436
|
+
// Compress images automatically (unless --no-compress flag is set)
|
|
437
|
+
const isImage = mimeType.startsWith("image/") && !mimeType.includes("svg");
|
|
438
|
+
if (isImage && options.compress !== false) {
|
|
439
|
+
try {
|
|
440
|
+
const sharp = await import("sharp");
|
|
441
|
+
console.log(`🔄 Compressing image...`);
|
|
442
|
+
const compressed = await sharp.default(fileBuffer)
|
|
443
|
+
.resize(1920, 1920, {
|
|
444
|
+
fit: "inside",
|
|
445
|
+
withoutEnlargement: true
|
|
446
|
+
})
|
|
447
|
+
.jpeg({ quality: 80 })
|
|
448
|
+
.toBuffer();
|
|
449
|
+
fileBuffer = Buffer.from(compressed);
|
|
450
|
+
const compressedSize = fileBuffer.length;
|
|
451
|
+
const savedPercent = ((1 - compressedSize / originalSize) * 100).toFixed(0);
|
|
452
|
+
console.log(`✓ Compressed: ${(originalSize / 1024).toFixed(1)} KB → ${(compressedSize / 1024).toFixed(1)} KB (saved ${savedPercent}%)`);
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
console.warn(`⚠️ Compression failed, uploading original: ${error.message}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const fileBase64 = fileBuffer.toString("base64");
|
|
459
|
+
console.log(`📤 Uploading ${fileName} (${(fileBuffer.length / 1024).toFixed(1)} KB)...`);
|
|
460
|
+
try {
|
|
461
|
+
const res = await fetch(`${huskyApiUrl}/api/google-chat/send-file`, {
|
|
462
|
+
method: "POST",
|
|
463
|
+
headers: {
|
|
464
|
+
"Content-Type": "application/json",
|
|
465
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
466
|
+
},
|
|
467
|
+
body: JSON.stringify({
|
|
468
|
+
fileBase64,
|
|
469
|
+
fileName,
|
|
470
|
+
mimeType,
|
|
471
|
+
text: options.text,
|
|
472
|
+
spaceName: options.space,
|
|
473
|
+
threadName: options.thread,
|
|
474
|
+
}),
|
|
475
|
+
});
|
|
476
|
+
if (!res.ok) {
|
|
477
|
+
const error = await res.text();
|
|
478
|
+
throw new Error(`API error: ${res.status} - ${error}`);
|
|
479
|
+
}
|
|
480
|
+
const data = await res.json();
|
|
481
|
+
console.log(`✅ File sent to Google Chat: ${data.fileName}`);
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
console.error("Error sending file:", error);
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
380
488
|
chatCommand
|
|
381
489
|
.command("reply-to <messageId> <response>")
|
|
382
490
|
.description("Reply to a specific inbox message in its thread (supports both GitHub and Google Chat)")
|
|
@@ -446,7 +554,22 @@ chatCommand
|
|
|
446
554
|
method: "POST",
|
|
447
555
|
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
448
556
|
});
|
|
449
|
-
|
|
557
|
+
// Add reaction to original message if messageName is available
|
|
558
|
+
if (msg.messageName) {
|
|
559
|
+
try {
|
|
560
|
+
await fetch(`${huskyApiUrl}/api/google-chat/messages/${encodeURIComponent(msg.messageName)}/react`, {
|
|
561
|
+
method: "POST",
|
|
562
|
+
headers: {
|
|
563
|
+
"Content-Type": "application/json",
|
|
564
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
565
|
+
},
|
|
566
|
+
body: JSON.stringify({ emoji: "✅" }),
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
// Reaction is optional - don't fail if it doesn't work
|
|
571
|
+
}
|
|
572
|
+
}
|
|
450
573
|
}
|
|
451
574
|
}
|
|
452
575
|
catch (error) {
|
|
@@ -24,6 +24,10 @@ interface Config {
|
|
|
24
24
|
gotessToken?: string;
|
|
25
25
|
gotessBookId?: string;
|
|
26
26
|
agentType?: string;
|
|
27
|
+
geminiApiKey?: string;
|
|
28
|
+
nocodbApiToken?: string;
|
|
29
|
+
nocodbBaseUrl?: string;
|
|
30
|
+
nocodbWorkspaceId?: string;
|
|
27
31
|
}
|
|
28
32
|
export declare function getConfig(): Config;
|
|
29
33
|
/**
|
package/dist/commands/config.js
CHANGED
|
@@ -159,6 +159,12 @@ configCommand
|
|
|
159
159
|
"gotess-token": "gotessToken",
|
|
160
160
|
"gotess-book-id": "gotessBookId",
|
|
161
161
|
"agent-type": "agentType",
|
|
162
|
+
// Gemini
|
|
163
|
+
"gemini-api-key": "geminiApiKey",
|
|
164
|
+
// NocoDB
|
|
165
|
+
"nocodb-api-token": "nocodbApiToken",
|
|
166
|
+
"nocodb-base-url": "nocodbBaseUrl",
|
|
167
|
+
"nocodb-workspace-id": "nocodbWorkspaceId",
|
|
162
168
|
};
|
|
163
169
|
const configKey = keyMappings[key];
|
|
164
170
|
if (!configKey) {
|
|
@@ -171,6 +177,8 @@ configCommand
|
|
|
171
177
|
console.log(" Qdrant: qdrant-url, qdrant-api-key");
|
|
172
178
|
console.log(" GCP: gcp-project-id, gcp-location");
|
|
173
179
|
console.log(" Gotess: gotess-token, gotess-book-id");
|
|
180
|
+
console.log(" Gemini: gemini-api-key");
|
|
181
|
+
console.log(" NocoDB: nocodb-api-token, nocodb-base-url, nocodb-workspace-id");
|
|
174
182
|
console.log(" Brain: agent-type");
|
|
175
183
|
process.exit(1);
|
|
176
184
|
}
|
|
@@ -193,7 +201,7 @@ configCommand
|
|
|
193
201
|
config[configKey] = value;
|
|
194
202
|
saveConfig(config);
|
|
195
203
|
// Mask sensitive values in output
|
|
196
|
-
const sensitiveKeys = ["api-key", "billbee-api-key", "billbee-password", "zendesk-api-token", "seatable-api-token", "gotess-token"];
|
|
204
|
+
const sensitiveKeys = ["api-key", "billbee-api-key", "billbee-password", "zendesk-api-token", "seatable-api-token", "gotess-token", "gemini-api-key", "nocodb-api-token"];
|
|
197
205
|
const displayValue = sensitiveKeys.includes(key) ? "***" : value;
|
|
198
206
|
console.log(`✓ Set ${key} = ${displayValue}`);
|
|
199
207
|
});
|