@simonfestl/husky-cli 1.3.0 → 1.4.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.
@@ -459,21 +459,51 @@ ticketsCommand
459
459
  console.log(` Searching ${options.collection}...`);
460
460
  const results = await qdrant.search(options.collection, vector, parseInt(options.limit, 10) + 1 // +1 to exclude self
461
461
  );
462
+ // Extract ticket_id from payload (handle nested metadata.ticket_id)
463
+ const extractTicketId = (payload) => {
464
+ if (!payload)
465
+ return null;
466
+ if (typeof payload.ticket_id === 'number')
467
+ return payload.ticket_id;
468
+ if (typeof payload.id === 'number')
469
+ return payload.id;
470
+ const metadata = payload.metadata;
471
+ if (metadata && typeof metadata.ticket_id === 'number')
472
+ return metadata.ticket_id;
473
+ return null;
474
+ };
462
475
  // Filter out the same ticket
463
476
  const filteredResults = results.filter(r => {
464
- const payload = r.payload;
465
- return payload?.ticket_id !== ticket.id && payload?.id !== ticket.id;
477
+ const ticketId = extractTicketId(r.payload);
478
+ return ticketId !== ticket.id;
466
479
  }).slice(0, parseInt(options.limit, 10));
480
+ // Fetch ticket details from Zendesk for each result
481
+ const ticketIds = filteredResults
482
+ .map(r => extractTicketId(r.payload))
483
+ .filter((id) => id !== null);
484
+ const ticketDetailsMap = new Map();
485
+ if (ticketIds.length > 0) {
486
+ console.log(` Fetching ${ticketIds.length} ticket details...`);
487
+ const ticketDetails = await Promise.all(ticketIds.map(id => zendesk.getTicket(id).catch(() => null)));
488
+ ticketDetails.forEach((t, i) => {
489
+ if (t)
490
+ ticketDetailsMap.set(ticketIds[i], { subject: t.subject, status: t.status });
491
+ });
492
+ }
467
493
  if (options.json) {
468
494
  console.log(JSON.stringify({
469
495
  success: true,
470
496
  source_ticket: { id: ticket.id, subject: ticket.subject },
471
- similar: filteredResults.map(r => ({
472
- id: r.payload?.ticket_id || r.id,
473
- score: r.score,
474
- subject: r.payload?.subject,
475
- status: r.payload?.status,
476
- })),
497
+ similar: filteredResults.map(r => {
498
+ const ticketId = extractTicketId(r.payload);
499
+ const details = ticketId ? ticketDetailsMap.get(ticketId) : undefined;
500
+ return {
501
+ id: ticketId,
502
+ score: r.score,
503
+ subject: details?.subject,
504
+ status: details?.status,
505
+ };
506
+ }),
477
507
  }, null, 2));
478
508
  return;
479
509
  }
@@ -483,10 +513,10 @@ ticketsCommand
483
513
  return;
484
514
  }
485
515
  for (const result of filteredResults) {
486
- const payload = result.payload;
487
- const ticketId = payload?.ticket_id || payload?.id || result.id;
488
- const subject = payload?.subject || '(no subject)';
489
- const status = payload?.status || '';
516
+ const ticketId = extractTicketId(result.payload);
517
+ const details = ticketId ? ticketDetailsMap.get(ticketId) : undefined;
518
+ const subject = details?.subject || '(no subject)';
519
+ const status = details?.status || '';
490
520
  console.log(` [${(result.score * 100).toFixed(1)}%] #${ticketId} │ ${status} │ ${String(subject).slice(0, 45)}`);
491
521
  }
492
522
  console.log("");
@@ -505,24 +535,51 @@ ticketsCommand
505
535
  .option("--json", "Output as JSON")
506
536
  .action(async (query, options) => {
507
537
  try {
538
+ const zendesk = ZendeskClient.fromConfig();
508
539
  const embeddings = EmbeddingService.fromConfig();
509
540
  const qdrant = QdrantClient.fromConfig();
510
- // 1. Create embedding from query
511
541
  console.log(` Generating embedding for query...`);
512
542
  const vector = await embeddings.embed(query);
513
- // 2. Search Qdrant
514
543
  console.log(` Searching ${options.collection}...`);
515
544
  const results = await qdrant.search(options.collection, vector, parseInt(options.limit, 10));
545
+ const extractTicketId = (payload) => {
546
+ if (!payload)
547
+ return null;
548
+ if (typeof payload.ticket_id === 'number')
549
+ return payload.ticket_id;
550
+ if (typeof payload.id === 'number')
551
+ return payload.id;
552
+ const metadata = payload.metadata;
553
+ if (metadata && typeof metadata.ticket_id === 'number')
554
+ return metadata.ticket_id;
555
+ return null;
556
+ };
557
+ const ticketIds = results
558
+ .map(r => extractTicketId(r.payload))
559
+ .filter((id) => id !== null);
560
+ const ticketDetailsMap = new Map();
561
+ if (ticketIds.length > 0) {
562
+ console.log(` Fetching ${ticketIds.length} ticket details...`);
563
+ const ticketDetails = await Promise.all(ticketIds.map(id => zendesk.getTicket(id).catch(() => null)));
564
+ ticketDetails.forEach((t, i) => {
565
+ if (t)
566
+ ticketDetailsMap.set(ticketIds[i], { subject: t.subject, status: t.status });
567
+ });
568
+ }
516
569
  if (options.json) {
517
570
  console.log(JSON.stringify({
518
571
  success: true,
519
572
  query,
520
- results: results.map(r => ({
521
- id: r.payload?.ticket_id || r.id,
522
- score: r.score,
523
- subject: r.payload?.subject,
524
- status: r.payload?.status,
525
- })),
573
+ results: results.map(r => {
574
+ const ticketId = extractTicketId(r.payload);
575
+ const details = ticketId ? ticketDetailsMap.get(ticketId) : undefined;
576
+ return {
577
+ id: ticketId,
578
+ score: r.score,
579
+ subject: details?.subject,
580
+ status: details?.status,
581
+ };
582
+ }),
526
583
  }, null, 2));
527
584
  return;
528
585
  }
@@ -532,10 +589,10 @@ ticketsCommand
532
589
  return;
533
590
  }
534
591
  for (const result of results) {
535
- const payload = result.payload;
536
- const ticketId = payload?.ticket_id || payload?.id || result.id;
537
- const subject = payload?.subject || '(no subject)';
538
- const status = payload?.status || '';
592
+ const ticketId = extractTicketId(result.payload);
593
+ const details = ticketId ? ticketDetailsMap.get(ticketId) : undefined;
594
+ const subject = details?.subject || '(no subject)';
595
+ const status = details?.status || '';
539
596
  console.log(` [${(result.score * 100).toFixed(1)}%] #${ticketId} │ ${status} │ ${String(subject).slice(0, 45)}`);
540
597
  }
541
598
  console.log("");
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare const brainCommand: Command;
3
+ export default brainCommand;
@@ -0,0 +1,168 @@
1
+ import { Command } from "commander";
2
+ import { AgentBrain } from "../lib/biz/agent-brain.js";
3
+ const DEFAULT_AGENT = process.env.HUSKY_AGENT_ID || 'default';
4
+ export const brainCommand = new Command("brain")
5
+ .description("Agent memory and knowledge management");
6
+ brainCommand
7
+ .command("remember <content>")
8
+ .description("Store a memory")
9
+ .option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
10
+ .option("-t, --tags <tags>", "Comma-separated tags")
11
+ .option("--json", "Output as JSON")
12
+ .action(async (content, options) => {
13
+ try {
14
+ const brain = new AgentBrain(options.agent);
15
+ const tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : [];
16
+ console.log(` Storing memory for agent: ${options.agent}...`);
17
+ const id = await brain.remember(content, tags);
18
+ if (options.json) {
19
+ console.log(JSON.stringify({ success: true, id, agent: options.agent }));
20
+ }
21
+ else {
22
+ console.log(` ✓ Memory stored: ${id}`);
23
+ }
24
+ }
25
+ catch (error) {
26
+ console.error("Error:", error.message);
27
+ process.exit(1);
28
+ }
29
+ });
30
+ brainCommand
31
+ .command("recall <query>")
32
+ .description("Search memories semantically")
33
+ .option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
34
+ .option("-l, --limit <num>", "Max results", "5")
35
+ .option("-m, --min-score <score>", "Minimum similarity score (0-1)", "0.5")
36
+ .option("--json", "Output as JSON")
37
+ .action(async (query, options) => {
38
+ try {
39
+ const brain = new AgentBrain(options.agent);
40
+ console.log(` Searching memories for: "${query}"...`);
41
+ const results = await brain.recall(query, parseInt(options.limit, 10), parseFloat(options.minScore));
42
+ if (options.json) {
43
+ console.log(JSON.stringify({ success: true, query, results }));
44
+ return;
45
+ }
46
+ console.log(`\n 🧠 Memories for "${query}" (${results.length} found)\n`);
47
+ if (results.length === 0) {
48
+ console.log(" No relevant memories found.");
49
+ return;
50
+ }
51
+ for (const r of results) {
52
+ const tags = r.memory.tags.length > 0 ? ` [${r.memory.tags.join(", ")}]` : "";
53
+ console.log(` [${(r.score * 100).toFixed(1)}%] ${r.memory.content.slice(0, 80)}${tags}`);
54
+ }
55
+ console.log("");
56
+ }
57
+ catch (error) {
58
+ console.error("Error:", error.message);
59
+ process.exit(1);
60
+ }
61
+ });
62
+ brainCommand
63
+ .command("list")
64
+ .description("List recent memories")
65
+ .option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
66
+ .option("-l, --limit <num>", "Max results", "20")
67
+ .option("--json", "Output as JSON")
68
+ .action(async (options) => {
69
+ try {
70
+ const brain = new AgentBrain(options.agent);
71
+ const memories = await brain.listMemories(parseInt(options.limit, 10));
72
+ if (options.json) {
73
+ console.log(JSON.stringify({ success: true, memories }));
74
+ return;
75
+ }
76
+ console.log(`\n 🧠 Memories for agent: ${options.agent} (${memories.length})\n`);
77
+ if (memories.length === 0) {
78
+ console.log(" No memories stored yet.");
79
+ return;
80
+ }
81
+ for (const m of memories) {
82
+ const date = m.createdAt.toLocaleDateString("de-DE");
83
+ const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
84
+ console.log(` ${date} │ ${m.content.slice(0, 60)}...${tags}`);
85
+ }
86
+ console.log("");
87
+ }
88
+ catch (error) {
89
+ console.error("Error:", error.message);
90
+ process.exit(1);
91
+ }
92
+ });
93
+ brainCommand
94
+ .command("forget <id>")
95
+ .description("Delete a memory")
96
+ .option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
97
+ .action(async (memoryId, options) => {
98
+ try {
99
+ const brain = new AgentBrain(options.agent);
100
+ await brain.forget(memoryId);
101
+ console.log(` ✓ Memory deleted: ${memoryId}`);
102
+ }
103
+ catch (error) {
104
+ console.error("Error:", error.message);
105
+ process.exit(1);
106
+ }
107
+ });
108
+ brainCommand
109
+ .command("stats")
110
+ .description("Show memory statistics")
111
+ .option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
112
+ .option("--json", "Output as JSON")
113
+ .action(async (options) => {
114
+ try {
115
+ const brain = new AgentBrain(options.agent);
116
+ const stats = await brain.stats();
117
+ if (options.json) {
118
+ console.log(JSON.stringify({ success: true, agent: options.agent, ...stats }));
119
+ return;
120
+ }
121
+ console.log(`\n 🧠 Brain Stats for: ${options.agent}`);
122
+ console.log(` ────────────────────────────────`);
123
+ console.log(` Total memories: ${stats.count}`);
124
+ if (Object.keys(stats.tags).length > 0) {
125
+ console.log(`\n Tags:`);
126
+ const sortedTags = Object.entries(stats.tags).sort((a, b) => b[1] - a[1]);
127
+ for (const [tag, count] of sortedTags.slice(0, 10)) {
128
+ console.log(` ${tag}: ${count}`);
129
+ }
130
+ }
131
+ console.log("");
132
+ }
133
+ catch (error) {
134
+ console.error("Error:", error.message);
135
+ process.exit(1);
136
+ }
137
+ });
138
+ brainCommand
139
+ .command("tags <tags>")
140
+ .description("Find memories by tags")
141
+ .option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
142
+ .option("-l, --limit <num>", "Max results", "10")
143
+ .option("--json", "Output as JSON")
144
+ .action(async (tags, options) => {
145
+ try {
146
+ const brain = new AgentBrain(options.agent);
147
+ const tagList = tags.split(",").map((t) => t.trim());
148
+ const memories = await brain.recallByTags(tagList, parseInt(options.limit, 10));
149
+ if (options.json) {
150
+ console.log(JSON.stringify({ success: true, tags: tagList, memories }));
151
+ return;
152
+ }
153
+ console.log(`\n 🏷️ Memories with tags: ${tagList.join(", ")} (${memories.length})\n`);
154
+ if (memories.length === 0) {
155
+ console.log(" No memories found with these tags.");
156
+ return;
157
+ }
158
+ for (const m of memories) {
159
+ console.log(` ${m.content.slice(0, 70)}...`);
160
+ }
161
+ console.log("");
162
+ }
163
+ catch (error) {
164
+ console.error("Error:", error.message);
165
+ process.exit(1);
166
+ }
167
+ });
168
+ export default brainCommand;
package/dist/index.js CHANGED
@@ -27,6 +27,7 @@ import { serviceAccountCommand } from "./commands/service-account.js";
27
27
  import { chatCommand } from "./commands/chat.js";
28
28
  import { previewCommand } from "./commands/preview.js";
29
29
  import { initCommand } from "./commands/init.js";
30
+ import { brainCommand } from "./commands/brain.js";
30
31
  // Read version from package.json
31
32
  const require = createRequire(import.meta.url);
32
33
  const packageJson = require("../package.json");
@@ -61,6 +62,7 @@ program.addCommand(previewCommand);
61
62
  program.addCommand(llmCommand);
62
63
  program.addCommand(initCommand);
63
64
  program.addCommand(agentMsgCommand);
65
+ program.addCommand(brainCommand);
64
66
  // Handle --llm flag specially
65
67
  if (process.argv.includes("--llm")) {
66
68
  printLLMContext();
@@ -0,0 +1,32 @@
1
+ export interface Memory {
2
+ id: string;
3
+ agent: string;
4
+ content: string;
5
+ tags: string[];
6
+ embedding?: number[];
7
+ createdAt: Date;
8
+ updatedAt: Date;
9
+ metadata?: Record<string, unknown>;
10
+ }
11
+ export interface RecallResult {
12
+ memory: Memory;
13
+ score: number;
14
+ }
15
+ export declare class AgentBrain {
16
+ private db;
17
+ private embeddings;
18
+ private agentId;
19
+ private collectionPath;
20
+ constructor(agentId: string, projectId?: string);
21
+ remember(content: string, tags?: string[], metadata?: Record<string, unknown>): Promise<string>;
22
+ recall(query: string, limit?: number, minScore?: number): Promise<RecallResult[]>;
23
+ recallByTags(tags: string[], limit?: number): Promise<Memory[]>;
24
+ forget(memoryId: string): Promise<void>;
25
+ listMemories(limit?: number): Promise<Memory[]>;
26
+ stats(): Promise<{
27
+ count: number;
28
+ tags: Record<string, number>;
29
+ }>;
30
+ private cosineSimilarity;
31
+ }
32
+ export default AgentBrain;
@@ -0,0 +1,132 @@
1
+ import { initializeApp, getApps } from 'firebase-admin/app';
2
+ import { getFirestore, Timestamp } from 'firebase-admin/firestore';
3
+ import { EmbeddingService } from './embeddings.js';
4
+ import { getConfig } from '../../commands/config.js';
5
+ const BRAIN_COLLECTION = 'agent_brains';
6
+ export class AgentBrain {
7
+ db;
8
+ embeddings;
9
+ agentId;
10
+ collectionPath;
11
+ constructor(agentId, projectId) {
12
+ this.agentId = agentId;
13
+ const config = getConfig();
14
+ const gcpProject = projectId || config.gcpProjectId || process.env.GOOGLE_CLOUD_PROJECT || 'tigerv0';
15
+ if (getApps().length === 0) {
16
+ initializeApp({ projectId: gcpProject });
17
+ }
18
+ this.db = getFirestore();
19
+ this.embeddings = new EmbeddingService({
20
+ projectId: gcpProject,
21
+ location: config.gcpLocation || 'europe-west1'
22
+ });
23
+ this.collectionPath = `${BRAIN_COLLECTION}/${agentId}/memories`;
24
+ }
25
+ async remember(content, tags = [], metadata) {
26
+ const embedding = await this.embeddings.embed(content);
27
+ const memory = {
28
+ agent: this.agentId,
29
+ content,
30
+ tags,
31
+ embedding,
32
+ metadata: metadata || {},
33
+ createdAt: Timestamp.now(),
34
+ updatedAt: Timestamp.now(),
35
+ };
36
+ const ref = await this.db.collection(this.collectionPath).add(memory);
37
+ return ref.id;
38
+ }
39
+ async recall(query, limit = 5, minScore = 0.5) {
40
+ const queryEmbedding = await this.embeddings.embed(query);
41
+ const snapshot = await this.db.collection(this.collectionPath).get();
42
+ const results = [];
43
+ for (const doc of snapshot.docs) {
44
+ const data = doc.data();
45
+ if (!data.embedding)
46
+ continue;
47
+ const score = this.cosineSimilarity(queryEmbedding, data.embedding);
48
+ if (score >= minScore) {
49
+ results.push({
50
+ memory: {
51
+ id: doc.id,
52
+ agent: data.agent,
53
+ content: data.content,
54
+ tags: data.tags || [],
55
+ createdAt: data.createdAt?.toDate() || new Date(),
56
+ updatedAt: data.updatedAt?.toDate() || new Date(),
57
+ metadata: data.metadata,
58
+ },
59
+ score,
60
+ });
61
+ }
62
+ }
63
+ return results
64
+ .sort((a, b) => b.score - a.score)
65
+ .slice(0, limit);
66
+ }
67
+ async recallByTags(tags, limit = 10) {
68
+ const snapshot = await this.db
69
+ .collection(this.collectionPath)
70
+ .where('tags', 'array-contains-any', tags)
71
+ .orderBy('createdAt', 'desc')
72
+ .limit(limit)
73
+ .get();
74
+ return snapshot.docs.map(doc => ({
75
+ id: doc.id,
76
+ agent: doc.data().agent,
77
+ content: doc.data().content,
78
+ tags: doc.data().tags || [],
79
+ createdAt: doc.data().createdAt?.toDate() || new Date(),
80
+ updatedAt: doc.data().updatedAt?.toDate() || new Date(),
81
+ metadata: doc.data().metadata,
82
+ }));
83
+ }
84
+ async forget(memoryId) {
85
+ await this.db.collection(this.collectionPath).doc(memoryId).delete();
86
+ }
87
+ async listMemories(limit = 20) {
88
+ const snapshot = await this.db
89
+ .collection(this.collectionPath)
90
+ .orderBy('createdAt', 'desc')
91
+ .limit(limit)
92
+ .get();
93
+ return snapshot.docs.map(doc => ({
94
+ id: doc.id,
95
+ agent: doc.data().agent,
96
+ content: doc.data().content,
97
+ tags: doc.data().tags || [],
98
+ createdAt: doc.data().createdAt?.toDate() || new Date(),
99
+ updatedAt: doc.data().updatedAt?.toDate() || new Date(),
100
+ metadata: doc.data().metadata,
101
+ }));
102
+ }
103
+ async stats() {
104
+ const snapshot = await this.db.collection(this.collectionPath).get();
105
+ const tagCounts = {};
106
+ for (const doc of snapshot.docs) {
107
+ const tags = doc.data().tags || [];
108
+ for (const tag of tags) {
109
+ tagCounts[tag] = (tagCounts[tag] || 0) + 1;
110
+ }
111
+ }
112
+ return {
113
+ count: snapshot.size,
114
+ tags: tagCounts,
115
+ };
116
+ }
117
+ cosineSimilarity(a, b) {
118
+ if (a.length !== b.length)
119
+ return 0;
120
+ let dotProduct = 0;
121
+ let normA = 0;
122
+ let normB = 0;
123
+ for (let i = 0; i < a.length; i++) {
124
+ dotProduct += a[i] * b[i];
125
+ normA += a[i] * a[i];
126
+ normB += b[i] * b[i];
127
+ }
128
+ const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
129
+ return magnitude === 0 ? 0 : dotProduct / magnitude;
130
+ }
131
+ }
132
+ export default AgentBrain;
@@ -27,7 +27,7 @@ export class EmbeddingService {
27
27
  static fromConfig() {
28
28
  const config = getConfig();
29
29
  const embeddingConfig = {
30
- projectId: config.gcpProjectId || process.env.GOOGLE_PROJECT_ID || process.env.GCP_PROJECT_ID || '',
30
+ projectId: config.gcpProjectId || process.env.GOOGLE_CLOUD_PROJECT || process.env.GOOGLE_PROJECT_ID || process.env.GCP_PROJECT_ID || '',
31
31
  location: config.gcpLocation || process.env.GOOGLE_LOCATION || 'europe-west1',
32
32
  model: process.env.EMBEDDING_MODEL || EMBEDDING_MODELS.TEXT_EMBEDDING_004,
33
33
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonfestl/husky-cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "CLI for Huskyv0 Task Orchestration with Claude Agent SDK",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,15 +21,16 @@
21
21
  "dependencies": {
22
22
  "@anthropic-ai/claude-code": "^1.0.0",
23
23
  "@inquirer/prompts": "^8.1.0",
24
- "commander": "^12.1.0"
24
+ "commander": "^12.1.0",
25
+ "firebase-admin": "^13.6.0"
25
26
  },
26
27
  "devDependencies": {
27
28
  "@types/node": "^22",
28
- "typescript": "^5",
29
- "vitest": "^2.1.8",
30
29
  "@vitest/coverage-v8": "^2.1.8",
30
+ "memfs": "^4.14.0",
31
31
  "msw": "^2.6.8",
32
- "memfs": "^4.14.0"
32
+ "typescript": "^5",
33
+ "vitest": "^2.1.8"
33
34
  },
34
35
  "files": [
35
36
  "dist"
@@ -43,4 +44,4 @@
43
44
  "bugs": {
44
45
  "url": "https://github.com/simon-sfxecom/huskyv0/issues"
45
46
  }
46
- }
47
+ }