@forgehive/hive-sdk 0.1.3 → 0.1.5

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/src/index.ts CHANGED
@@ -1,9 +1,25 @@
1
1
  import axios from 'axios'
2
2
  import debug from 'debug'
3
+ import { v7 as uuidv7 } from 'uuid'
4
+ import fs from 'fs'
3
5
  import type { ExecutionRecord } from '@forgehive/task'
4
6
 
5
7
  const log = debug('hive-sdk')
6
8
 
9
+ interface ForgeConfig {
10
+ project: {
11
+ name: string
12
+ uuid: string
13
+ }
14
+ tasks: {
15
+ [taskName: string]: {
16
+ path: string
17
+ handler: string
18
+ uuid: string
19
+ }
20
+ }
21
+ }
22
+
7
23
  // Metadata interface
8
24
  export interface Metadata {
9
25
  [key: string]: string
@@ -15,10 +31,12 @@ export type { ExecutionRecord } from '@forgehive/task'
15
31
  // Configuration interface for HiveLogClient
16
32
  export interface HiveLogClientConfig {
17
33
  projectName: string
34
+ projectUuid?: string // Optional UUID for new endpoint
18
35
  apiKey?: string
19
36
  apiSecret?: string
20
37
  host?: string
21
38
  metadata?: Metadata
39
+ forgeConfigPath?: string // Optional path to forge.json file
22
40
  }
23
41
 
24
42
  // API Response Types
@@ -56,8 +74,10 @@ export class HiveLogClient {
56
74
  private apiSecret: string | null
57
75
  private host: string | null
58
76
  private projectName: string
77
+ private projectUuid: string | null
59
78
  private baseMetadata: Metadata
60
79
  private isInitialized: boolean
80
+ private forgeConfig: ForgeConfig | null = null
61
81
 
62
82
  constructor(config: HiveLogClientConfig) {
63
83
  const apiKey = config.apiKey || process.env.HIVE_API_KEY
@@ -65,6 +85,7 @@ export class HiveLogClient {
65
85
  const host = config.host || process.env.HIVE_HOST || 'https://www.forgehive.cloud'
66
86
 
67
87
  this.projectName = config.projectName
88
+ this.projectUuid = config.projectUuid || null
68
89
  this.baseMetadata = config.metadata || {}
69
90
 
70
91
  if (!apiKey || !apiSecret) {
@@ -80,12 +101,145 @@ export class HiveLogClient {
80
101
  this.isInitialized = true
81
102
  log('HiveLogClient initialized for project "%s" with host "%s"', config.projectName, host)
82
103
  }
104
+
105
+ // Load forge.json - use provided path or default to ./forge.json
106
+ const configPath = config.forgeConfigPath || './forge.json'
107
+ this.loadForgeConfig(configPath)
83
108
  }
84
109
 
85
110
  isActive(): boolean {
86
111
  return this.isInitialized
87
112
  }
88
113
 
114
+ private maskSecret(secret: string | null): string {
115
+ if (!secret || secret.length <= 8) {
116
+ return secret ? '****' : 'null'
117
+ }
118
+ const first4 = secret.slice(0, 4)
119
+ const last4 = secret.slice(-4)
120
+ const middle = '*'.repeat(secret.length - 8)
121
+ return `${first4}${middle}${last4}`
122
+ }
123
+
124
+ getConf(): Record<string, unknown> {
125
+ return {
126
+ projectName: this.projectName,
127
+ projectUuid: this.projectUuid,
128
+ host: this.host,
129
+ apiKey: this.maskSecret(this.apiKey),
130
+ apiSecret: this.maskSecret(this.apiSecret),
131
+ isInitialized: this.isInitialized,
132
+ baseMetadata: this.baseMetadata,
133
+ forgeConfig: this.forgeConfig
134
+ }
135
+ }
136
+
137
+ async testConfig(): Promise<{
138
+ success: boolean
139
+ teamName?: string
140
+ teamUuid?: string
141
+ userName?: string
142
+ projectName?: string
143
+ projectExists?: boolean
144
+ tasksVerified?: {
145
+ total: number
146
+ found: number
147
+ missing: string[]
148
+ }
149
+ error?: string
150
+ }> {
151
+ if (!this.isInitialized) {
152
+ return {
153
+ success: false,
154
+ error: 'Client not initialized - missing API credentials'
155
+ }
156
+ }
157
+
158
+ try {
159
+ // First verify credentials with /api/me
160
+ const meResponse = await axios.get(`${this.host}/api/me`, {
161
+ headers: {
162
+ 'Authorization': `Bearer ${this.apiKey}:${this.apiSecret}`,
163
+ 'Content-Type': 'application/json'
164
+ }
165
+ })
166
+
167
+ if (meResponse.status !== 200) {
168
+ const error = `Credential verification failed: HTTP ${meResponse.status}`
169
+ log('Failed to verify credentials: %s', error)
170
+ return { success: false, error }
171
+ }
172
+
173
+ const meData = meResponse.data
174
+ log('Successfully verified credentials for user "%s" in team "%s"', meData.user?.name, meData.team?.name)
175
+
176
+ // Then verify project exists if we have a projectUuid
177
+ let projectExists = false
178
+ let projectName: string | undefined
179
+ let tasksVerified: { total: number; found: number; missing: string[] } | undefined
180
+
181
+ if (this.projectUuid) {
182
+ try {
183
+ const projectResponse = await axios.get(`${this.host}/api/projects/${this.projectUuid}`, {
184
+ headers: {
185
+ 'Authorization': `Bearer ${this.apiKey}:${this.apiSecret}`,
186
+ 'Content-Type': 'application/json'
187
+ }
188
+ })
189
+
190
+ if (projectResponse.status === 200) {
191
+ projectExists = true
192
+ projectName = projectResponse.data.project?.projectName
193
+ log('Successfully verified project "%s" exists with UUID "%s"', projectName, this.projectUuid)
194
+
195
+ // Verify tasks if we have forge config
196
+ if (this.forgeConfig && this.forgeConfig.tasks) {
197
+ const localTasks = Object.keys(this.forgeConfig.tasks)
198
+ const remoteTasks = projectResponse.data.project?.tasks || []
199
+ const remoteTaskUuids = new Set(remoteTasks.map((task: { uuid: string }) => task.uuid))
200
+
201
+ const missing: string[] = []
202
+ let found = 0
203
+
204
+ for (const taskName of localTasks) {
205
+ const taskUuid = this.forgeConfig.tasks[taskName].uuid
206
+ if (remoteTaskUuids.has(taskUuid)) {
207
+ found++
208
+ } else {
209
+ missing.push(taskName)
210
+ }
211
+ }
212
+
213
+ tasksVerified = {
214
+ total: localTasks.length,
215
+ found,
216
+ missing
217
+ }
218
+
219
+ log('Task verification: %d/%d tasks found, missing: %s', found, localTasks.length, missing.join(', '))
220
+ }
221
+ }
222
+ } catch (projectError) {
223
+ log('Project verification failed for UUID "%s": %s', this.projectUuid, projectError instanceof Error ? projectError.message : String(projectError))
224
+ }
225
+ }
226
+
227
+ return {
228
+ success: true,
229
+ teamName: meData.team?.name,
230
+ teamUuid: meData.team?.uuid,
231
+ userName: meData.user?.name,
232
+ projectName,
233
+ projectExists: this.projectUuid ? projectExists : undefined,
234
+ tasksVerified
235
+ }
236
+ } catch (e) {
237
+ const error = e instanceof Error ? e.message : 'Network error'
238
+ log('Error during config test: %s', error)
239
+ return { success: false, error }
240
+ }
241
+ }
242
+
89
243
  private mergeMetadata(record: ExecutionRecord, sendLogMetadata?: Metadata): Metadata {
90
244
  // Start with base metadata from client
91
245
  let finalMetadata = { ...this.baseMetadata }
@@ -112,6 +266,9 @@ export class HiveLogClient {
112
266
  return 'silent'
113
267
  }
114
268
 
269
+ // Deprecation warning for legacy endpoint
270
+ log('DEPRECATION WARNING: sendLog() is deprecated. Use sendLogByUuid() with project and task UUIDs for enhanced features and better performance.')
271
+
115
272
  try {
116
273
  const logsUrl = `${this.host}/api/tasks/log-ingest`
117
274
  log('Sending log for task "%s" to %s', taskName, logsUrl)
@@ -121,7 +278,7 @@ export class HiveLogClient {
121
278
  // Merge metadata with priority: sendLog > record.metadata > client
122
279
  const finalMetadata = this.mergeMetadata(record, metadata)
123
280
 
124
- // Create logItem with merged metadata
281
+ // Create logItem with merged metadata (no UUID generation for legacy method)
125
282
  const logItem = {
126
283
  ...record,
127
284
  taskName,
@@ -154,6 +311,62 @@ export class HiveLogClient {
154
311
  }
155
312
  }
156
313
 
314
+ async sendLogByUuid(record: ExecutionRecord, taskUuid: string, metadata?: Metadata): Promise<'success' | 'error' | 'silent' | LogApiSuccess> {
315
+ if (!this.isInitialized) {
316
+ log('Silent mode: Skipping sendLogByUuid for task UUID "%s" - client not initialized', taskUuid)
317
+ return 'silent'
318
+ }
319
+
320
+ if (!this.projectUuid) {
321
+ log('Error: sendLogByUuid requires projectUuid to be set in client config')
322
+ return 'error'
323
+ }
324
+
325
+ try {
326
+ const logsUrl = `${this.host}/api/log-ingest`
327
+ log('Sending log for task UUID "%s" to %s', taskUuid, logsUrl)
328
+
329
+ const authToken = `${this.apiKey}:${this.apiSecret}`
330
+
331
+ // Merge metadata with priority: sendLog > record.metadata > client
332
+ const finalMetadata = this.mergeMetadata(record, metadata)
333
+
334
+ // Ensure execution record has a UUID - generate one if missing
335
+ const recordWithUuid = {
336
+ ...record,
337
+ uuid: record.uuid || uuidv7(),
338
+ metadata: finalMetadata
339
+ }
340
+
341
+ // Create logItem with merged metadata and UUID
342
+ const logItem = recordWithUuid
343
+
344
+ const response = await axios.post(logsUrl, {
345
+ projectUuid: this.projectUuid,
346
+ taskUuid,
347
+ logItem: JSON.stringify(logItem)
348
+ }, {
349
+ headers: {
350
+ Authorization: `Bearer ${authToken}`,
351
+ 'Content-Type': 'application/json'
352
+ }
353
+ })
354
+
355
+ log('Success: Sent log for task UUID "%s"', taskUuid)
356
+
357
+ // Return the full response data if available
358
+ if (response.data && typeof response.data === 'object' && 'uuid' in response.data) {
359
+ return response.data as LogApiSuccess
360
+ }
361
+
362
+ return 'success'
363
+ } catch (e) {
364
+ const error = e as Error
365
+ log('Error: Failed to send log for task UUID "%s": %s', taskUuid, error.message)
366
+ return 'error'
367
+ }
368
+ }
369
+
157
370
  getListener(): (record: ExecutionRecord) => Promise<void> {
158
371
  return async (record: ExecutionRecord) => {
159
372
  await this.sendLog(record)
@@ -217,6 +430,58 @@ export class HiveLogClient {
217
430
  return false
218
431
  }
219
432
  }
433
+
434
+ private loadForgeConfig(configPath: string): void {
435
+ try {
436
+ if (fs.existsSync(configPath)) {
437
+ const configContent = fs.readFileSync(configPath, 'utf8')
438
+ this.forgeConfig = JSON.parse(configContent) as ForgeConfig
439
+ log('Found forge.json configuration at %s', configPath)
440
+ } else {
441
+ log('No forge.json configuration found at %s', configPath)
442
+ }
443
+ } catch (error) {
444
+ log('Error loading forge.json: %s', error instanceof Error ? error.message : String(error))
445
+ }
446
+ }
447
+
448
+ private getTaskUUID(taskName: string): string | null {
449
+ if (!this.forgeConfig) {
450
+ log('No forge.json configuration loaded, cannot get UUID for task "%s"', taskName)
451
+ return null
452
+ }
453
+
454
+ const task = this.forgeConfig.tasks[taskName]
455
+ if (!task) {
456
+ log('Task "%s" not found in forge.json configuration', taskName)
457
+ return null
458
+ }
459
+
460
+ log('Found UUID "%s" for task "%s"', task.uuid, taskName)
461
+ return task.uuid
462
+ }
463
+
464
+ async sendLogByName(taskName: string, record: ExecutionRecord, metadata?: Metadata): Promise<'success' | 'error' | 'silent' | LogApiSuccess> {
465
+ if (!this.isInitialized) {
466
+ log('Silent mode: Skipping sendLogByName for task "%s" - client not initialized', taskName)
467
+ return 'silent'
468
+ }
469
+
470
+ if (!this.projectUuid) {
471
+ log('Error: sendLogByName requires projectUuid to be set in client config')
472
+ return 'error'
473
+ }
474
+
475
+ const taskUuid = this.getTaskUUID(taskName)
476
+ if (!taskUuid) {
477
+ log('Error: Cannot find UUID for task "%s" in forge.json', taskName)
478
+ return 'error'
479
+ }
480
+
481
+ // Use the existing sendLogByUuid method
482
+ log('Sending log for task "%s" with uuid "%s"', taskName, taskUuid)
483
+ return await this.sendLogByUuid(record, taskUuid, metadata)
484
+ }
220
485
  }
221
486
 
222
487
  export const createHiveLogClient = (config: HiveLogClientConfig): HiveLogClient => {
@@ -271,10 +536,97 @@ export class HiveClient {
271
536
  log('HiveClient initialized for project "%s" with host "%s"', config.projectUuid, host)
272
537
  }
273
538
 
274
- async invoke(taskName: string, payload: unknown): Promise<InvokeResult | null> {
539
+ private maskSecret(secret: string): string {
540
+ if (secret.length <= 8) {
541
+ return '****'
542
+ }
543
+ const first4 = secret.slice(0, 4)
544
+ const last4 = secret.slice(-4)
545
+ const middle = '*'.repeat(secret.length - 8)
546
+ return `${first4}${middle}${last4}`
547
+ }
548
+
549
+ getConf(): Record<string, unknown> {
550
+ return {
551
+ projectUuid: this.projectUuid,
552
+ host: this.host,
553
+ apiKey: this.maskSecret(this.apiKey),
554
+ apiSecret: this.maskSecret(this.apiSecret)
555
+ }
556
+ }
557
+
558
+ async testConfig(): Promise<{
559
+ success: boolean
560
+ teamName?: string
561
+ teamUuid?: string
562
+ userName?: string
563
+ projectName?: string
564
+ projectExists?: boolean
565
+ tasksVerified?: {
566
+ total: number
567
+ found: number
568
+ missing: string[]
569
+ }
570
+ error?: string
571
+ }> {
275
572
  try {
276
- const invokeUrl = `${this.host}/api/project/${this.projectUuid}/task/${taskName}/invoke`
277
- log('Invoking task "%s" at %s', taskName, invokeUrl)
573
+ // First verify credentials with /api/me
574
+ const meResponse = await axios.get(`${this.host}/api/me`, {
575
+ headers: {
576
+ 'Authorization': `Bearer ${this.apiKey}:${this.apiSecret}`,
577
+ 'Content-Type': 'application/json'
578
+ }
579
+ })
580
+
581
+ if (meResponse.status !== 200) {
582
+ const error = `Credential verification failed: HTTP ${meResponse.status}`
583
+ log('Failed to verify credentials: %s', error)
584
+ return { success: false, error }
585
+ }
586
+
587
+ const meData = meResponse.data
588
+ log('Successfully verified credentials for user "%s" in team "%s"', meData.user?.name, meData.team?.name)
589
+
590
+ // Then verify project exists
591
+ let projectExists = false
592
+ let projectName: string | undefined
593
+
594
+ try {
595
+ const projectResponse = await axios.get(`${this.host}/api/projects/${this.projectUuid}`, {
596
+ headers: {
597
+ 'Authorization': `Bearer ${this.apiKey}:${this.apiSecret}`,
598
+ 'Content-Type': 'application/json'
599
+ }
600
+ })
601
+
602
+ if (projectResponse.status === 200) {
603
+ projectExists = true
604
+ projectName = projectResponse.data.project?.projectName
605
+ log('Successfully verified project "%s" exists with UUID "%s"', projectName, this.projectUuid)
606
+ }
607
+ } catch (projectError) {
608
+ log('Project verification failed for UUID "%s": %s', this.projectUuid, projectError instanceof Error ? projectError.message : String(projectError))
609
+ }
610
+
611
+ return {
612
+ success: true,
613
+ teamName: meData.team?.name,
614
+ teamUuid: meData.team?.uuid,
615
+ userName: meData.user?.name,
616
+ projectName,
617
+ projectExists
618
+ }
619
+ } catch (e) {
620
+ const error = e instanceof Error ? e.message : 'Network error'
621
+ log('Error during config test: %s', error)
622
+ return { success: false, error }
623
+ }
624
+ }
625
+
626
+ async invoke(taskUuid: string, payload: unknown): Promise<InvokeResult | null> {
627
+ try {
628
+ const invokeUrl = `${this.host}/api/projects/${this.projectUuid}/tasks/${taskUuid}/invoke`
629
+ log('Invoking task UUID "%s" at %s', taskUuid, invokeUrl)
278
630
 
279
631
  const authToken = `${this.apiKey}:${this.apiSecret}`
280
632
 
@@ -287,11 +639,11 @@ export class HiveClient {
287
639
  }
288
640
  })
289
641
 
290
- log('Success: Invoked task "%s"', taskName)
642
+ log('Success: Invoked task UUID "%s"', taskUuid)
291
643
  return response.data as InvokeResult
292
644
  } catch (e) {
293
645
  const error = e as Error
294
- log('Error: Failed to invoke task "%s": %s', taskName, error.message)
646
+ log('Error: Failed to invoke task UUID "%s": %s', taskUuid, error.message)
295
647
 
296
648
  // Check if it's an axios error with response data
297
649
  if (axios.isAxiosError(error) && error.response?.data) {
@@ -307,3 +659,41 @@ export const createHiveClient = (config: HiveClientConfig): HiveClient => {
307
659
  log('Creating HiveClient for project "%s"', config.projectUuid)
308
660
  return new HiveClient(config)
309
661
  }
662
+
663
+ /**
664
+ * Create a HiveLogClient from forge.json configuration
665
+ * @param forgeConfigPath Path to forge.json file (defaults to './forge.json')
666
+ * @param additionalConfig Additional config options to override forge.json values
667
+ * @returns HiveLogClient configured from forge.json
668
+ */
669
+ export const createClientFromForgeConf = (
670
+ forgeConfigPath: string = './forge.json',
671
+ additionalConfig: Partial<HiveLogClientConfig> = {}
672
+ ): HiveLogClient => {
673
+ log('Creating HiveLogClient from forge.json at "%s"', forgeConfigPath)
674
+
675
+ let forgeConfig: ForgeConfig | null = null
676
+
677
+ try {
678
+ if (fs.existsSync(forgeConfigPath)) {
679
+ const configContent = fs.readFileSync(forgeConfigPath, 'utf8')
680
+ forgeConfig = JSON.parse(configContent) as ForgeConfig
681
+ log('Loaded forge.json configuration from %s', forgeConfigPath)
682
+ } else {
683
+ log('No forge.json found at %s', forgeConfigPath)
684
+ throw new Error(`forge.json not found at ${forgeConfigPath}`)
685
+ }
686
+ } catch (error) {
687
+ log('Error loading forge.json: %s', error instanceof Error ? error.message : String(error))
688
+ throw error
689
+ }
690
+
691
+ const config: HiveLogClientConfig = {
692
+ projectName: forgeConfig.project.name,
693
+ projectUuid: forgeConfig.project.uuid,
694
+ forgeConfigPath,
695
+ ...additionalConfig // Allow overriding any config values
696
+ }
697
+
698
+ return new HiveLogClient(config)
699
+ }