@simonfestl/husky-cli 1.23.0 → 1.25.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.
@@ -317,4 +317,43 @@ gotessCommand
317
317
  process.exit(1);
318
318
  }
319
319
  });
320
+ gotessCommand
321
+ .command("upload <file>")
322
+ .description("Upload invoice PDF to Gotess")
323
+ .option("-s, --sender <name>", "Sender/vendor name (required)")
324
+ .option("-a, --amount <amount>", "Invoice amount", parseFloat)
325
+ .option("-d, --date <date>", "Invoice date (YYYY-MM-DD)")
326
+ .option("--link <transactionId>", "Auto-link to transaction after upload")
327
+ .action(async (file, options) => {
328
+ try {
329
+ if (!options.sender) {
330
+ console.error("āœ— Sender name is required (-s, --sender)");
331
+ process.exit(1);
332
+ }
333
+ const client = GotessClient.fromConfig();
334
+ console.log(`\n šŸ“¤ Uploading ${file}...`);
335
+ const invoice = await client.uploadInvoicePdf(file, {
336
+ senderName: options.sender,
337
+ amount: options.amount,
338
+ invoiceDate: options.date,
339
+ });
340
+ console.log(` āœ“ Invoice created: ${invoice.id}`);
341
+ console.log(` Filename: ${invoice.filename}`);
342
+ console.log(` Sender: ${invoice.sender_name}`);
343
+ if (invoice.amount) {
344
+ console.log(` Amount: ${invoice.amount.toFixed(2)} EUR`);
345
+ }
346
+ // Auto-link if transaction ID provided
347
+ if (options.link) {
348
+ console.log(`\n šŸ”— Linking to transaction ${options.link}...`);
349
+ await client.linkInvoice(options.link, invoice.id);
350
+ console.log(` āœ“ Linked successfully`);
351
+ }
352
+ console.log("");
353
+ }
354
+ catch (error) {
355
+ console.error("Error:", error.message);
356
+ process.exit(1);
357
+ }
358
+ });
320
359
  export default gotessCommand;
@@ -698,6 +698,112 @@ brainCommand
698
698
  process.exit(1);
699
699
  }
700
700
  });
701
+ // ============================================================================
702
+ // Auto-Brain: Hook Integration Commands
703
+ // ============================================================================
704
+ brainCommand
705
+ .command("auto-recall <prompt>")
706
+ .description("Automatically search brain for relevant memories based on user prompt (for hook integration)")
707
+ .option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
708
+ .option("-l, --limit <num>", "Max results", "3")
709
+ .option("-m, --min-score <score>", "Minimum similarity score (0-1)", "0.6")
710
+ .option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
711
+ .option("--format <format>", "Output format (hint, json, markdown)", "hint")
712
+ .option("--quiet", "Suppress output if no results found")
713
+ .action(async (prompt, options) => {
714
+ try {
715
+ // Skip very short prompts (likely not meaningful queries)
716
+ if (prompt.length < 10) {
717
+ if (!options.quiet) {
718
+ console.log("");
719
+ }
720
+ return;
721
+ }
722
+ const brain = createBrain(options.agent, options.agentType);
723
+ const results = await brain.recall(prompt, parseInt(options.limit, 10), parseFloat(options.minScore));
724
+ if (results.length === 0) {
725
+ if (!options.quiet) {
726
+ console.log("");
727
+ }
728
+ return;
729
+ }
730
+ if (options.format === "json") {
731
+ console.log(JSON.stringify({ success: true, results }));
732
+ return;
733
+ }
734
+ if (options.format === "markdown") {
735
+ console.log("\n## 🧠 Brain Recall - Relevante Erinnerungen\n");
736
+ for (const r of results) {
737
+ const tags = r.memory.tags.length > 0 ? ` (Tags: ${r.memory.tags.join(", ")})` : "";
738
+ console.log(`- **[${(r.score * 100).toFixed(0)}%]** ${r.memory.content.slice(0, 150)}...${tags}`);
739
+ }
740
+ console.log("");
741
+ return;
742
+ }
743
+ // Default: hint format (compact, for hooks)
744
+ console.log("\n🧠 BRAIN RECALL - Relevante Erinnerungen:");
745
+ console.log("─".repeat(50));
746
+ for (const r of results) {
747
+ const tags = r.memory.tags.length > 0 ? ` [${r.memory.tags.join(", ")}]` : "";
748
+ console.log(` [${(r.score * 100).toFixed(0)}%] ${r.memory.content.slice(0, 120)}...${tags}`);
749
+ }
750
+ console.log("─".repeat(50));
751
+ console.log("");
752
+ }
753
+ catch (error) {
754
+ // Fail silently for hook integration - don't block agent workflow
755
+ if (options.format === "json") {
756
+ console.log(JSON.stringify({ success: false, error: error.message }));
757
+ }
758
+ }
759
+ });
760
+ brainCommand
761
+ .command("auto-remember <content>")
762
+ .description("Automatically store a learning/insight (for hook integration after task completion)")
763
+ .option("-a, --agent <id>", "Agent ID", DEFAULT_AGENT)
764
+ .option("--agent-type <type>", `Agent type for database selection (${AGENT_TYPES.join(", ")})`)
765
+ .option("--task-id <id>", "Associated task ID")
766
+ .option("-t, --tags <tags>", "Comma-separated tags", "auto-learning")
767
+ .option("--source <source>", "Source of learning (task, conversation, tool)", "task")
768
+ .option("--json", "Output as JSON")
769
+ .action(async (content, options) => {
770
+ try {
771
+ // Skip very short content
772
+ if (content.length < 20) {
773
+ if (options.json) {
774
+ console.log(JSON.stringify({ success: false, error: "Content too short" }));
775
+ }
776
+ return;
777
+ }
778
+ const brain = createBrain(options.agent, options.agentType);
779
+ const tags = options.tags.split(",").map((t) => t.trim());
780
+ // Add source and task info to tags
781
+ if (options.source && !tags.includes(options.source)) {
782
+ tags.push(options.source);
783
+ }
784
+ if (options.taskId) {
785
+ tags.push(`task:${options.taskId}`);
786
+ }
787
+ const id = await brain.remember(content, tags, {
788
+ source: options.source,
789
+ taskId: options.taskId,
790
+ autoGenerated: true,
791
+ timestamp: new Date().toISOString(),
792
+ });
793
+ if (options.json) {
794
+ console.log(JSON.stringify({ success: true, id, tags }));
795
+ }
796
+ else {
797
+ console.log(` āœ“ Auto-Remember: ${id.slice(0, 8)}... [${tags.join(", ")}]`);
798
+ }
799
+ }
800
+ catch (error) {
801
+ if (options.json) {
802
+ console.log(JSON.stringify({ success: false, error: error.message }));
803
+ }
804
+ // Fail silently for hook integration
805
+ }
806
+ });
701
807
  brainCommand
702
808
  .command("kb-stats <kb>")
703
809
  .description("Show statistics for a knowledge base")
@@ -98,30 +98,54 @@ chatCommand
98
98
  });
99
99
  chatCommand
100
100
  .command("send <message>")
101
- .description("Send a message as supervisor")
101
+ .description("Send a message (to Google Chat if --space provided, otherwise to dashboard chat)")
102
102
  .option("--task-id <id>", "Link to a specific task")
103
+ .option("--space <id>", "Google Chat space ID (e.g., spaces/ABC123 or DM space)")
104
+ .option("--thread <name>", "Reply in Google Chat thread")
103
105
  .action(async (message, options) => {
104
106
  const config = getConfig();
105
- if (!config.apiUrl) {
107
+ const huskyApiUrl = getHuskyApiUrl();
108
+ if (!huskyApiUrl) {
106
109
  console.error("Error: API URL not configured.");
107
110
  process.exit(1);
108
111
  }
109
112
  try {
110
- const res = await fetch(`${config.apiUrl}/api/chat/supervisor`, {
111
- method: "POST",
112
- headers: {
113
- "Content-Type": "application/json",
114
- ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
115
- },
116
- body: JSON.stringify({
117
- content: message,
118
- taskId: options.taskId,
119
- }),
120
- });
121
- if (!res.ok) {
122
- throw new Error(`API error: ${res.status}`);
113
+ if (options.space) {
114
+ const res = await fetch(`${huskyApiUrl}/api/google-chat/send`, {
115
+ method: "POST",
116
+ headers: {
117
+ "Content-Type": "application/json",
118
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
119
+ },
120
+ body: JSON.stringify({
121
+ text: message,
122
+ spaceName: options.space,
123
+ threadName: options.thread,
124
+ }),
125
+ });
126
+ if (!res.ok) {
127
+ const error = await res.text();
128
+ throw new Error(`API error: ${res.status} - ${error}`);
129
+ }
130
+ console.log("āœ… Message sent to Google Chat.");
131
+ }
132
+ else {
133
+ const res = await fetch(`${huskyApiUrl}/api/chat/supervisor`, {
134
+ method: "POST",
135
+ headers: {
136
+ "Content-Type": "application/json",
137
+ ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
138
+ },
139
+ body: JSON.stringify({
140
+ content: message,
141
+ taskId: options.taskId,
142
+ }),
143
+ });
144
+ if (!res.ok) {
145
+ throw new Error(`API error: ${res.status}`);
146
+ }
147
+ console.log("Message sent to dashboard chat.");
123
148
  }
124
- console.log("Message sent.");
125
149
  }
126
150
  catch (error) {
127
151
  console.error("Error sending message:", error);
@@ -43,6 +43,7 @@ interface Config {
43
43
  wattizPassword?: string;
44
44
  wattizBaseUrl?: string;
45
45
  wattizLanguage?: string;
46
+ gcsBucket?: string;
46
47
  }
47
48
  export declare function getConfig(): Config;
48
49
  export declare function saveConfig(config: Config): void;
@@ -81,6 +81,19 @@ export declare class GotessClient {
81
81
  }): Promise<GotessInvoice[]>;
82
82
  linkInvoice(transactionId: string, invoiceId: string): Promise<void>;
83
83
  markProofless(transactionId: string): Promise<void>;
84
+ /**
85
+ * Create an invoice record in Gotess
86
+ * Used for auto-upload from invoice extraction
87
+ */
88
+ createInvoice(invoice: {
89
+ invoiceDate?: string;
90
+ amount?: number;
91
+ senderName: string;
92
+ filename: string;
93
+ gcsUri?: string;
94
+ s3Uri?: string;
95
+ bookId?: string;
96
+ }): Promise<GotessInvoice>;
84
97
  autoMatch(bookId?: string): Promise<{
85
98
  matched: Array<{
86
99
  transaction: GotessTransaction;
@@ -93,5 +106,19 @@ export declare class GotessClient {
93
106
  setBookId(id: string): void;
94
107
  getAccessToken(): string | undefined;
95
108
  getBookId(): string | undefined;
109
+ /**
110
+ * Upload a PDF file to Supabase Storage and create invoice record
111
+ * This uploads directly to Gotess's storage system
112
+ */
113
+ uploadInvoicePdf(filePath: string, options: {
114
+ invoiceDate?: string;
115
+ amount?: number;
116
+ senderName: string;
117
+ bookId?: string;
118
+ }): Promise<GotessInvoice>;
119
+ /**
120
+ * Get signed URL for private file access
121
+ */
122
+ getSignedUrl(storagePath: string, expiresIn?: number): Promise<string>;
96
123
  }
97
124
  export default GotessClient;
@@ -137,6 +137,34 @@ export class GotessClient {
137
137
  if (!res.ok)
138
138
  throw new Error('Failed to mark proofless');
139
139
  }
140
+ /**
141
+ * Create an invoice record in Gotess
142
+ * Used for auto-upload from invoice extraction
143
+ */
144
+ async createInvoice(invoice) {
145
+ const bid = invoice.bookId || this.bookId;
146
+ if (!bid)
147
+ throw new Error('Book ID required');
148
+ const body = {
149
+ invoice_date: invoice.invoiceDate,
150
+ amount: invoice.amount,
151
+ sender_name: invoice.senderName,
152
+ filename: invoice.filename,
153
+ s3_uri: invoice.s3Uri || invoice.gcsUri, // Use GCS URI as s3_uri field
154
+ book_id: bid,
155
+ };
156
+ const res = await fetch(`${SUPABASE_URL}/rest/v1/Invoices`, {
157
+ method: 'POST',
158
+ headers: { ...this.headers, 'Prefer': 'return=representation' },
159
+ body: JSON.stringify(body),
160
+ });
161
+ if (!res.ok) {
162
+ const err = await res.json().catch(() => ({}));
163
+ throw new Error(err.message || 'Failed to create invoice');
164
+ }
165
+ const data = await res.json();
166
+ return Array.isArray(data) ? data[0] : data;
167
+ }
140
168
  async autoMatch(bookId) {
141
169
  const bid = bookId || this.bookId;
142
170
  if (!bid)
@@ -198,5 +226,65 @@ export class GotessClient {
198
226
  getBookId() {
199
227
  return this.bookId;
200
228
  }
229
+ /**
230
+ * Upload a PDF file to Supabase Storage and create invoice record
231
+ * This uploads directly to Gotess's storage system
232
+ */
233
+ async uploadInvoicePdf(filePath, options) {
234
+ const bid = options.bookId || this.bookId;
235
+ if (!bid)
236
+ throw new Error('Book ID required');
237
+ // Read file
238
+ const fs = await import('fs');
239
+ const path = await import('path');
240
+ if (!fs.existsSync(filePath)) {
241
+ throw new Error(`File not found: ${filePath}`);
242
+ }
243
+ const fileBuffer = fs.readFileSync(filePath);
244
+ const filename = path.basename(filePath);
245
+ // Generate unique storage path
246
+ const timestamp = Date.now();
247
+ const storagePath = `invoices/${bid}/${timestamp}-${filename}`;
248
+ // Upload to Supabase Storage
249
+ const uploadRes = await fetch(`${SUPABASE_URL}/storage/v1/object/invoices/${storagePath}`, {
250
+ method: 'POST',
251
+ headers: {
252
+ ...this.headers,
253
+ 'Content-Type': 'application/pdf',
254
+ },
255
+ body: fileBuffer,
256
+ });
257
+ if (!uploadRes.ok) {
258
+ const err = await uploadRes.json().catch(() => ({}));
259
+ throw new Error(err.message || `Upload failed: ${uploadRes.status}`);
260
+ }
261
+ // Get the public URL
262
+ const s3Uri = `${SUPABASE_URL}/storage/v1/object/public/invoices/${storagePath}`;
263
+ // Create invoice record
264
+ const invoice = await this.createInvoice({
265
+ invoiceDate: options.invoiceDate,
266
+ amount: options.amount,
267
+ senderName: options.senderName,
268
+ filename,
269
+ s3Uri,
270
+ bookId: bid,
271
+ });
272
+ return invoice;
273
+ }
274
+ /**
275
+ * Get signed URL for private file access
276
+ */
277
+ async getSignedUrl(storagePath, expiresIn = 3600) {
278
+ const res = await fetch(`${SUPABASE_URL}/storage/v1/object/sign/invoices/${storagePath}`, {
279
+ method: 'POST',
280
+ headers: this.headers,
281
+ body: JSON.stringify({ expiresIn }),
282
+ });
283
+ if (!res.ok) {
284
+ throw new Error('Failed to generate signed URL');
285
+ }
286
+ const data = await res.json();
287
+ return `${SUPABASE_URL}/storage/v1${data.signedURL}`;
288
+ }
201
289
  }
202
290
  export default GotessClient;
@@ -11,3 +11,4 @@ export type { EmbeddingConfig, EmbeddingResult } from './embeddings.js';
11
11
  export * from './billbee-types.js';
12
12
  export * from './zendesk-types.js';
13
13
  export * from './seatable-types.js';
14
+ export { GotessClient } from './gotess.js';
@@ -9,3 +9,4 @@ export { EmbeddingService, EMBEDDING_MODELS } from './embeddings.js';
9
9
  export * from './billbee-types.js';
10
10
  export * from './zendesk-types.js';
11
11
  export * from './seatable-types.js';
12
+ export { GotessClient } from './gotess.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonfestl/husky-cli",
3
- "version": "1.23.0",
3
+ "version": "1.25.1",
4
4
  "description": "CLI for Huskyv0 Task Orchestration with Claude Agent SDK",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,13 +20,16 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@anthropic-ai/claude-code": "^1.0.0",
23
+ "@google-cloud/storage": "^7.14.0",
23
24
  "@google-cloud/vertexai": "^1.10.0",
24
25
  "@google/generative-ai": "^0.24.1",
25
26
  "@inquirer/prompts": "^8.1.0",
27
+ "@types/uuid": "^10.0.0",
26
28
  "commander": "^12.1.0",
27
29
  "firebase-admin": "^13.6.0",
28
30
  "playwright": "^1.57.0",
29
31
  "sharp": "^0.34.5",
32
+ "uuid": "^13.0.0",
30
33
  "youtube-transcript": "^1.2.1",
31
34
  "zod": "^4.3.5"
32
35
  },