@simonfestl/husky-cli 1.24.0 → 1.25.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.
@@ -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;
@@ -98,42 +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("--dm <user>", "Send as direct message to user")
104
- .option("--space <name>", "Target Google Chat space (e.g., spaces/ABC123)")
103
+ .option("--space <id>", "Google Chat space ID (e.g., spaces/ABC123 or DM space)")
104
+ .option("--thread <name>", "Reply in Google Chat thread")
105
105
  .action(async (message, options) => {
106
106
  const config = getConfig();
107
- if (!config.apiUrl) {
107
+ const huskyApiUrl = getHuskyApiUrl();
108
+ if (!huskyApiUrl) {
108
109
  console.error("Error: API URL not configured.");
109
110
  process.exit(1);
110
111
  }
111
112
  try {
112
- let endpoint = `${config.apiUrl}/api/chat/supervisor`;
113
- let payload = {
114
- content: message,
115
- ...(options.taskId && { taskId: options.taskId }),
116
- };
117
- // If --space is provided, use Google Chat API instead
118
113
  if (options.space) {
119
- endpoint = `${config.apiUrl}/api/google-chat/send`;
120
- payload = {
121
- text: message,
122
- spaceName: options.space,
123
- };
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.");
124
131
  }
125
- const res = await fetch(endpoint, {
126
- method: "POST",
127
- headers: {
128
- "Content-Type": "application/json",
129
- ...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
130
- },
131
- body: JSON.stringify(payload),
132
- });
133
- if (!res.ok) {
134
- throw new Error(`API error: ${res.status}`);
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.");
135
148
  }
136
- console.log(options.space ? "✅ Message sent to Google Chat." : "Message sent.");
137
149
  }
138
150
  catch (error) {
139
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.24.0",
3
+ "version": "1.25.2",
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
  },