@simonfestl/husky-cli 1.5.0 → 1.6.1
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/brain.js +64 -17
- package/dist/commands/chat.js +222 -0
- package/dist/commands/config.d.ts +1 -0
- package/dist/commands/config.js +9 -0
- package/dist/commands/vm.js +335 -0
- package/dist/commands/worker.js +25 -1
- package/dist/lib/biz/agent-brain.d.ts +17 -1
- package/dist/lib/biz/agent-brain.js +70 -6
- package/package.json +1 -1
package/dist/commands/brain.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import { AgentBrain } from "../lib/biz/agent-brain.js";
|
|
2
|
+
import { AgentBrain, AGENT_TYPES, isValidAgentType } from "../lib/biz/agent-brain.js";
|
|
3
3
|
const DEFAULT_AGENT = process.env.HUSKY_AGENT_ID || 'default';
|
|
4
|
+
function createBrain(agentId, agentType) {
|
|
5
|
+
const validAgentType = isValidAgentType(agentType) ? agentType : undefined;
|
|
6
|
+
return new AgentBrain({ agentId, agentType: validAgentType });
|
|
7
|
+
}
|
|
4
8
|
export const brainCommand = new Command("brain")
|
|
5
9
|
.description("Agent memory and knowledge management");
|
|
6
10
|
brainCommand
|
|
@@ -8,15 +12,17 @@ brainCommand
|
|
|
8
12
|
.description("Store a memory")
|
|
9
13
|
.option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
|
|
10
14
|
.option("-t, --tags <tags>", "Comma-separated tags")
|
|
15
|
+
.option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
|
|
11
16
|
.option("--json", "Output as JSON")
|
|
12
17
|
.action(async (content, options) => {
|
|
13
18
|
try {
|
|
14
|
-
const brain =
|
|
19
|
+
const brain = createBrain(options.agent, options.agentType);
|
|
15
20
|
const tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : [];
|
|
16
|
-
|
|
21
|
+
const dbInfo = brain.getDatabaseInfo();
|
|
22
|
+
console.log(` Storing memory for agent: ${options.agent} (db: ${dbInfo.databaseName})...`);
|
|
17
23
|
const id = await brain.remember(content, tags);
|
|
18
24
|
if (options.json) {
|
|
19
|
-
console.log(JSON.stringify({ success: true, id, agent: options.agent }));
|
|
25
|
+
console.log(JSON.stringify({ success: true, id, agent: options.agent, database: dbInfo.databaseName }));
|
|
20
26
|
}
|
|
21
27
|
else {
|
|
22
28
|
console.log(` ✓ Memory stored: ${id}`);
|
|
@@ -33,14 +39,16 @@ brainCommand
|
|
|
33
39
|
.option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
|
|
34
40
|
.option("-l, --limit <num>", "Max results", "5")
|
|
35
41
|
.option("-m, --min-score <score>", "Minimum similarity score (0-1)", "0.5")
|
|
42
|
+
.option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
|
|
36
43
|
.option("--json", "Output as JSON")
|
|
37
44
|
.action(async (query, options) => {
|
|
38
45
|
try {
|
|
39
|
-
const brain =
|
|
40
|
-
|
|
46
|
+
const brain = createBrain(options.agent, options.agentType);
|
|
47
|
+
const dbInfo = brain.getDatabaseInfo();
|
|
48
|
+
console.log(` Searching memories for: "${query}" (db: ${dbInfo.databaseName})...`);
|
|
41
49
|
const results = await brain.recall(query, parseInt(options.limit, 10), parseFloat(options.minScore));
|
|
42
50
|
if (options.json) {
|
|
43
|
-
console.log(JSON.stringify({ success: true, query, results }));
|
|
51
|
+
console.log(JSON.stringify({ success: true, query, database: dbInfo.databaseName, results }));
|
|
44
52
|
return;
|
|
45
53
|
}
|
|
46
54
|
console.log(`\n 🧠 Memories for "${query}" (${results.length} found)\n`);
|
|
@@ -64,16 +72,18 @@ brainCommand
|
|
|
64
72
|
.description("List recent memories")
|
|
65
73
|
.option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
|
|
66
74
|
.option("-l, --limit <num>", "Max results", "20")
|
|
75
|
+
.option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
|
|
67
76
|
.option("--json", "Output as JSON")
|
|
68
77
|
.action(async (options) => {
|
|
69
78
|
try {
|
|
70
|
-
const brain =
|
|
79
|
+
const brain = createBrain(options.agent, options.agentType);
|
|
80
|
+
const dbInfo = brain.getDatabaseInfo();
|
|
71
81
|
const memories = await brain.listMemories(parseInt(options.limit, 10));
|
|
72
82
|
if (options.json) {
|
|
73
|
-
console.log(JSON.stringify({ success: true, memories }));
|
|
83
|
+
console.log(JSON.stringify({ success: true, database: dbInfo.databaseName, memories }));
|
|
74
84
|
return;
|
|
75
85
|
}
|
|
76
|
-
console.log(`\n 🧠 Memories for agent: ${options.agent} (${memories.length})\n`);
|
|
86
|
+
console.log(`\n 🧠 Memories for agent: ${options.agent} (db: ${dbInfo.databaseName}) (${memories.length})\n`);
|
|
77
87
|
if (memories.length === 0) {
|
|
78
88
|
console.log(" No memories stored yet.");
|
|
79
89
|
return;
|
|
@@ -94,9 +104,10 @@ brainCommand
|
|
|
94
104
|
.command("forget <id>")
|
|
95
105
|
.description("Delete a memory")
|
|
96
106
|
.option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
|
|
107
|
+
.option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
|
|
97
108
|
.action(async (memoryId, options) => {
|
|
98
109
|
try {
|
|
99
|
-
const brain =
|
|
110
|
+
const brain = createBrain(options.agent, options.agentType);
|
|
100
111
|
await brain.forget(memoryId);
|
|
101
112
|
console.log(` ✓ Memory deleted: ${memoryId}`);
|
|
102
113
|
}
|
|
@@ -109,16 +120,18 @@ brainCommand
|
|
|
109
120
|
.command("stats")
|
|
110
121
|
.description("Show memory statistics")
|
|
111
122
|
.option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
|
|
123
|
+
.option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
|
|
112
124
|
.option("--json", "Output as JSON")
|
|
113
125
|
.action(async (options) => {
|
|
114
126
|
try {
|
|
115
|
-
const brain =
|
|
127
|
+
const brain = createBrain(options.agent, options.agentType);
|
|
128
|
+
const dbInfo = brain.getDatabaseInfo();
|
|
116
129
|
const stats = await brain.stats();
|
|
117
130
|
if (options.json) {
|
|
118
|
-
console.log(JSON.stringify({ success: true, agent: options.agent, ...stats }));
|
|
131
|
+
console.log(JSON.stringify({ success: true, agent: options.agent, database: dbInfo.databaseName, ...stats }));
|
|
119
132
|
return;
|
|
120
133
|
}
|
|
121
|
-
console.log(`\n 🧠 Brain Stats for: ${options.agent}`);
|
|
134
|
+
console.log(`\n 🧠 Brain Stats for: ${options.agent} (db: ${dbInfo.databaseName})`);
|
|
122
135
|
console.log(` ────────────────────────────────`);
|
|
123
136
|
console.log(` Total memories: ${stats.count}`);
|
|
124
137
|
if (Object.keys(stats.tags).length > 0) {
|
|
@@ -140,17 +153,19 @@ brainCommand
|
|
|
140
153
|
.description("Find memories by tags")
|
|
141
154
|
.option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
|
|
142
155
|
.option("-l, --limit <num>", "Max results", "10")
|
|
156
|
+
.option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
|
|
143
157
|
.option("--json", "Output as JSON")
|
|
144
158
|
.action(async (tags, options) => {
|
|
145
159
|
try {
|
|
146
|
-
const brain =
|
|
160
|
+
const brain = createBrain(options.agent, options.agentType);
|
|
161
|
+
const dbInfo = brain.getDatabaseInfo();
|
|
147
162
|
const tagList = tags.split(",").map((t) => t.trim());
|
|
148
163
|
const memories = await brain.recallByTags(tagList, parseInt(options.limit, 10));
|
|
149
164
|
if (options.json) {
|
|
150
|
-
console.log(JSON.stringify({ success: true, tags: tagList, memories }));
|
|
165
|
+
console.log(JSON.stringify({ success: true, tags: tagList, database: dbInfo.databaseName, memories }));
|
|
151
166
|
return;
|
|
152
167
|
}
|
|
153
|
-
console.log(`\n 🏷️ Memories with tags: ${tagList.join(", ")} (${memories.length})\n`);
|
|
168
|
+
console.log(`\n 🏷️ Memories with tags: ${tagList.join(", ")} (db: ${dbInfo.databaseName}) (${memories.length})\n`);
|
|
154
169
|
if (memories.length === 0) {
|
|
155
170
|
console.log(" No memories found with these tags.");
|
|
156
171
|
return;
|
|
@@ -165,4 +180,36 @@ brainCommand
|
|
|
165
180
|
process.exit(1);
|
|
166
181
|
}
|
|
167
182
|
});
|
|
183
|
+
brainCommand
|
|
184
|
+
.command("info")
|
|
185
|
+
.description("Show current brain configuration")
|
|
186
|
+
.option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
|
|
187
|
+
.option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
|
|
188
|
+
.option("--json", "Output as JSON")
|
|
189
|
+
.action(async (options) => {
|
|
190
|
+
try {
|
|
191
|
+
const brain = createBrain(options.agent, options.agentType);
|
|
192
|
+
const dbInfo = brain.getDatabaseInfo();
|
|
193
|
+
if (options.json) {
|
|
194
|
+
console.log(JSON.stringify({
|
|
195
|
+
agent: options.agent,
|
|
196
|
+
agentType: dbInfo.agentType || null,
|
|
197
|
+
database: dbInfo.databaseName,
|
|
198
|
+
availableTypes: AGENT_TYPES
|
|
199
|
+
}));
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
console.log(`\n 🧠 Brain Configuration`);
|
|
203
|
+
console.log(` ────────────────────────────────`);
|
|
204
|
+
console.log(` Agent ID: ${options.agent}`);
|
|
205
|
+
console.log(` Agent Type: ${dbInfo.agentType || '(not set - using default)'}`);
|
|
206
|
+
console.log(` Database: ${dbInfo.databaseName}`);
|
|
207
|
+
console.log(`\n Available agent types: ${AGENT_TYPES.join(", ")}`);
|
|
208
|
+
console.log("");
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
console.error("Error:", error.message);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
168
215
|
export default brainCommand;
|
package/dist/commands/chat.js
CHANGED
|
@@ -620,6 +620,228 @@ chatCommand
|
|
|
620
620
|
await new Promise(() => { });
|
|
621
621
|
});
|
|
622
622
|
// ============================================
|
|
623
|
+
// AGENT QUESTION COMMANDS (prevents supervisor loop)
|
|
624
|
+
// ============================================
|
|
625
|
+
// Agent persona/role icons for Google Chat messages
|
|
626
|
+
const AGENT_PERSONA_ICONS = {
|
|
627
|
+
support: "🎧",
|
|
628
|
+
worker: "👷",
|
|
629
|
+
supervisor: "🎯",
|
|
630
|
+
reviewer: "🔍",
|
|
631
|
+
research: "🔬",
|
|
632
|
+
accounting: "📊",
|
|
633
|
+
marketing: "📢",
|
|
634
|
+
developer: "💻",
|
|
635
|
+
devops: "🔧",
|
|
636
|
+
default: "🤖",
|
|
637
|
+
};
|
|
638
|
+
chatCommand
|
|
639
|
+
.command("ask <question>")
|
|
640
|
+
.description("Ask a question to human via Google Chat (registers conversation for reply routing)")
|
|
641
|
+
.requiredOption("--space <id>", "Google Chat space ID (e.g., spaces/ABC123)")
|
|
642
|
+
.option("--agent-id <id>", "Agent ID (default: from env or hostname)")
|
|
643
|
+
.option("--vm-name <name>", "VM name (default: from env or hostname)")
|
|
644
|
+
.option("--session <name>", "Tmux session for reply routing", "main")
|
|
645
|
+
.option("--role <role>", "Agent role/persona (support, worker, supervisor, etc.)")
|
|
646
|
+
.option("--context <text>", "Additional context for the question")
|
|
647
|
+
.option("--task-id <id>", "Related task ID")
|
|
648
|
+
.option("--json", "Output as JSON")
|
|
649
|
+
.action(async (question, options) => {
|
|
650
|
+
const config = getConfig();
|
|
651
|
+
const huskyApiUrl = getHuskyApiUrl();
|
|
652
|
+
if (!huskyApiUrl) {
|
|
653
|
+
console.error("Error: API URL not configured. Set husky-api-url or api-url.");
|
|
654
|
+
process.exit(1);
|
|
655
|
+
}
|
|
656
|
+
// Get agent/VM identity
|
|
657
|
+
const agentId = options.agentId || process.env.HUSKY_AGENT_ID || process.env.HUSKY_WORKER_ID || `agent-${process.pid}`;
|
|
658
|
+
const vmName = options.vmName || process.env.HUSKY_VM_NAME || process.env.HOSTNAME || "unknown-vm";
|
|
659
|
+
const tmuxSession = options.session;
|
|
660
|
+
const agentRole = options.role || process.env.HUSKY_AGENT_TYPE || process.env.HUSKY_AGENT_ROLE || "worker";
|
|
661
|
+
const taskId = options.taskId || process.env.HUSKY_TASK_ID;
|
|
662
|
+
// Build formatted message with metadata
|
|
663
|
+
const icon = AGENT_PERSONA_ICONS[agentRole] || AGENT_PERSONA_ICONS.default;
|
|
664
|
+
let formattedMessage = `${icon} *Agent Question*\n\n`;
|
|
665
|
+
formattedMessage += `*From:* ${agentId} (${agentRole})\n`;
|
|
666
|
+
formattedMessage += `*VM:* ${vmName} / session: ${tmuxSession}\n`;
|
|
667
|
+
if (taskId) {
|
|
668
|
+
formattedMessage += `*Task:* ${taskId}\n`;
|
|
669
|
+
}
|
|
670
|
+
formattedMessage += `\n---\n\n${question}`;
|
|
671
|
+
if (options.context) {
|
|
672
|
+
formattedMessage += `\n\n*Context:* ${options.context}`;
|
|
673
|
+
}
|
|
674
|
+
try {
|
|
675
|
+
// 1. Send message to Google Chat
|
|
676
|
+
const sendRes = await fetch(`${huskyApiUrl}/api/google-chat/send`, {
|
|
677
|
+
method: "POST",
|
|
678
|
+
headers: {
|
|
679
|
+
"Content-Type": "application/json",
|
|
680
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
681
|
+
},
|
|
682
|
+
body: JSON.stringify({
|
|
683
|
+
text: formattedMessage,
|
|
684
|
+
spaceName: options.space,
|
|
685
|
+
// Don't specify threadName - let Google Chat create a new thread
|
|
686
|
+
}),
|
|
687
|
+
});
|
|
688
|
+
if (!sendRes.ok) {
|
|
689
|
+
const error = await sendRes.text();
|
|
690
|
+
throw new Error(`Failed to send message: ${sendRes.status} - ${error}`);
|
|
691
|
+
}
|
|
692
|
+
const sendData = await sendRes.json();
|
|
693
|
+
const threadName = sendData.threadName || sendData.name;
|
|
694
|
+
if (!threadName) {
|
|
695
|
+
console.error("Warning: Could not get thread name from response. Reply routing may not work.");
|
|
696
|
+
}
|
|
697
|
+
// 2. Register conversation for reply routing (with full metadata)
|
|
698
|
+
const convRes = await fetch(`${huskyApiUrl}/api/agent-conversations`, {
|
|
699
|
+
method: "POST",
|
|
700
|
+
headers: {
|
|
701
|
+
"Content-Type": "application/json",
|
|
702
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
703
|
+
},
|
|
704
|
+
body: JSON.stringify({
|
|
705
|
+
agentId,
|
|
706
|
+
vmName,
|
|
707
|
+
tmuxSession,
|
|
708
|
+
spaceId: options.space,
|
|
709
|
+
threadName,
|
|
710
|
+
question,
|
|
711
|
+
status: "active",
|
|
712
|
+
// Additional metadata for routing and context
|
|
713
|
+
agentRole,
|
|
714
|
+
taskId: taskId || null,
|
|
715
|
+
context: options.context || null,
|
|
716
|
+
}),
|
|
717
|
+
});
|
|
718
|
+
let conversationId;
|
|
719
|
+
if (convRes.ok) {
|
|
720
|
+
const convData = await convRes.json();
|
|
721
|
+
conversationId = convData.id;
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
console.error("Warning: Failed to register conversation. Reply may go to supervisor instead.");
|
|
725
|
+
}
|
|
726
|
+
if (options.json) {
|
|
727
|
+
console.log(JSON.stringify({
|
|
728
|
+
success: true,
|
|
729
|
+
agentId,
|
|
730
|
+
agentRole,
|
|
731
|
+
vmName,
|
|
732
|
+
tmuxSession,
|
|
733
|
+
threadName,
|
|
734
|
+
conversationId,
|
|
735
|
+
space: options.space,
|
|
736
|
+
question,
|
|
737
|
+
taskId: taskId || null,
|
|
738
|
+
}, null, 2));
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
console.log("✅ Question sent to Google Chat");
|
|
742
|
+
console.log(` Agent: ${agentId} (${agentRole})`);
|
|
743
|
+
console.log(` VM: ${vmName} / session: ${tmuxSession}`);
|
|
744
|
+
if (taskId) {
|
|
745
|
+
console.log(` Task: ${taskId}`);
|
|
746
|
+
}
|
|
747
|
+
console.log(` Thread: ${threadName || "(unknown)"}`);
|
|
748
|
+
if (conversationId) {
|
|
749
|
+
console.log(` Conversation ID: ${conversationId}`);
|
|
750
|
+
}
|
|
751
|
+
console.log("\n When human replies, it will be routed to your tmux session.");
|
|
752
|
+
console.log(` To resolve conversation: husky chat resolve ${conversationId || threadName}`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
catch (error) {
|
|
756
|
+
console.error("Error asking question:", error);
|
|
757
|
+
process.exit(1);
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
chatCommand
|
|
761
|
+
.command("resolve <conversationId>")
|
|
762
|
+
.description("Mark a conversation as resolved (stops routing replies to agent)")
|
|
763
|
+
.action(async (conversationId) => {
|
|
764
|
+
const config = getConfig();
|
|
765
|
+
const huskyApiUrl = getHuskyApiUrl();
|
|
766
|
+
if (!huskyApiUrl) {
|
|
767
|
+
console.error("Error: API URL not configured. Set husky-api-url or api-url.");
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
const res = await fetch(`${huskyApiUrl}/api/agent-conversations/${conversationId}`, {
|
|
772
|
+
method: "PATCH",
|
|
773
|
+
headers: {
|
|
774
|
+
"Content-Type": "application/json",
|
|
775
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
776
|
+
},
|
|
777
|
+
body: JSON.stringify({ status: "resolved" }),
|
|
778
|
+
});
|
|
779
|
+
if (!res.ok) {
|
|
780
|
+
if (res.status === 404) {
|
|
781
|
+
console.error("Conversation not found.");
|
|
782
|
+
process.exit(1);
|
|
783
|
+
}
|
|
784
|
+
throw new Error(`API error: ${res.status}`);
|
|
785
|
+
}
|
|
786
|
+
console.log("✅ Conversation marked as resolved.");
|
|
787
|
+
console.log(" Future replies in this thread will go to supervisor.");
|
|
788
|
+
}
|
|
789
|
+
catch (error) {
|
|
790
|
+
console.error("Error resolving conversation:", error);
|
|
791
|
+
process.exit(1);
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
chatCommand
|
|
795
|
+
.command("conversations")
|
|
796
|
+
.description("List active agent conversations")
|
|
797
|
+
.option("--all", "Show all conversations (including resolved)")
|
|
798
|
+
.option("--agent <id>", "Filter by agent ID")
|
|
799
|
+
.option("--json", "Output as JSON")
|
|
800
|
+
.action(async (options) => {
|
|
801
|
+
const config = getConfig();
|
|
802
|
+
const huskyApiUrl = getHuskyApiUrl();
|
|
803
|
+
if (!huskyApiUrl) {
|
|
804
|
+
console.error("Error: API URL not configured. Set husky-api-url or api-url.");
|
|
805
|
+
process.exit(1);
|
|
806
|
+
}
|
|
807
|
+
try {
|
|
808
|
+
const params = new URLSearchParams();
|
|
809
|
+
if (!options.all)
|
|
810
|
+
params.set("status", "active");
|
|
811
|
+
if (options.agent)
|
|
812
|
+
params.set("agentId", options.agent);
|
|
813
|
+
const res = await fetch(`${huskyApiUrl}/api/agent-conversations?${params}`, {
|
|
814
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
815
|
+
});
|
|
816
|
+
if (!res.ok) {
|
|
817
|
+
throw new Error(`API error: ${res.status}`);
|
|
818
|
+
}
|
|
819
|
+
const data = await res.json();
|
|
820
|
+
if (options.json) {
|
|
821
|
+
console.log(JSON.stringify(data, null, 2));
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (!data.conversations || data.conversations.length === 0) {
|
|
825
|
+
console.log("No active conversations.");
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
console.log("\n Agent Conversations");
|
|
829
|
+
console.log(" " + "─".repeat(60));
|
|
830
|
+
for (const conv of data.conversations) {
|
|
831
|
+
const time = new Date(conv.createdAt).toLocaleString();
|
|
832
|
+
const statusIcon = conv.status === "active" ? "🟢" : "⚪";
|
|
833
|
+
console.log(` ${statusIcon} [${conv.id.slice(0, 8)}] ${conv.agentId} @ ${conv.vmName}`);
|
|
834
|
+
console.log(` Q: "${conv.question.slice(0, 50)}${conv.question.length > 50 ? "..." : ""}"`);
|
|
835
|
+
console.log(` Created: ${time}`);
|
|
836
|
+
console.log("");
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
catch (error) {
|
|
840
|
+
console.error("Error fetching conversations:", error);
|
|
841
|
+
process.exit(1);
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
// ============================================
|
|
623
845
|
// REVIEW COMMANDS (kept for backwards compatibility)
|
|
624
846
|
// ============================================
|
|
625
847
|
chatCommand
|
package/dist/commands/config.js
CHANGED
|
@@ -158,6 +158,7 @@ configCommand
|
|
|
158
158
|
"gcp-location": "gcpLocation",
|
|
159
159
|
"gotess-token": "gotessToken",
|
|
160
160
|
"gotess-book-id": "gotessBookId",
|
|
161
|
+
"agent-type": "agentType",
|
|
161
162
|
};
|
|
162
163
|
const configKey = keyMappings[key];
|
|
163
164
|
if (!configKey) {
|
|
@@ -170,6 +171,7 @@ configCommand
|
|
|
170
171
|
console.log(" Qdrant: qdrant-url, qdrant-api-key");
|
|
171
172
|
console.log(" GCP: gcp-project-id, gcp-location");
|
|
172
173
|
console.log(" Gotess: gotess-token, gotess-book-id");
|
|
174
|
+
console.log(" Brain: agent-type");
|
|
173
175
|
process.exit(1);
|
|
174
176
|
}
|
|
175
177
|
// Validation for specific keys
|
|
@@ -180,6 +182,13 @@ configCommand
|
|
|
180
182
|
process.exit(1);
|
|
181
183
|
}
|
|
182
184
|
}
|
|
185
|
+
if (key === "agent-type") {
|
|
186
|
+
const validTypes = ["support", "claude", "gotess", "supervisor", "worker"];
|
|
187
|
+
if (!validTypes.includes(value)) {
|
|
188
|
+
console.error(`Error: Invalid agent type. Must be one of: ${validTypes.join(", ")}`);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
183
192
|
// Set the value
|
|
184
193
|
config[configKey] = value;
|
|
185
194
|
saveConfig(config);
|
package/dist/commands/vm.js
CHANGED
|
@@ -3,6 +3,7 @@ import { getConfig } from "./config.js";
|
|
|
3
3
|
import * as readline from "readline";
|
|
4
4
|
import * as fs from "fs";
|
|
5
5
|
import * as path from "path";
|
|
6
|
+
import { spawnSync } from "child_process";
|
|
6
7
|
import { DEFAULT_AGENT_CONFIGS, generateStartupScript, listDefaultAgentTypes, getDefaultAgentConfig, } from "../lib/agent-templates.js";
|
|
7
8
|
import { requirePermission } from "../lib/permissions.js";
|
|
8
9
|
export const vmCommand = new Command("vm").description("Manage VM sessions");
|
|
@@ -747,6 +748,340 @@ vmCommand
|
|
|
747
748
|
const script = generateStartupScript(options.type, options.huskyUrl, options.huskyKey, options.project);
|
|
748
749
|
console.log(script);
|
|
749
750
|
});
|
|
751
|
+
// ============================================================================
|
|
752
|
+
// VM Monitoring Commands
|
|
753
|
+
// ============================================================================
|
|
754
|
+
// Patterns that indicate an agent is stuck
|
|
755
|
+
const STUCK_PATTERNS = [
|
|
756
|
+
{ pattern: /Do you want to allow|Allow this action|allow this tool/i, reason: "Waiting for allow/deny prompt" },
|
|
757
|
+
{ pattern: /\[y\/n\]|\[Y\/n\]|\(yes\/no\)/i, reason: "Waiting for yes/no confirmation" },
|
|
758
|
+
{ pattern: /Press Enter to continue|press any key/i, reason: "Waiting for user input" },
|
|
759
|
+
{ pattern: /Error:|error:|FAILED|failed/i, reason: "Error detected" },
|
|
760
|
+
{ pattern: /permission denied|Permission denied/i, reason: "Permission denied error" },
|
|
761
|
+
{ pattern: /timed out|timeout|Timeout/i, reason: "Timeout error" },
|
|
762
|
+
];
|
|
763
|
+
// Helper to run SSH command on a VM using spawnSync (no shell injection)
|
|
764
|
+
function sshCommand(vmName, zone, command) {
|
|
765
|
+
try {
|
|
766
|
+
const result = spawnSync("gcloud", [
|
|
767
|
+
"compute", "ssh", vmName,
|
|
768
|
+
`--zone=${zone}`,
|
|
769
|
+
`--command=${command}`,
|
|
770
|
+
], { encoding: "utf-8", timeout: 30000 });
|
|
771
|
+
if (result.status !== 0)
|
|
772
|
+
return null;
|
|
773
|
+
return result.stdout?.trim() || null;
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
// Get tmux sessions on a VM
|
|
780
|
+
function getTmuxSessions(vmName, zone) {
|
|
781
|
+
const result = sshCommand(vmName, zone, "tmux list-sessions -F '#{session_name}' 2>/dev/null || true");
|
|
782
|
+
if (!result)
|
|
783
|
+
return [];
|
|
784
|
+
return result.split("\n").filter(Boolean);
|
|
785
|
+
}
|
|
786
|
+
// Capture last lines from a tmux session
|
|
787
|
+
function captureTmuxOutput(vmName, zone, sessionName, lines = 50) {
|
|
788
|
+
return sshCommand(vmName, zone, `tmux capture-pane -t ${sessionName} -p -S -${lines} 2>/dev/null || true`);
|
|
789
|
+
}
|
|
790
|
+
// Check if output matches stuck patterns
|
|
791
|
+
function checkForStuckPatterns(output) {
|
|
792
|
+
for (const { pattern, reason } of STUCK_PATTERNS) {
|
|
793
|
+
if (pattern.test(output)) {
|
|
794
|
+
return { isStuck: true, reason };
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return { isStuck: false };
|
|
798
|
+
}
|
|
799
|
+
// husky vm monitor
|
|
800
|
+
vmCommand
|
|
801
|
+
.command("monitor")
|
|
802
|
+
.description("Monitor agent status across running VMs")
|
|
803
|
+
.option("--vm <name>", "Monitor specific VM only")
|
|
804
|
+
.option("--watch", "Continuous monitoring (refresh every 10s)")
|
|
805
|
+
.option("--json", "Output as JSON")
|
|
806
|
+
.action(async (options) => {
|
|
807
|
+
const config = ensureConfig();
|
|
808
|
+
// Get running VMs from API
|
|
809
|
+
const getRunningVMs = async () => {
|
|
810
|
+
const url = new URL("/api/vm-sessions", config.apiUrl);
|
|
811
|
+
url.searchParams.set("vmStatus", "running");
|
|
812
|
+
const res = await fetch(url.toString(), {
|
|
813
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
814
|
+
});
|
|
815
|
+
if (!res.ok) {
|
|
816
|
+
throw new Error(`API error: ${res.status}`);
|
|
817
|
+
}
|
|
818
|
+
const data = await res.json();
|
|
819
|
+
return data.sessions || [];
|
|
820
|
+
};
|
|
821
|
+
const monitorVMs = async () => {
|
|
822
|
+
let sessions;
|
|
823
|
+
try {
|
|
824
|
+
sessions = await getRunningVMs();
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
console.error("Error fetching VMs:", error);
|
|
828
|
+
return [];
|
|
829
|
+
}
|
|
830
|
+
// Filter by VM name if specified
|
|
831
|
+
if (options.vm) {
|
|
832
|
+
sessions = sessions.filter((s) => s.vmName === options.vm || s.name === options.vm || s.id === options.vm);
|
|
833
|
+
}
|
|
834
|
+
if (sessions.length === 0) {
|
|
835
|
+
console.log("\n No running VMs found.");
|
|
836
|
+
return [];
|
|
837
|
+
}
|
|
838
|
+
const results = [];
|
|
839
|
+
for (const session of sessions) {
|
|
840
|
+
const vmStatus = {
|
|
841
|
+
vmName: session.vmName,
|
|
842
|
+
zone: session.vmZone,
|
|
843
|
+
ip: session.vmIpAddress,
|
|
844
|
+
sessions: [],
|
|
845
|
+
isStuck: false,
|
|
846
|
+
};
|
|
847
|
+
// Get tmux sessions on this VM
|
|
848
|
+
const tmuxSessions = getTmuxSessions(session.vmName, session.vmZone);
|
|
849
|
+
if (tmuxSessions.length === 0) {
|
|
850
|
+
vmStatus.sessions.push({
|
|
851
|
+
sessionName: "(none)",
|
|
852
|
+
lastOutput: "No tmux sessions running",
|
|
853
|
+
isStuck: false,
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
else {
|
|
857
|
+
for (const tmuxSession of tmuxSessions) {
|
|
858
|
+
const output = captureTmuxOutput(session.vmName, session.vmZone, tmuxSession) || "";
|
|
859
|
+
const stuckCheck = checkForStuckPatterns(output);
|
|
860
|
+
const sessionStatus = {
|
|
861
|
+
sessionName: tmuxSession,
|
|
862
|
+
lastOutput: output.split("\n").slice(-10).join("\n"),
|
|
863
|
+
isStuck: stuckCheck.isStuck,
|
|
864
|
+
stuckReason: stuckCheck.reason,
|
|
865
|
+
};
|
|
866
|
+
vmStatus.sessions.push(sessionStatus);
|
|
867
|
+
if (stuckCheck.isStuck) {
|
|
868
|
+
vmStatus.isStuck = true;
|
|
869
|
+
vmStatus.stuckReason = `${tmuxSession}: ${stuckCheck.reason}`;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
results.push(vmStatus);
|
|
874
|
+
}
|
|
875
|
+
return results;
|
|
876
|
+
};
|
|
877
|
+
// Initial check
|
|
878
|
+
const results = await monitorVMs();
|
|
879
|
+
if (options.json) {
|
|
880
|
+
console.log(JSON.stringify(results, null, 2));
|
|
881
|
+
if (!options.watch)
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
printMonitorResults(results);
|
|
886
|
+
}
|
|
887
|
+
// Watch mode
|
|
888
|
+
if (options.watch) {
|
|
889
|
+
console.log("\n--- Monitoring (Ctrl+C to stop, refreshing every 10s) ---\n");
|
|
890
|
+
const interval = setInterval(async () => {
|
|
891
|
+
const newResults = await monitorVMs();
|
|
892
|
+
if (options.json) {
|
|
893
|
+
console.log(JSON.stringify(newResults, null, 2));
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
console.log("\x1b[2J\x1b[H"); // Clear screen
|
|
897
|
+
console.log(`VM Monitor - ${new Date().toLocaleTimeString()}`);
|
|
898
|
+
printMonitorResults(newResults);
|
|
899
|
+
}
|
|
900
|
+
}, 10000);
|
|
901
|
+
process.on("SIGINT", () => {
|
|
902
|
+
clearInterval(interval);
|
|
903
|
+
console.log("\nMonitoring stopped.");
|
|
904
|
+
process.exit(0);
|
|
905
|
+
});
|
|
906
|
+
await new Promise(() => { });
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
// husky vm unstuck <vmName>
|
|
910
|
+
vmCommand
|
|
911
|
+
.command("unstuck <vmName>")
|
|
912
|
+
.description("Try to unstick an agent by sending input")
|
|
913
|
+
.option("--session <name>", "Target specific tmux session", "main")
|
|
914
|
+
.option("--action <action>", "Action to send (see --list-actions)", "enter")
|
|
915
|
+
.option("--zone <zone>", "GCP zone", "europe-west1-b")
|
|
916
|
+
.option("--text <text>", "Send custom text (use with --action custom)")
|
|
917
|
+
.option("--list-actions", "Show all available actions")
|
|
918
|
+
.option("--json", "Output as JSON")
|
|
919
|
+
.action(async (vmName, options) => {
|
|
920
|
+
// All available actions for different AI agents
|
|
921
|
+
const actionKeys = {
|
|
922
|
+
// Basic actions
|
|
923
|
+
enter: "Enter",
|
|
924
|
+
yes: "y Enter",
|
|
925
|
+
no: "n Enter",
|
|
926
|
+
// Claude Code / OpenCode prompts
|
|
927
|
+
allow: "y Enter", // Allow single action
|
|
928
|
+
deny: "n Enter", // Deny action
|
|
929
|
+
"allow-all": "a Enter", // Allow all (OpenCode)
|
|
930
|
+
a: "a Enter", // Shorthand for allow-all
|
|
931
|
+
// Numbered menu selections (common in OpenCode)
|
|
932
|
+
"1": "1 Enter",
|
|
933
|
+
"2": "2 Enter",
|
|
934
|
+
"3": "3 Enter",
|
|
935
|
+
"4": "4 Enter",
|
|
936
|
+
"5": "5 Enter",
|
|
937
|
+
// Common responses
|
|
938
|
+
skip: "s Enter", // Skip
|
|
939
|
+
cancel: "Escape", // Cancel/escape
|
|
940
|
+
quit: "q Enter", // Quit
|
|
941
|
+
continue: "c Enter", // Continue
|
|
942
|
+
retry: "r Enter", // Retry
|
|
943
|
+
// Custom text (use with --text)
|
|
944
|
+
custom: "",
|
|
945
|
+
};
|
|
946
|
+
// Show available actions
|
|
947
|
+
if (options.listActions) {
|
|
948
|
+
console.log("\n Available unstuck actions:\n");
|
|
949
|
+
console.log(" Basic:");
|
|
950
|
+
console.log(" enter - Press Enter");
|
|
951
|
+
console.log(" yes / y - Send 'y' + Enter");
|
|
952
|
+
console.log(" no / n - Send 'n' + Enter");
|
|
953
|
+
console.log("\n AI Agent Prompts:");
|
|
954
|
+
console.log(" allow - Allow action (y + Enter)");
|
|
955
|
+
console.log(" deny - Deny action (n + Enter)");
|
|
956
|
+
console.log(" allow-all/a - Allow all (a + Enter) - OpenCode");
|
|
957
|
+
console.log("\n Menu Selections:");
|
|
958
|
+
console.log(" 1, 2, 3, 4, 5 - Send number + Enter");
|
|
959
|
+
console.log("\n Other:");
|
|
960
|
+
console.log(" skip - Skip (s + Enter)");
|
|
961
|
+
console.log(" cancel - Escape key");
|
|
962
|
+
console.log(" quit - Quit (q + Enter)");
|
|
963
|
+
console.log(" continue - Continue (c + Enter)");
|
|
964
|
+
console.log(" retry - Retry (r + Enter)");
|
|
965
|
+
console.log(" custom - Use with --text to send custom input");
|
|
966
|
+
console.log("\n Examples:");
|
|
967
|
+
console.log(" husky vm unstuck supervisor --action allow-all");
|
|
968
|
+
console.log(" husky vm unstuck worker-1 --action 2");
|
|
969
|
+
console.log(' husky vm unstuck support --action custom --text "my response"');
|
|
970
|
+
console.log("");
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
// Handle custom text action
|
|
974
|
+
let keys;
|
|
975
|
+
if (options.action === "custom") {
|
|
976
|
+
if (!options.text) {
|
|
977
|
+
console.error("Error: --text is required when using --action custom");
|
|
978
|
+
process.exit(1);
|
|
979
|
+
}
|
|
980
|
+
// Escape special characters for tmux send-keys
|
|
981
|
+
const escapedText = options.text.replace(/"/g, '\\"').replace(/'/g, "\\'");
|
|
982
|
+
keys = `"${escapedText}" Enter`;
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
keys = actionKeys[options.action] || "Enter";
|
|
986
|
+
if (!actionKeys[options.action]) {
|
|
987
|
+
console.warn(`Warning: Unknown action "${options.action}", sending Enter`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
console.log(`Sending "${options.action}" to ${vmName}:${options.session}...`);
|
|
991
|
+
try {
|
|
992
|
+
// First check if session exists
|
|
993
|
+
const sessions = getTmuxSessions(vmName, options.zone);
|
|
994
|
+
if (!sessions.includes(options.session)) {
|
|
995
|
+
console.error(`Error: Session "${options.session}" not found on ${vmName}`);
|
|
996
|
+
console.log(`Available sessions: ${sessions.join(", ") || "(none)"}`);
|
|
997
|
+
process.exit(1);
|
|
998
|
+
}
|
|
999
|
+
// Send keys to tmux session
|
|
1000
|
+
const result = spawnSync("gcloud", [
|
|
1001
|
+
"compute", "ssh", vmName,
|
|
1002
|
+
`--zone=${options.zone}`,
|
|
1003
|
+
`--command=tmux send-keys -t ${options.session} ${keys}`,
|
|
1004
|
+
], { encoding: "utf-8", timeout: 30000 });
|
|
1005
|
+
if (result.status !== 0) {
|
|
1006
|
+
throw new Error(result.stderr || "Failed to send keys");
|
|
1007
|
+
}
|
|
1008
|
+
// Wait a moment and capture output
|
|
1009
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1010
|
+
const output = captureTmuxOutput(vmName, options.zone, options.session, 20);
|
|
1011
|
+
if (options.json) {
|
|
1012
|
+
console.log(JSON.stringify({
|
|
1013
|
+
success: true,
|
|
1014
|
+
action: options.action,
|
|
1015
|
+
vmName,
|
|
1016
|
+
session: options.session,
|
|
1017
|
+
lastOutput: output?.split("\n").slice(-10).join("\n"),
|
|
1018
|
+
}, null, 2));
|
|
1019
|
+
}
|
|
1020
|
+
else {
|
|
1021
|
+
console.log(`\nAction sent successfully!`);
|
|
1022
|
+
console.log(`\nLast output from ${options.session}:`);
|
|
1023
|
+
console.log("---");
|
|
1024
|
+
console.log(output?.split("\n").slice(-10).join("\n") || "(no output)");
|
|
1025
|
+
console.log("---");
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
catch (error) {
|
|
1029
|
+
console.error("Error:", error);
|
|
1030
|
+
process.exit(1);
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
// husky vm ssh <vmName>
|
|
1034
|
+
vmCommand
|
|
1035
|
+
.command("ssh <vmName>")
|
|
1036
|
+
.description("SSH into a VM (opens interactive shell)")
|
|
1037
|
+
.option("--zone <zone>", "GCP zone", "europe-west1-b")
|
|
1038
|
+
.option("--command <cmd>", "Run command instead of interactive shell")
|
|
1039
|
+
.action(async (vmName, options) => {
|
|
1040
|
+
if (options.command) {
|
|
1041
|
+
const result = sshCommand(vmName, options.zone, options.command);
|
|
1042
|
+
if (result !== null) {
|
|
1043
|
+
console.log(result);
|
|
1044
|
+
}
|
|
1045
|
+
else {
|
|
1046
|
+
console.error("Failed to execute command");
|
|
1047
|
+
process.exit(1);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
else {
|
|
1051
|
+
// Interactive SSH
|
|
1052
|
+
spawnSync("gcloud", ["compute", "ssh", vmName, `--zone=${options.zone}`], {
|
|
1053
|
+
stdio: "inherit",
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
function printMonitorResults(results) {
|
|
1058
|
+
console.log("\n VM MONITOR STATUS");
|
|
1059
|
+
console.log(" " + "=".repeat(80));
|
|
1060
|
+
for (const vm of results) {
|
|
1061
|
+
const stuckIndicator = vm.isStuck ? " [STUCK]" : "";
|
|
1062
|
+
console.log(`\n ${vm.vmName}${stuckIndicator}`);
|
|
1063
|
+
console.log(` Zone: ${vm.zone} | IP: ${vm.ip || "N/A"}`);
|
|
1064
|
+
console.log(" " + "-".repeat(60));
|
|
1065
|
+
for (const session of vm.sessions) {
|
|
1066
|
+
const sessionStuck = session.isStuck ? ` <- STUCK: ${session.stuckReason}` : "";
|
|
1067
|
+
console.log(`\n [${session.sessionName}]${sessionStuck}`);
|
|
1068
|
+
const lines = session.lastOutput.split("\n").slice(-5);
|
|
1069
|
+
for (const line of lines) {
|
|
1070
|
+
console.log(` ${line.substring(0, 76)}`);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
const stuckVMs = results.filter((r) => r.isStuck);
|
|
1075
|
+
if (stuckVMs.length > 0) {
|
|
1076
|
+
console.log("\n " + "=".repeat(80));
|
|
1077
|
+
console.log(` WARNING: ${stuckVMs.length} VM(s) appear stuck:`);
|
|
1078
|
+
for (const vm of stuckVMs) {
|
|
1079
|
+
console.log(` - ${vm.vmName}: ${vm.stuckReason}`);
|
|
1080
|
+
}
|
|
1081
|
+
console.log(`\n To unstick, run: husky vm unstuck <vmName> --action allow`);
|
|
1082
|
+
}
|
|
1083
|
+
console.log("");
|
|
1084
|
+
}
|
|
750
1085
|
// Print helpers
|
|
751
1086
|
function printVMSessions(sessions, stats) {
|
|
752
1087
|
if (sessions.length === 0) {
|
package/dist/commands/worker.js
CHANGED
|
@@ -9,9 +9,28 @@ workerCommand
|
|
|
9
9
|
.description("Show current worker identity")
|
|
10
10
|
.option("--json", "Output as JSON")
|
|
11
11
|
.action(async (options) => {
|
|
12
|
+
const config = getConfig();
|
|
12
13
|
const identity = getWorkerIdentity();
|
|
14
|
+
// Fetch role from API
|
|
15
|
+
let role = "unknown";
|
|
16
|
+
let permissions = [];
|
|
17
|
+
if (config.apiUrl && config.apiKey) {
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(`${config.apiUrl}/api/auth/whoami`, {
|
|
20
|
+
headers: { "x-api-key": config.apiKey },
|
|
21
|
+
});
|
|
22
|
+
if (res.ok) {
|
|
23
|
+
const data = await res.json();
|
|
24
|
+
role = data.role || "unknown";
|
|
25
|
+
permissions = data.permissions || [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Silently fail - role will show as "unknown"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
13
32
|
if (options.json) {
|
|
14
|
-
console.log(JSON.stringify(identity, null, 2));
|
|
33
|
+
console.log(JSON.stringify({ ...identity, role, permissions }, null, 2));
|
|
15
34
|
}
|
|
16
35
|
else {
|
|
17
36
|
console.log("\n Worker Identity");
|
|
@@ -22,6 +41,11 @@ workerCommand
|
|
|
22
41
|
console.log(` User: ${identity.username}`);
|
|
23
42
|
console.log(` Platform: ${identity.platform}`);
|
|
24
43
|
console.log(` Version: ${identity.agentVersion}`);
|
|
44
|
+
console.log(" " + "─".repeat(40));
|
|
45
|
+
console.log(` Role: ${role}`);
|
|
46
|
+
if (permissions.length > 0) {
|
|
47
|
+
console.log(` Perms: ${permissions.join(", ")}`);
|
|
48
|
+
}
|
|
25
49
|
console.log("");
|
|
26
50
|
}
|
|
27
51
|
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export declare const AGENT_TYPES: readonly ["support", "claude", "gotess", "supervisor", "worker"];
|
|
2
|
+
export type AgentType = typeof AGENT_TYPES[number];
|
|
1
3
|
export interface Memory {
|
|
2
4
|
id: string;
|
|
3
5
|
agent: string;
|
|
@@ -12,12 +14,26 @@ export interface RecallResult {
|
|
|
12
14
|
memory: Memory;
|
|
13
15
|
score: number;
|
|
14
16
|
}
|
|
17
|
+
export interface AgentBrainOptions {
|
|
18
|
+
agentId: string;
|
|
19
|
+
agentType?: AgentType;
|
|
20
|
+
projectId?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function isValidAgentType(value: string | undefined): value is AgentType;
|
|
23
|
+
export declare function getAgentType(): AgentType | undefined;
|
|
24
|
+
export declare function getDatabaseName(agentType?: AgentType): string;
|
|
15
25
|
export declare class AgentBrain {
|
|
16
26
|
private db;
|
|
17
27
|
private embeddings;
|
|
18
28
|
private agentId;
|
|
29
|
+
private agentType?;
|
|
19
30
|
private collectionPath;
|
|
20
|
-
|
|
31
|
+
private databaseName;
|
|
32
|
+
constructor(agentIdOrOptions: string | AgentBrainOptions, projectId?: string);
|
|
33
|
+
getDatabaseInfo(): {
|
|
34
|
+
agentType?: AgentType;
|
|
35
|
+
databaseName: string;
|
|
36
|
+
};
|
|
21
37
|
remember(content: string, tags?: string[], metadata?: Record<string, unknown>): Promise<string>;
|
|
22
38
|
recall(query: string, limit?: number, minScore?: number): Promise<RecallResult[]>;
|
|
23
39
|
recallByTags(tags: string[], limit?: number): Promise<Memory[]>;
|
|
@@ -3,24 +3,85 @@ import { getFirestore, Timestamp } from 'firebase-admin/firestore';
|
|
|
3
3
|
import { EmbeddingService } from './embeddings.js';
|
|
4
4
|
import { getConfig } from '../../commands/config.js';
|
|
5
5
|
const BRAIN_COLLECTION = 'agent_brains';
|
|
6
|
+
const DEFAULT_DATABASE = '(default)';
|
|
7
|
+
export const AGENT_TYPES = ['support', 'claude', 'gotess', 'supervisor', 'worker'];
|
|
8
|
+
const AGENT_DB_MAP = {
|
|
9
|
+
support: 'support-brain-db',
|
|
10
|
+
claude: 'claude-brain-db',
|
|
11
|
+
gotess: 'gotess-brain-db',
|
|
12
|
+
supervisor: 'supervisor-brain-db',
|
|
13
|
+
worker: 'worker-brain-db',
|
|
14
|
+
};
|
|
15
|
+
const firestoreCache = new Map();
|
|
16
|
+
let firebaseInitialized = false;
|
|
17
|
+
export function isValidAgentType(value) {
|
|
18
|
+
return value !== undefined && AGENT_TYPES.includes(value);
|
|
19
|
+
}
|
|
20
|
+
export function getAgentType() {
|
|
21
|
+
const envType = process.env.HUSKY_AGENT_TYPE;
|
|
22
|
+
if (isValidAgentType(envType)) {
|
|
23
|
+
return envType;
|
|
24
|
+
}
|
|
25
|
+
const config = getConfig();
|
|
26
|
+
const configType = config.agentType;
|
|
27
|
+
if (isValidAgentType(configType)) {
|
|
28
|
+
return configType;
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
export function getDatabaseName(agentType) {
|
|
33
|
+
if (!agentType) {
|
|
34
|
+
return DEFAULT_DATABASE;
|
|
35
|
+
}
|
|
36
|
+
return AGENT_DB_MAP[agentType];
|
|
37
|
+
}
|
|
6
38
|
export class AgentBrain {
|
|
7
39
|
db;
|
|
8
40
|
embeddings;
|
|
9
41
|
agentId;
|
|
42
|
+
agentType;
|
|
10
43
|
collectionPath;
|
|
11
|
-
|
|
12
|
-
|
|
44
|
+
databaseName;
|
|
45
|
+
constructor(agentIdOrOptions, projectId) {
|
|
46
|
+
let options;
|
|
47
|
+
if (typeof agentIdOrOptions === 'string') {
|
|
48
|
+
options = { agentId: agentIdOrOptions, projectId };
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
options = agentIdOrOptions;
|
|
52
|
+
}
|
|
13
53
|
const config = getConfig();
|
|
14
|
-
|
|
15
|
-
|
|
54
|
+
this.agentId = options.agentId;
|
|
55
|
+
this.agentType = options.agentType || getAgentType();
|
|
56
|
+
this.databaseName = getDatabaseName(this.agentType);
|
|
57
|
+
const gcpProject = options.projectId || config.gcpProjectId || process.env.GOOGLE_CLOUD_PROJECT || 'tigerv0';
|
|
58
|
+
if (!firebaseInitialized && getApps().length === 0) {
|
|
16
59
|
initializeApp({ projectId: gcpProject });
|
|
60
|
+
firebaseInitialized = true;
|
|
17
61
|
}
|
|
18
|
-
|
|
62
|
+
const cacheKey = `${gcpProject}:${this.databaseName}`;
|
|
63
|
+
let db = firestoreCache.get(cacheKey);
|
|
64
|
+
if (!db) {
|
|
65
|
+
if (this.databaseName === DEFAULT_DATABASE) {
|
|
66
|
+
db = getFirestore();
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
db = getFirestore(this.databaseName);
|
|
70
|
+
}
|
|
71
|
+
firestoreCache.set(cacheKey, db);
|
|
72
|
+
}
|
|
73
|
+
this.db = db;
|
|
19
74
|
this.embeddings = new EmbeddingService({
|
|
20
75
|
projectId: gcpProject,
|
|
21
76
|
location: config.gcpLocation || 'europe-west1'
|
|
22
77
|
});
|
|
23
|
-
this.collectionPath = `${BRAIN_COLLECTION}/${agentId}/memories`;
|
|
78
|
+
this.collectionPath = `${BRAIN_COLLECTION}/${this.agentId}/memories`;
|
|
79
|
+
}
|
|
80
|
+
getDatabaseInfo() {
|
|
81
|
+
return {
|
|
82
|
+
agentType: this.agentType,
|
|
83
|
+
databaseName: this.databaseName,
|
|
84
|
+
};
|
|
24
85
|
}
|
|
25
86
|
async remember(content, tags = [], metadata) {
|
|
26
87
|
const embedding = await this.embeddings.embed(content);
|
|
@@ -65,6 +126,9 @@ export class AgentBrain {
|
|
|
65
126
|
.slice(0, limit);
|
|
66
127
|
}
|
|
67
128
|
async recallByTags(tags, limit = 10) {
|
|
129
|
+
if (tags.length === 0) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
68
132
|
const snapshot = await this.db
|
|
69
133
|
.collection(this.collectionPath)
|
|
70
134
|
.where('tags', 'array-contains-any', tags)
|