@k-system/tickr-mcp 1.20.0 → 1.22.0
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/api-client.d.ts +4 -0
- package/dist/api-client.js +63 -8
- package/dist/config.js +21 -3
- package/dist/resources/pending-tasks-resource.d.ts +9 -0
- package/dist/resources/pending-tasks-resource.js +34 -0
- package/dist/server.js +28 -1
- package/dist/signalr-client.js +18 -1
- package/dist/tools/add-epic-dependency.d.ts +4 -0
- package/dist/tools/add-epic-dependency.js +23 -0
- package/dist/tools/list-epics.js +1 -1
- package/dist/tools/remove-epic-dependency.d.ts +4 -0
- package/dist/tools/remove-epic-dependency.js +22 -0
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
package/dist/api-client.d.ts
CHANGED
|
@@ -24,6 +24,10 @@ export declare class ApiClient {
|
|
|
24
24
|
patch<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
25
25
|
put<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
26
26
|
delete<T = unknown>(path: string): Promise<T>;
|
|
27
|
+
/** Health check — vrací true pokud API odpovídá */
|
|
28
|
+
healthCheck(): Promise<boolean>;
|
|
27
29
|
private request;
|
|
30
|
+
private shouldRetry;
|
|
31
|
+
private getRetryDelay;
|
|
28
32
|
}
|
|
29
33
|
//# sourceMappingURL=api-client.d.ts.map
|
package/dist/api-client.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { getAuthHeader } from "./auth.js";
|
|
2
|
+
const REQUEST_TIMEOUT_MS = 30_000;
|
|
3
|
+
const MAX_RETRIES = 3;
|
|
4
|
+
const RETRY_BASE_MS = 200;
|
|
2
5
|
/** Fetch wrapper pro Tickr REST API s automatickou autentizací */
|
|
3
6
|
export class ApiClient {
|
|
4
7
|
config;
|
|
@@ -20,6 +23,20 @@ export class ApiClient {
|
|
|
20
23
|
async delete(path) {
|
|
21
24
|
return this.request("DELETE", path);
|
|
22
25
|
}
|
|
26
|
+
/** Health check — vrací true pokud API odpovídá */
|
|
27
|
+
async healthCheck() {
|
|
28
|
+
try {
|
|
29
|
+
const url = `${this.config.apiUrl}/health`;
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timeout = setTimeout(() => controller.abort(), 5_000);
|
|
32
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
33
|
+
clearTimeout(timeout);
|
|
34
|
+
return res.ok;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
23
40
|
async request(method, path, body, retryCount = 0) {
|
|
24
41
|
const auth = await getAuthHeader(this.config);
|
|
25
42
|
const url = `${this.config.apiUrl}${path}`;
|
|
@@ -30,14 +47,38 @@ export class ApiClient {
|
|
|
30
47
|
if (body !== undefined) {
|
|
31
48
|
headers["Content-Type"] = "application/json";
|
|
32
49
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
// Fetch s timeout přes AbortController
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
53
|
+
let res;
|
|
54
|
+
try {
|
|
55
|
+
res = await fetch(url, {
|
|
56
|
+
method,
|
|
57
|
+
headers,
|
|
58
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
59
|
+
signal: controller.signal,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
clearTimeout(timeout);
|
|
64
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
65
|
+
throw new Error(`Request timeout after ${REQUEST_TIMEOUT_MS / 1000}s: ${method} ${path}`);
|
|
66
|
+
}
|
|
67
|
+
// Síťová chyba — retry pokud máme pokusy
|
|
68
|
+
if (retryCount < MAX_RETRIES) {
|
|
69
|
+
const delay = RETRY_BASE_MS * Math.pow(2, retryCount);
|
|
70
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
71
|
+
return this.request(method, path, body, retryCount + 1);
|
|
72
|
+
}
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
clearTimeout(timeout);
|
|
77
|
+
}
|
|
78
|
+
// Auto-retry na 409 Conflict, 429 Too Many Requests, 5xx — max 3 pokusy
|
|
79
|
+
if (this.shouldRetry(res.status) && retryCount < MAX_RETRIES) {
|
|
80
|
+
const delay = this.getRetryDelay(res, retryCount);
|
|
81
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
41
82
|
return this.request(method, path, body, retryCount + 1);
|
|
42
83
|
}
|
|
43
84
|
if (!res.ok) {
|
|
@@ -66,5 +107,19 @@ export class ApiClient {
|
|
|
66
107
|
}
|
|
67
108
|
return json.data;
|
|
68
109
|
}
|
|
110
|
+
shouldRetry(status) {
|
|
111
|
+
return status === 409 || status === 429 || status >= 500;
|
|
112
|
+
}
|
|
113
|
+
getRetryDelay(res, retryCount) {
|
|
114
|
+
// Respektovat Retry-After header pokud existuje (429)
|
|
115
|
+
const retryAfter = res.headers.get("Retry-After");
|
|
116
|
+
if (retryAfter) {
|
|
117
|
+
const seconds = parseInt(retryAfter, 10);
|
|
118
|
+
if (!isNaN(seconds))
|
|
119
|
+
return seconds * 1000;
|
|
120
|
+
}
|
|
121
|
+
// Exponential backoff: 200ms, 400ms, 800ms
|
|
122
|
+
return RETRY_BASE_MS * Math.pow(2, retryCount);
|
|
123
|
+
}
|
|
69
124
|
}
|
|
70
125
|
//# sourceMappingURL=api-client.js.map
|
package/dist/config.js
CHANGED
|
@@ -8,26 +8,44 @@ export function loadConfig() {
|
|
|
8
8
|
const defaultProject = process.env.TICKR_DEFAULT_PROJECT;
|
|
9
9
|
// Env vars mají prioritu
|
|
10
10
|
if (apiUrl && apiKey) {
|
|
11
|
-
|
|
11
|
+
const config = { apiUrl, apiKey, defaultProject };
|
|
12
|
+
validateConfig(config);
|
|
13
|
+
return config;
|
|
12
14
|
}
|
|
13
15
|
// Fallback na config soubor
|
|
14
16
|
const configPath = join(homedir(), ".tickr", "config.json");
|
|
15
17
|
try {
|
|
16
18
|
const raw = readFileSync(configPath, "utf-8");
|
|
17
19
|
const file = JSON.parse(raw);
|
|
18
|
-
|
|
20
|
+
const config = {
|
|
19
21
|
apiUrl: apiUrl || file.apiUrl || "https://localhost:6001",
|
|
20
22
|
apiKey: apiKey || file.apiKey || "",
|
|
21
23
|
defaultProject: defaultProject || file.defaultProject,
|
|
22
24
|
};
|
|
25
|
+
validateConfig(config);
|
|
26
|
+
return config;
|
|
23
27
|
}
|
|
24
28
|
catch {
|
|
25
29
|
// Config soubor neexistuje — použijeme defaults
|
|
26
|
-
|
|
30
|
+
const config = {
|
|
27
31
|
apiUrl: apiUrl || "https://localhost:6001",
|
|
28
32
|
apiKey: apiKey || "",
|
|
29
33
|
defaultProject,
|
|
30
34
|
};
|
|
35
|
+
validateConfig(config);
|
|
36
|
+
return config;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Validace konfigurace — loguje varování pro problematické hodnoty */
|
|
40
|
+
function validateConfig(config) {
|
|
41
|
+
if (!config.apiKey) {
|
|
42
|
+
console.error("[tickr-mcp] WARNING: No API key configured — tools will fail with 401");
|
|
43
|
+
}
|
|
44
|
+
if (config.apiUrl && !config.apiUrl.startsWith("http://") && !config.apiUrl.startsWith("https://")) {
|
|
45
|
+
console.error(`[tickr-mcp] WARNING: apiUrl "${config.apiUrl}" doesn't start with http(s)://`);
|
|
46
|
+
}
|
|
47
|
+
if (config.apiUrl?.endsWith("/")) {
|
|
48
|
+
config.apiUrl = config.apiUrl.replace(/\/+$/, "");
|
|
31
49
|
}
|
|
32
50
|
}
|
|
33
51
|
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { ApiClient } from "../api-client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Dynamic resource tickr://pending-tasks
|
|
5
|
+
* Vrací aktuální stav dev queue — počet čekajících tasků a detail dalšího v řadě.
|
|
6
|
+
* Aktualizuje se přes sendResourceListChanged() při DevTaskQueued eventu.
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerPendingTasksResource(server: McpServer, api: ApiClient): void;
|
|
9
|
+
//# sourceMappingURL=pending-tasks-resource.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic resource tickr://pending-tasks
|
|
3
|
+
* Vrací aktuální stav dev queue — počet čekajících tasků a detail dalšího v řadě.
|
|
4
|
+
* Aktualizuje se přes sendResourceListChanged() při DevTaskQueued eventu.
|
|
5
|
+
*/
|
|
6
|
+
export function registerPendingTasksResource(server, api) {
|
|
7
|
+
server.resource("pending-tasks", "tickr://pending-tasks", { description: "Current dev queue status — queued task count and next task preview" }, async (uri) => {
|
|
8
|
+
// Heartbeat vrací počet queued tasků bez side-effectu na assignment
|
|
9
|
+
const heartbeat = await api
|
|
10
|
+
.post("/api/dev-queue/heartbeat", {})
|
|
11
|
+
.catch(() => null);
|
|
12
|
+
const queuedTasks = heartbeat?.queuedTasks ?? 0;
|
|
13
|
+
const agentStatus = heartbeat?.agentStatus ?? "unknown";
|
|
14
|
+
let text = `## Dev Queue Status\n`;
|
|
15
|
+
text += `- **Agent Status:** ${agentStatus}\n`;
|
|
16
|
+
text += `- **Queued Tasks:** ${queuedTasks}\n`;
|
|
17
|
+
if (queuedTasks > 0) {
|
|
18
|
+
text += `\n⚠️ There are ${queuedTasks} task(s) waiting. Run \`poll_dev_queue\` to pick up the next one.\n`;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
text += `\n✅ Queue is empty — no pending tasks.\n`;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
contents: [
|
|
25
|
+
{
|
|
26
|
+
uri: uri.href,
|
|
27
|
+
mimeType: "text/markdown",
|
|
28
|
+
text,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=pending-tasks-resource.js.map
|
package/dist/server.js
CHANGED
|
@@ -72,6 +72,9 @@ import { registerUpdateCycle } from "./tools/update-cycle.js";
|
|
|
72
72
|
import { registerDeleteCycle } from "./tools/delete-cycle.js";
|
|
73
73
|
import { registerUpdateEpic } from "./tools/update-epic.js";
|
|
74
74
|
import { registerDeleteEpic } from "./tools/delete-epic.js";
|
|
75
|
+
// ADR-0050: Epic Dependencies
|
|
76
|
+
import { registerAddEpicDependency } from "./tools/add-epic-dependency.js";
|
|
77
|
+
import { registerRemoveEpicDependency } from "./tools/remove-epic-dependency.js";
|
|
75
78
|
// ADR-0026 Phase 2f: Project Members
|
|
76
79
|
import { registerListProjectMembers } from "./tools/list-project-members.js";
|
|
77
80
|
import { registerAddProjectMember } from "./tools/add-project-member.js";
|
|
@@ -90,14 +93,23 @@ import { registerLinkCommit } from "./tools/link-commit.js";
|
|
|
90
93
|
// Resources
|
|
91
94
|
import { registerTicketResource } from "./resources/ticket-resource.js";
|
|
92
95
|
import { registerProjectResource } from "./resources/project-resource.js";
|
|
96
|
+
import { registerPendingTasksResource } from "./resources/pending-tasks-resource.js";
|
|
93
97
|
// SignalR push notifications
|
|
94
98
|
import { initSignalRClient, stopSignalRClient } from "./signalr-client.js";
|
|
95
99
|
export async function startServer() {
|
|
96
100
|
const config = loadConfig();
|
|
97
101
|
const api = new ApiClient(config);
|
|
102
|
+
// Startup health check — ověření dostupnosti API
|
|
103
|
+
const healthy = await api.healthCheck();
|
|
104
|
+
if (healthy) {
|
|
105
|
+
console.error(`[tickr-mcp] API reachable at ${config.apiUrl}`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.error(`[tickr-mcp] WARNING: API unreachable at ${config.apiUrl} — tools may fail`);
|
|
109
|
+
}
|
|
98
110
|
const server = new McpServer({
|
|
99
111
|
name: "tickr",
|
|
100
|
-
version: "
|
|
112
|
+
version: "1.22.0",
|
|
101
113
|
});
|
|
102
114
|
// Debug logging wrapper (dedup odstraněn — nefunkční cross-process, řeší se na API straně: TKR-ADR-0043)
|
|
103
115
|
{
|
|
@@ -194,6 +206,9 @@ export async function startServer() {
|
|
|
194
206
|
registerDeleteCycle(server, api);
|
|
195
207
|
registerUpdateEpic(server, api);
|
|
196
208
|
registerDeleteEpic(server, api);
|
|
209
|
+
// ADR-0050: Epic Dependencies
|
|
210
|
+
registerAddEpicDependency(server, api);
|
|
211
|
+
registerRemoveEpicDependency(server, api);
|
|
197
212
|
// ADR-0026 Phase 2f: Project Members
|
|
198
213
|
registerListProjectMembers(server, api);
|
|
199
214
|
registerAddProjectMember(server, api);
|
|
@@ -212,14 +227,26 @@ export async function startServer() {
|
|
|
212
227
|
// Registrace resources
|
|
213
228
|
registerTicketResource(server, api);
|
|
214
229
|
registerProjectResource(server, api);
|
|
230
|
+
registerPendingTasksResource(server, api);
|
|
215
231
|
// Spuštění na stdio transportu
|
|
216
232
|
const transport = new StdioServerTransport();
|
|
217
233
|
await server.connect(transport);
|
|
218
234
|
// SignalR push notifications — pripojeni na BoardHub pro real-time eventy
|
|
219
235
|
// Neni kriticke — MCP server funguje i bez SignalR
|
|
220
236
|
initSignalRClient(config, server, config.defaultProject).catch(() => { });
|
|
237
|
+
// Initial heartbeat — nastav agenta jako idle
|
|
238
|
+
api.post("/api/dev-queue/heartbeat", {}).catch((err) => {
|
|
239
|
+
console.error("[tickr-mcp] initial heartbeat failed:", err instanceof Error ? err.message : String(err));
|
|
240
|
+
});
|
|
241
|
+
// Periodicky heartbeat kazdych 60 sekund — udržuje agent status + snižuje latenci task discovery
|
|
242
|
+
const heartbeatInterval = setInterval(() => {
|
|
243
|
+
api.post("/api/dev-queue/heartbeat", {}).catch((err) => {
|
|
244
|
+
console.error("[tickr-mcp] heartbeat failed:", err instanceof Error ? err.message : String(err));
|
|
245
|
+
});
|
|
246
|
+
}, 60_000);
|
|
221
247
|
// Cleanup pri ukonceni procesu
|
|
222
248
|
const cleanup = async () => {
|
|
249
|
+
clearInterval(heartbeatInterval);
|
|
223
250
|
await stopSignalRClient();
|
|
224
251
|
process.exit(0);
|
|
225
252
|
};
|
package/dist/signalr-client.js
CHANGED
|
@@ -19,8 +19,13 @@ export async function initSignalRClient(config, server, projectSlug) {
|
|
|
19
19
|
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
|
|
20
20
|
.configureLogging(LogLevel.None)
|
|
21
21
|
.build();
|
|
22
|
-
// Event handlery — notifikace do MCP session
|
|
22
|
+
// Event handlery — resource update + logging notifikace do MCP session
|
|
23
23
|
connection.on("DevTaskQueued", () => {
|
|
24
|
+
// Aktualizuj tickr://pending-tasks resource — agent uvidí změnu při dalším tool callu
|
|
25
|
+
try {
|
|
26
|
+
server.sendResourceListChanged();
|
|
27
|
+
}
|
|
28
|
+
catch { }
|
|
24
29
|
server
|
|
25
30
|
.sendLoggingMessage({
|
|
26
31
|
level: "notice",
|
|
@@ -30,6 +35,10 @@ export async function initSignalRClient(config, server, projectSlug) {
|
|
|
30
35
|
.catch(() => { });
|
|
31
36
|
});
|
|
32
37
|
connection.on("DevTaskCompleted", () => {
|
|
38
|
+
try {
|
|
39
|
+
server.sendResourceListChanged();
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
33
42
|
server
|
|
34
43
|
.sendLoggingMessage({
|
|
35
44
|
level: "info",
|
|
@@ -39,6 +48,10 @@ export async function initSignalRClient(config, server, projectSlug) {
|
|
|
39
48
|
.catch(() => { });
|
|
40
49
|
});
|
|
41
50
|
connection.on("TicketUpdated", () => {
|
|
51
|
+
try {
|
|
52
|
+
server.sendResourceListChanged();
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
42
55
|
server
|
|
43
56
|
.sendLoggingMessage({
|
|
44
57
|
level: "debug",
|
|
@@ -48,6 +61,10 @@ export async function initSignalRClient(config, server, projectSlug) {
|
|
|
48
61
|
.catch(() => { });
|
|
49
62
|
});
|
|
50
63
|
connection.on("TicketCreated", () => {
|
|
64
|
+
try {
|
|
65
|
+
server.sendResourceListChanged();
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
51
68
|
server
|
|
52
69
|
.sendLoggingMessage({
|
|
53
70
|
level: "debug",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerAddEpicDependency(server, api) {
|
|
3
|
+
server.tool("add_epic_dependency", "Add a dependency between two epics (predecessor blocks successor)", {
|
|
4
|
+
project: z.string().describe("Project slug"),
|
|
5
|
+
predecessor_epic_id: z.string().describe("UUID of the predecessor epic (must finish first)"),
|
|
6
|
+
successor_epic_id: z.string().describe("UUID of the successor epic (depends on predecessor)"),
|
|
7
|
+
type: z.string().optional().describe("Dependency type: finish-to-start (default), start-to-start, finish-to-finish, start-to-finish"),
|
|
8
|
+
}, async (params) => {
|
|
9
|
+
try {
|
|
10
|
+
const result = await api.post(`/api/projects/${params.project}/epics/${params.predecessor_epic_id}/dependencies`, { successorEpicId: params.successor_epic_id, dependencyType: params.type });
|
|
11
|
+
return {
|
|
12
|
+
content: [{ type: "text", text: `Dependency created (${params.type ?? "finish-to-start"}): ${params.predecessor_epic_id} → ${params.successor_epic_id}` }],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
return {
|
|
17
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
18
|
+
isError: true,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=add-epic-dependency.js.map
|
package/dist/tools/list-epics.js
CHANGED
|
@@ -6,7 +6,7 @@ export function registerListEpics(server, api) {
|
|
|
6
6
|
try {
|
|
7
7
|
const epics = await api.get(`/api/projects/${params.project}/epics`);
|
|
8
8
|
const text = epics
|
|
9
|
-
.map((e) => `${e.name} (${e.id}) [${e.status}] (${e.
|
|
9
|
+
.map((e) => `${e.name} (${e.id}) [${e.status}] (${e.completedTicketCount}/${e.ticketCount})${e.targetDate ? ` target: ${e.targetDate}` : ""}`)
|
|
10
10
|
.join("\n");
|
|
11
11
|
return {
|
|
12
12
|
content: [{ type: "text", text: text || "No epics found." }],
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { ApiClient } from "../api-client.js";
|
|
3
|
+
export declare function registerRemoveEpicDependency(server: McpServer, api: ApiClient): void;
|
|
4
|
+
//# sourceMappingURL=remove-epic-dependency.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerRemoveEpicDependency(server, api) {
|
|
3
|
+
server.tool("remove_epic_dependency", "Remove a dependency between two epics", {
|
|
4
|
+
project: z.string().describe("Project slug"),
|
|
5
|
+
epic_id: z.string().describe("UUID of either epic in the dependency"),
|
|
6
|
+
dependency_id: z.string().describe("UUID of the dependency to remove"),
|
|
7
|
+
}, async (params) => {
|
|
8
|
+
try {
|
|
9
|
+
await api.delete(`/api/projects/${params.project}/epics/${params.epic_id}/dependencies/${params.dependency_id}`);
|
|
10
|
+
return {
|
|
11
|
+
content: [{ type: "text", text: `Dependency ${params.dependency_id} removed` }],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
return {
|
|
16
|
+
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
|
17
|
+
isError: true,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=remove-epic-dependency.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -93,8 +93,8 @@ export interface Epic {
|
|
|
93
93
|
color: string;
|
|
94
94
|
status: string;
|
|
95
95
|
targetDate: string | null;
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
ticketCount: number;
|
|
97
|
+
completedTicketCount: number;
|
|
98
98
|
createdAt: string;
|
|
99
99
|
}
|
|
100
100
|
//# sourceMappingURL=types.d.ts.map
|