@jagit/shared 0.0.1 → 0.0.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.
package/dist/config.d.ts CHANGED
@@ -8,6 +8,8 @@ export declare function parseConfig(env: RawEnv): {
8
8
  approvalTimeoutMs: number;
9
9
  acpRequestTimeoutMs: number;
10
10
  anthropicApiKey: string;
11
+ anthropicAuthToken: string;
12
+ anthropicBaseUrl: string | undefined;
11
13
  telegramBotToken: string;
12
14
  publicBaseUrl: string;
13
15
  apiPort: number;
package/dist/config.js CHANGED
@@ -7,15 +7,20 @@ const Schema = z.object({
7
7
  MAX_RETRIES: z.coerce.number().int().nonnegative(),
8
8
  APPROVAL_TIMEOUT_MS: z.coerce.number().int().positive(),
9
9
  ACP_REQUEST_TIMEOUT_MS: z.coerce.number().int().positive().default(600_000),
10
- ANTHROPIC_API_KEY: z.string().min(1),
10
+ ANTHROPIC_API_KEY: z.string().min(1).optional(),
11
+ ANTHROPIC_AUTH_TOKEN: z.string().min(1).optional(),
12
+ ANTHROPIC_BASE_URL: z.string().url().optional(),
11
13
  TELEGRAM_BOT_TOKEN: z.string().min(1),
12
14
  PUBLIC_BASE_URL: z.string().url(),
13
15
  API_PORT: z.coerce.number().int().positive().default(3000),
14
16
  API_WEBHOOK_SECRET: z.string().min(1),
15
17
  DASHBOARD_API_TOKEN: z.string().min(1),
18
+ }).refine((data) => !!(data.ANTHROPIC_AUTH_TOKEN || data.ANTHROPIC_API_KEY), {
19
+ message: "Either ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY must be provided",
16
20
  });
17
21
  export function parseConfig(env) {
18
22
  const p = Schema.parse(env);
23
+ const anthropicAuthToken = p.ANTHROPIC_AUTH_TOKEN || p.ANTHROPIC_API_KEY || "";
19
24
  return {
20
25
  databaseUrl: p.DATABASE_URL,
21
26
  redisUrl: p.REDIS_URL,
@@ -24,7 +29,9 @@ export function parseConfig(env) {
24
29
  maxRetries: p.MAX_RETRIES,
25
30
  approvalTimeoutMs: p.APPROVAL_TIMEOUT_MS,
26
31
  acpRequestTimeoutMs: p.ACP_REQUEST_TIMEOUT_MS,
27
- anthropicApiKey: p.ANTHROPIC_API_KEY,
32
+ anthropicApiKey: anthropicAuthToken,
33
+ anthropicAuthToken,
34
+ anthropicBaseUrl: p.ANTHROPIC_BASE_URL,
28
35
  telegramBotToken: p.TELEGRAM_BOT_TOKEN,
29
36
  publicBaseUrl: p.PUBLIC_BASE_URL,
30
37
  apiPort: p.API_PORT,
@@ -29,7 +29,8 @@ export declare const AnthropicCredentialSchema: z.ZodObject<{
29
29
  baseUrl: z.ZodOptional<z.ZodString>;
30
30
  }, z.core.$catchall<z.ZodString>>>>;
31
31
  secrets: z.ZodObject<{
32
- apiKey: z.ZodString;
32
+ apiKey: z.ZodOptional<z.ZodString>;
33
+ authToken: z.ZodOptional<z.ZodString>;
33
34
  }, z.core.$strip>;
34
35
  }, z.core.$strip>;
35
36
  export declare const TelegramCredentialSchema: z.ZodObject<{
@@ -61,7 +62,8 @@ export declare function validateCredential(kind: CredentialKind, input: unknown)
61
62
  baseUrl?: string | undefined;
62
63
  };
63
64
  secrets: {
64
- apiKey: string;
65
+ apiKey?: string | undefined;
66
+ authToken?: string | undefined;
65
67
  };
66
68
  } | {
67
69
  meta: {
@@ -32,8 +32,9 @@ export const AnthropicCredentialSchema = z.object({
32
32
  .optional()
33
33
  .default({}),
34
34
  secrets: z.object({
35
- apiKey: z.string().min(1),
36
- }),
35
+ apiKey: z.string().min(1).optional(),
36
+ authToken: z.string().min(1).optional(),
37
+ }).refine((s) => !!(s.apiKey || s.authToken), { message: "Either apiKey or authToken is required" }),
37
38
  });
38
39
  export const TelegramCredentialSchema = z.object({
39
40
  meta: z
@@ -55,7 +56,7 @@ export function credentialSecretKeys(kind) {
55
56
  case "gitlab":
56
57
  return ["token"];
57
58
  case "anthropic":
58
- return ["apiKey"];
59
+ return ["authToken", "apiKey"];
59
60
  case "telegram":
60
61
  return ["botToken"];
61
62
  }
package/dist/events.js CHANGED
@@ -6,7 +6,11 @@ export const controlChannel = (jobId) => `control:${jobId}`;
6
6
  /** Channel name for global approval events (SSE + worker graph) */
7
7
  export const approvalsChannel = "approvals";
8
8
  export function makeRedis(url) {
9
- return new Redis(url, { maxRetriesPerRequest: null, lazyConnect: false });
9
+ const client = new Redis(url, { maxRetriesPerRequest: null, lazyConnect: false });
10
+ client.on("error", (err) => {
11
+ console.error("Redis client error:", err.message);
12
+ });
13
+ return client;
10
14
  }
11
15
  /** Publish any JSON-serialisable payload to a channel */
12
16
  export async function publishEvent(url, channel, data) {
package/dist/index.d.ts CHANGED
@@ -13,3 +13,4 @@ export * from "./seed.js";
13
13
  export * from "./mcp-config.js";
14
14
  export * from "./mcp-servers.js";
15
15
  export * from "./approval-bridge.js";
16
+ export * from "./jira-worklog.js";
package/dist/index.js CHANGED
@@ -12,3 +12,4 @@ export * from "./seed.js";
12
12
  export * from "./mcp-config.js";
13
13
  export * from "./mcp-servers.js";
14
14
  export * from "./approval-bridge.js";
15
+ export * from "./jira-worklog.js";
@@ -0,0 +1,11 @@
1
+ export interface CreateWorklogOpts {
2
+ ticketId: string;
3
+ durationMs: number;
4
+ baseTokens: number;
5
+ comment?: string;
6
+ }
7
+ export interface CreateWorklogResult {
8
+ success: boolean;
9
+ reason?: string;
10
+ }
11
+ export declare function createJiraWorklog(opts: CreateWorklogOpts): Promise<CreateWorklogResult>;
@@ -0,0 +1,88 @@
1
+ import { prisma } from "./prisma.js";
2
+ import { withRetry } from "./retry.js";
3
+ import { decrypt } from "./crypto.js";
4
+ import { loadConfig } from "./config.js";
5
+ function formatJiraApiError(status, detail) {
6
+ return `Jira API error: ${status} ${detail}`;
7
+ }
8
+ function formatDuration(ms) {
9
+ const totalMinutes = Math.floor(ms / 60000);
10
+ const hours = Math.floor(totalMinutes / 60);
11
+ const minutes = totalMinutes % 60;
12
+ if (hours > 0 && minutes > 0) {
13
+ return `${hours}h ${minutes}m`;
14
+ }
15
+ else if (hours > 0) {
16
+ return `${hours}h`;
17
+ }
18
+ else {
19
+ return `${minutes}m`;
20
+ }
21
+ }
22
+ function formatBT(bt) {
23
+ return bt.toLocaleString("en-US");
24
+ }
25
+ // Jira's worklog API rejects timeSpentSeconds values that round to 0 minutes under the
26
+ // project's time-tracking format with "Worklog must not be null" / "must indicate time spent".
27
+ const MIN_JIRA_WORKLOG_SECONDS = 60;
28
+ export async function createJiraWorklog(opts) {
29
+ try {
30
+ const credential = await prisma.credential.findUnique({
31
+ where: { kind_name: { kind: "jira", name: "default" } },
32
+ });
33
+ if (!credential) {
34
+ console.error("[jira-worklog] No Jira credentials found, skipping worklog");
35
+ return { success: false, reason: "No Jira credentials found" };
36
+ }
37
+ const meta = credential.meta;
38
+ const encrypted = credential.secrets.encrypted;
39
+ if (!encrypted || !meta.baseUrl) {
40
+ console.error("[jira-worklog] Incomplete Jira credentials, skipping worklog");
41
+ return { success: false, reason: "Incomplete Jira credentials" };
42
+ }
43
+ const { encryptionKey } = loadConfig();
44
+ const secrets = JSON.parse(decrypt(encrypted, encryptionKey));
45
+ if (!secrets.email || !secrets.token) {
46
+ console.error("[jira-worklog] Incomplete Jira credentials, skipping worklog");
47
+ return { success: false, reason: "Incomplete Jira credentials" };
48
+ }
49
+ // Jira Cloud API tokens authenticate via HTTP Basic auth (email:token), not Bearer —
50
+ // Bearer is reserved for OAuth/Connect-app sessions and Jira rejects it with
51
+ // "Failed to parse Connect Session Auth Token".
52
+ const basicAuth = Buffer.from(`${secrets.email}:${secrets.token}`).toString("base64");
53
+ const timeSpentSeconds = Math.max(Math.floor(opts.durationMs / 1000), MIN_JIRA_WORKLOG_SECONDS);
54
+ const durationStr = formatDuration(opts.durationMs);
55
+ const btStr = formatBT(opts.baseTokens);
56
+ const defaultComment = `AI Logwork for ${opts.ticketId}\nTime Spent: ${durationStr}\nToken Spent: ${btStr} BT`;
57
+ const url = `${meta.baseUrl.replace(/\/+$/, "")}/rest/api/2/issue/${opts.ticketId}/worklog`;
58
+ return await withRetry(async () => {
59
+ const res = await fetch(url, {
60
+ method: "POST",
61
+ headers: {
62
+ Authorization: `Basic ${basicAuth}`,
63
+ "Content-Type": "application/json",
64
+ },
65
+ body: JSON.stringify({
66
+ timeSpentSeconds,
67
+ comment: opts.comment ?? defaultComment,
68
+ }),
69
+ });
70
+ if (!res.ok) {
71
+ const detail = await res.text().catch(() => "");
72
+ if (res.status >= 500) {
73
+ throw new Error(formatJiraApiError(res.status, detail));
74
+ }
75
+ const reason = formatJiraApiError(res.status, detail);
76
+ console.error(`[jira-worklog] Non-retryable error ${res.status}: ${detail}`);
77
+ return { success: false, reason };
78
+ }
79
+ console.log(`[jira-worklog] Created worklog for ${opts.ticketId}`);
80
+ return { success: true };
81
+ }, { maxRetries: 3, baseDelayMs: 1000 });
82
+ }
83
+ catch (err) {
84
+ const reason = err instanceof Error ? err.message : String(err);
85
+ console.error("[jira-worklog] Failed to create worklog:", reason);
86
+ return { success: false, reason };
87
+ }
88
+ }
package/dist/queue.js CHANGED
@@ -2,6 +2,18 @@ import { Queue, Worker } from "bullmq";
2
2
  import { JOB_QUEUE } from "./types.js";
3
3
  const connection = (url) => ({ connection: { url } });
4
4
  /** Creates the BullMQ queue used by the API to enqueue jobs */
5
- export const createQueue = (redisUrl) => new Queue(JOB_QUEUE, connection(redisUrl));
5
+ export const createQueue = (redisUrl) => {
6
+ const queue = new Queue(JOB_QUEUE, connection(redisUrl));
7
+ queue.on("error", (err) => {
8
+ console.error("BullMQ Queue error:", err.message);
9
+ });
10
+ return queue;
11
+ };
6
12
  /** Creates the BullMQ worker consumed by the worker service */
7
- export const createWorker = (redisUrl, processor, concurrency) => new Worker(JOB_QUEUE, processor, { ...connection(redisUrl), concurrency });
13
+ export const createWorker = (redisUrl, processor, concurrency) => {
14
+ const worker = new Worker(JOB_QUEUE, processor, { ...connection(redisUrl), concurrency });
15
+ worker.on("error", (err) => {
16
+ console.error("BullMQ Worker error:", err.message);
17
+ });
18
+ return worker;
19
+ };
package/dist/seed.d.ts CHANGED
@@ -93,7 +93,8 @@ export interface SeedPrismaClient {
93
93
  };
94
94
  }
95
95
  export declare function buildSeedData(input: {
96
- anthropicApiKey: string;
96
+ anthropicAuthToken: string;
97
+ anthropicBaseUrl?: string;
97
98
  }): SeedData;
98
99
  export declare function seedDatabase(client: SeedPrismaClient, seedData: SeedData, encryptionKey: string): Promise<void>;
99
100
  export {};
package/dist/seed.js CHANGED
@@ -60,8 +60,8 @@ export function buildSeedData(input) {
60
60
  {
61
61
  kind: "anthropic",
62
62
  name: "default",
63
- secrets: { apiKey: input.anthropicApiKey },
64
- meta: {},
63
+ secrets: { authToken: input.anthropicAuthToken },
64
+ meta: input.anthropicBaseUrl ? { baseUrl: input.anthropicBaseUrl } : {},
65
65
  },
66
66
  ],
67
67
  repoMapping: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jagit/shared",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,22 @@
1
+ -- AlterTable
2
+ ALTER TABLE "AgentSession" ADD COLUMN "cacheCreationInputTokens" INTEGER NOT NULL DEFAULT 0;
3
+
4
+ -- CreateTable
5
+ CREATE TABLE "ModelPricing" (
6
+ "id" TEXT NOT NULL,
7
+ "model" TEXT NOT NULL,
8
+ "inputCostPerToken" DOUBLE PRECISION NOT NULL DEFAULT 0,
9
+ "outputCostPerToken" DOUBLE PRECISION NOT NULL DEFAULT 0,
10
+ "cacheCreationInputTokenCost" DOUBLE PRECISION,
11
+ "cacheReadInputTokenCost" DOUBLE PRECISION,
12
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
13
+ "updatedAt" TIMESTAMP(3) NOT NULL,
14
+
15
+ CONSTRAINT "ModelPricing_pkey" PRIMARY KEY ("id")
16
+ );
17
+
18
+ -- CreateIndex
19
+ CREATE UNIQUE INDEX "ModelPricing_model_key" ON "ModelPricing"("model");
20
+
21
+ -- CreateIndex
22
+ CREATE INDEX "ModelPricing_model_idx" ON "ModelPricing"("model");
@@ -0,0 +1,9 @@
1
+ -- AlterTable
2
+ ALTER TABLE "AgentSession" ADD COLUMN "durationMs" INTEGER,
3
+ ADD COLUMN "initialCommitSha" TEXT,
4
+ ADD COLUMN "jiraTicketId" TEXT,
5
+ ADD COLUMN "linesAdded" INTEGER,
6
+ ADD COLUMN "linesRemoved" INTEGER;
7
+
8
+ -- CreateIndex
9
+ CREATE INDEX "AgentSession_jiraTicketId_idx" ON "AgentSession"("jiraTicketId");
@@ -1,3 +1,3 @@
1
1
  # Please do not edit this file manually
2
- # It should be added in your version-control system (i.e. Git)
2
+ # It should be added in your version-control system (e.g., Git)
3
3
  provider = "postgresql"
@@ -4,6 +4,7 @@ generator client {
4
4
 
5
5
  datasource db {
6
6
  provider = "postgresql"
7
+
7
8
  }
8
9
 
9
10
  // ─── Enums ───────────────────────────────────────────────────────────────────
@@ -235,23 +236,52 @@ enum AgentTool {
235
236
  }
236
237
 
237
238
  model AgentSession {
238
- id String @id @default(cuid())
239
- tool AgentTool
240
- sessionId String
241
- userId String
242
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
243
- model String
244
- inputTokens Int @default(0)
245
- cachedInputTokens Int @default(0)
246
- outputTokens Int @default(0)
247
- costUsd Float?
248
- toolCallCount Int?
249
- startedAt DateTime
250
- lastUpdatedAt DateTime @updatedAt
251
- rawPayload Json @default("{}")
252
- createdAt DateTime @default(now())
239
+ id String @id @default(cuid())
240
+ tool AgentTool
241
+ sessionId String
242
+ userId String
243
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
244
+ model String
245
+ inputTokens Int @default(0)
246
+ cachedInputTokens Int @default(0)
247
+ cacheCreationInputTokens Int @default(0)
248
+ outputTokens Int @default(0)
249
+ costUsd Float?
250
+ toolCallCount Int?
251
+ startedAt DateTime
252
+ lastUpdatedAt DateTime @updatedAt
253
+ rawPayload Json @default("{}")
254
+ createdAt DateTime @default(now())
255
+
256
+ // Jira association
257
+ jiraTicketId String?
258
+
259
+ // Time tracking
260
+ initialCommitSha String?
261
+ durationMs Int?
262
+
263
+ // Lines of code
264
+ linesAdded Int?
265
+ linesRemoved Int?
253
266
 
254
267
  @@unique([tool, sessionId])
255
268
  @@index([userId, lastUpdatedAt])
256
269
  @@index([tool, lastUpdatedAt])
270
+ @@index([jiraTicketId])
271
+ }
272
+
273
+ // ─── ModelPricing ────────────────────────────────────────────────────────────
274
+ // Periodically updated model API pricing.
275
+
276
+ model ModelPricing {
277
+ id String @id @default(cuid())
278
+ model String @unique
279
+ inputCostPerToken Float @default(0)
280
+ outputCostPerToken Float @default(0)
281
+ cacheCreationInputTokenCost Float?
282
+ cacheReadInputTokenCost Float?
283
+ createdAt DateTime @default(now())
284
+ updatedAt DateTime @updatedAt
285
+
286
+ @@index([model])
257
287
  }