@jagit/shared 0.0.1 → 0.0.3
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 +2 -0
- package/dist/config.js +9 -2
- package/dist/credentials.d.ts +4 -2
- package/dist/credentials.js +4 -3
- package/dist/events.js +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/jira-worklog.d.ts +11 -0
- package/dist/jira-worklog.js +88 -0
- package/dist/queue.js +14 -2
- package/dist/seed.d.ts +2 -1
- package/dist/seed.js +2 -2
- package/package.json +1 -1
- package/prisma/migrations/20260621082352_add_cache_creation_input_tokens/migration.sql +22 -0
- package/prisma/migrations/20260621120000_add_session_tracking_fields/migration.sql +9 -0
- package/prisma/migrations/migration_lock.toml +1 -1
- package/prisma/schema.prisma +45 -15
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:
|
|
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,
|
package/dist/credentials.d.ts
CHANGED
|
@@ -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
|
|
65
|
+
apiKey?: string | undefined;
|
|
66
|
+
authToken?: string | undefined;
|
|
65
67
|
};
|
|
66
68
|
} | {
|
|
67
69
|
meta: {
|
package/dist/credentials.js
CHANGED
|
@@ -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
|
-
|
|
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
package/dist/index.js
CHANGED
|
@@ -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) =>
|
|
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) =>
|
|
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
|
-
|
|
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: {
|
|
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
|
@@ -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");
|
package/prisma/schema.prisma
CHANGED
|
@@ -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
|
|
239
|
-
tool
|
|
240
|
-
sessionId
|
|
241
|
-
userId
|
|
242
|
-
user
|
|
243
|
-
model
|
|
244
|
-
inputTokens
|
|
245
|
-
cachedInputTokens
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
}
|