@locusai/server 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Locus AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@locusai/server",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "scripts": {
7
+ "dev": "bun run src/index.ts",
8
+ "lint": "bun lint"
9
+ },
10
+ "dependencies": {
11
+ "express": "^4.19.2",
12
+ "better-sqlite3": "^11.5.0",
13
+ "zod": "^3.23.8",
14
+ "@locusai/shared": "workspace:*",
15
+ "cors": "^2.8.5",
16
+ "dockerode": "^4.0.2"
17
+ },
18
+ "devDependencies": {
19
+ "@types/better-sqlite3": "^7.6.11",
20
+ "@types/cors": "^2.8.19",
21
+ "@types/dockerode": "^3.3.33",
22
+ "@types/express": "^5.0.6"
23
+ }
24
+ }
@@ -0,0 +1,45 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { NextFunction, Request, Response } from "express";
4
+ import type { ArtifactRepository } from "../repositories/artifact.repository.js";
5
+
6
+ export class ArtifactController {
7
+ constructor(
8
+ private artifactRepo: ArtifactRepository,
9
+ private workspaceDir: string
10
+ ) {}
11
+
12
+ getArtifactFile = (req: Request, res: Response, next: NextFunction) => {
13
+ try {
14
+ const { taskId, type, filename } = req.params;
15
+ const filePath = join(
16
+ this.workspaceDir,
17
+ "artifacts",
18
+ taskId as string,
19
+ type as string,
20
+ filename as string
21
+ );
22
+
23
+ if (!existsSync(filePath)) {
24
+ return res
25
+ .status(404)
26
+ .json({ error: { message: "Artifact not found" } });
27
+ }
28
+
29
+ res.sendFile(filePath);
30
+ } catch (err) {
31
+ next(err);
32
+ }
33
+ };
34
+
35
+ getByTaskId = (req: Request, res: Response, next: NextFunction) => {
36
+ try {
37
+ const artifacts = this.artifactRepo.findByTaskId(
38
+ req.params.taskId as string
39
+ );
40
+ res.json(artifacts);
41
+ } catch (err) {
42
+ next(err);
43
+ }
44
+ };
45
+ }
@@ -0,0 +1,16 @@
1
+ import type { NextFunction, Request, Response } from "express";
2
+ import type { CiService } from "../services/ci.service.js";
3
+
4
+ export class CiController {
5
+ constructor(private ciService: CiService) {}
6
+
7
+ run = async (req: Request, res: Response, next: NextFunction) => {
8
+ try {
9
+ const { taskId, preset } = req.body;
10
+ const result = await this.ciService.runCi(taskId, preset);
11
+ res.json(result);
12
+ } catch (err) {
13
+ next(err);
14
+ }
15
+ };
16
+ }
@@ -0,0 +1,116 @@
1
+ import {
2
+ existsSync,
3
+ readdirSync,
4
+ readFileSync,
5
+ statSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { join } from "node:path";
9
+ import type { NextFunction, Request, Response } from "express";
10
+
11
+ export interface DocNode {
12
+ type: "file" | "directory";
13
+ name: string;
14
+ path: string;
15
+ children?: DocNode[];
16
+ }
17
+
18
+ export class DocController {
19
+ constructor(private config: { repoPath: string; docsPath: string }) {}
20
+
21
+ getAll = (_req: Request, res: Response, next: NextFunction) => {
22
+ try {
23
+ const locusDir = join(this.config.repoPath, ".locus");
24
+ const docsFile = join(locusDir, "docs.json");
25
+
26
+ if (!existsSync(docsFile)) {
27
+ return res.json([]);
28
+ }
29
+
30
+ const docs = JSON.parse(readFileSync(docsFile, "utf-8"));
31
+ res.json(docs);
32
+ } catch (err) {
33
+ next(err);
34
+ }
35
+ };
36
+
37
+ getTree = (_req: Request, res: Response, next: NextFunction) => {
38
+ try {
39
+ if (!existsSync(this.config.docsPath)) {
40
+ return res.json([]);
41
+ }
42
+ const tree = this.buildTree(this.config.docsPath);
43
+ res.json(tree);
44
+ } catch (err) {
45
+ next(err);
46
+ }
47
+ };
48
+
49
+ read = (req: Request, res: Response, next: NextFunction) => {
50
+ try {
51
+ const filePath = req.query.path as string;
52
+ if (!filePath) {
53
+ return res.status(400).json({ error: { message: "Path is required" } });
54
+ }
55
+
56
+ const fullPath = join(this.config.docsPath, filePath);
57
+ if (!fullPath.startsWith(this.config.docsPath)) {
58
+ return res.status(403).json({ error: { message: "Invalid path" } });
59
+ }
60
+
61
+ const content = readFileSync(fullPath, "utf-8");
62
+ res.json({ content });
63
+ } catch (err) {
64
+ next(err);
65
+ }
66
+ };
67
+
68
+ write = (req: Request, res: Response, next: NextFunction) => {
69
+ try {
70
+ const { path: filePath, content } = req.body;
71
+ if (!filePath) {
72
+ return res.status(400).json({ error: { message: "Path is required" } });
73
+ }
74
+
75
+ const fullPath = join(this.config.docsPath, filePath);
76
+ if (!fullPath.startsWith(this.config.docsPath)) {
77
+ return res.status(403).json({ error: { message: "Invalid path" } });
78
+ }
79
+
80
+ writeFileSync(fullPath, content, "utf-8");
81
+ res.json({ ok: true });
82
+ } catch (err) {
83
+ next(err);
84
+ }
85
+ };
86
+
87
+ private buildTree(dir: string, basePath: string = ""): DocNode[] {
88
+ const entries = readdirSync(dir);
89
+ const nodes: DocNode[] = [];
90
+
91
+ for (const entry of entries) {
92
+ if (entry.startsWith(".")) continue;
93
+
94
+ const fullPath = join(dir, entry);
95
+ const relativePath = join(basePath, entry);
96
+ const stats = statSync(fullPath);
97
+
98
+ if (stats.isDirectory()) {
99
+ nodes.push({
100
+ type: "directory",
101
+ name: entry,
102
+ path: relativePath,
103
+ children: this.buildTree(fullPath, relativePath),
104
+ });
105
+ } else {
106
+ nodes.push({
107
+ type: "file",
108
+ name: entry,
109
+ path: relativePath,
110
+ });
111
+ }
112
+ }
113
+
114
+ return nodes;
115
+ }
116
+ }
@@ -0,0 +1,20 @@
1
+ import type { NextFunction, Request, Response } from "express";
2
+ import type { EventRepository } from "../repositories/event.repository.js";
3
+
4
+ export class EventController {
5
+ constructor(private eventRepo: EventRepository) {}
6
+
7
+ getByTaskId = (req: Request, res: Response, next: NextFunction) => {
8
+ try {
9
+ const taskId = req.query.taskId as string;
10
+ const events = this.eventRepo.findByTaskId(taskId);
11
+ const formattedEvents = events.map((e) => ({
12
+ ...e,
13
+ payload: JSON.parse(e.payload || "{}"),
14
+ }));
15
+ res.json(formattedEvents);
16
+ } catch (err) {
17
+ next(err);
18
+ }
19
+ };
20
+ }
@@ -0,0 +1,34 @@
1
+ import type { NextFunction, Request, Response } from "express";
2
+ import type { SprintService } from "../services/sprint.service.js";
3
+
4
+ export class SprintController {
5
+ constructor(private sprintService: SprintService) {}
6
+
7
+ getAll = (_req: Request, res: Response, next: NextFunction) => {
8
+ try {
9
+ const sprints = this.sprintService.getAllSprints();
10
+ res.json(sprints);
11
+ } catch (err) {
12
+ next(err);
13
+ }
14
+ };
15
+
16
+ create = (req: Request, res: Response, next: NextFunction) => {
17
+ try {
18
+ const { name } = req.body;
19
+ const id = this.sprintService.createSprint(name);
20
+ res.json({ id });
21
+ } catch (err) {
22
+ next(err);
23
+ }
24
+ };
25
+
26
+ updateStatus = (req: Request, res: Response, next: NextFunction) => {
27
+ try {
28
+ this.sprintService.updateSprint(req.params.id as string, req.body);
29
+ res.json({ ok: true });
30
+ } catch (err) {
31
+ next(err);
32
+ }
33
+ };
34
+ }
@@ -0,0 +1,98 @@
1
+ import { TaskSchema } from "@locusai/shared";
2
+ import type { NextFunction, Request, Response } from "express";
3
+ import type { TaskService } from "../services/task.service.js";
4
+
5
+ export class TaskController {
6
+ constructor(private taskService: TaskService) {}
7
+
8
+ getAll = (_req: Request, res: Response, next: NextFunction) => {
9
+ try {
10
+ const tasks = this.taskService.getAllTasks();
11
+ res.json(tasks);
12
+ } catch (err) {
13
+ next(err);
14
+ }
15
+ };
16
+
17
+ getById = (req: Request, res: Response, next: NextFunction) => {
18
+ try {
19
+ const task = this.taskService.getTaskById(req.params.id as string);
20
+ res.json(task);
21
+ } catch (err) {
22
+ next(err);
23
+ }
24
+ };
25
+
26
+ create = (req: Request, res: Response, next: NextFunction) => {
27
+ try {
28
+ const data = TaskSchema.parse(req.body);
29
+ const id = this.taskService.createTask(data);
30
+ res.json({ id });
31
+ } catch (err) {
32
+ next(err);
33
+ }
34
+ };
35
+
36
+ update = (req: Request, res: Response, next: NextFunction) => {
37
+ try {
38
+ this.taskService.updateTask(req.params.id as string, req.body);
39
+ res.json({ ok: true });
40
+ } catch (err) {
41
+ next(err);
42
+ }
43
+ };
44
+
45
+ delete = (req: Request, res: Response, next: NextFunction) => {
46
+ try {
47
+ this.taskService.deleteTask(req.params.id as string);
48
+ res.json({ ok: true });
49
+ } catch (err) {
50
+ next(err);
51
+ }
52
+ };
53
+
54
+ addComment = (req: Request, res: Response, next: NextFunction) => {
55
+ try {
56
+ const { author, text } = req.body;
57
+ this.taskService.addComment(req.params.id as string, author, text);
58
+ res.json({ ok: true });
59
+ } catch (err) {
60
+ next(err);
61
+ }
62
+ };
63
+
64
+ dispatch = (req: Request, res: Response, next: NextFunction) => {
65
+ try {
66
+ const { workerId, sprintId } = req.body;
67
+ if (!sprintId) {
68
+ return res
69
+ .status(400)
70
+ .json({ error: { message: "sprintId is required" } });
71
+ }
72
+ const task = this.taskService.dispatchTask(workerId, Number(sprintId));
73
+ res.json(task);
74
+ } catch (err) {
75
+ next(err);
76
+ }
77
+ };
78
+
79
+ lock = (req: Request, res: Response, next: NextFunction) => {
80
+ try {
81
+ const { agentId, ttlSeconds } = req.body;
82
+ this.taskService.lockTask(req.params.id as string, agentId, ttlSeconds);
83
+ res.json({ ok: true });
84
+ } catch (err) {
85
+ next(err);
86
+ }
87
+ };
88
+
89
+ unlock = (req: Request, res: Response, next: NextFunction) => {
90
+ try {
91
+ const { agentId } = req.body;
92
+ this.taskService.unlockTask(req.params.id as string, agentId);
93
+ res.json({ ok: true });
94
+ } catch (err) {
95
+ next(err);
96
+ }
97
+ };
98
+ }
package/src/db.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { join } from "node:path";
3
+
4
+ export function initDb(workspaceDir: string) {
5
+ const db = new Database(join(workspaceDir, "db.sqlite"));
6
+
7
+ // Create tables if they don't exist
8
+ db.run(`
9
+ CREATE TABLE IF NOT EXISTS tasks (
10
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
11
+ title TEXT NOT NULL,
12
+ description TEXT,
13
+ status TEXT NOT NULL,
14
+ priority TEXT NOT NULL DEFAULT 'MEDIUM',
15
+ labels TEXT,
16
+ assigneeRole TEXT,
17
+ parentId INTEGER,
18
+ lockedBy TEXT,
19
+ lockExpiresAt INTEGER,
20
+ acceptanceChecklist TEXT,
21
+ createdAt INTEGER NOT NULL,
22
+ updatedAt INTEGER NOT NULL,
23
+ FOREIGN KEY(parentId) REFERENCES tasks(id)
24
+ );`);
25
+
26
+ db.run(`
27
+ CREATE TABLE IF NOT EXISTS comments (
28
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ taskId INTEGER NOT NULL,
30
+ author TEXT NOT NULL,
31
+ text TEXT NOT NULL,
32
+ createdAt INTEGER NOT NULL,
33
+ FOREIGN KEY(taskId) REFERENCES tasks(id)
34
+ );`);
35
+
36
+ db.run(`
37
+ CREATE TABLE IF NOT EXISTS artifacts (
38
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
39
+ taskId INTEGER NOT NULL,
40
+ type TEXT NOT NULL,
41
+ title TEXT NOT NULL,
42
+ contentText TEXT,
43
+ filePath TEXT,
44
+ createdBy TEXT NOT NULL,
45
+ createdAt INTEGER NOT NULL,
46
+ FOREIGN KEY(taskId) REFERENCES tasks(id)
47
+ );`);
48
+
49
+ db.run(`
50
+ CREATE TABLE IF NOT EXISTS events (
51
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
52
+ taskId INTEGER NOT NULL,
53
+ type TEXT NOT NULL,
54
+ payload TEXT,
55
+ createdAt INTEGER NOT NULL,
56
+ FOREIGN KEY(taskId) REFERENCES tasks(id)
57
+ );`);
58
+
59
+ // Run migrations for existing tasks table columns
60
+ try {
61
+ const tableInfo = db.prepare("PRAGMA table_info(tasks)").all() as {
62
+ name: string;
63
+ }[];
64
+ const columns = tableInfo.map((col) => col.name);
65
+
66
+ if (!columns.includes("priority")) {
67
+ db.run(
68
+ "ALTER TABLE tasks ADD COLUMN priority TEXT NOT NULL DEFAULT 'MEDIUM'"
69
+ );
70
+ console.log("Migration: Added priority column to tasks table");
71
+ }
72
+
73
+ if (!columns.includes("parentId")) {
74
+ db.run("ALTER TABLE tasks ADD COLUMN parentId INTEGER");
75
+ console.log("Migration: Added parentId column to tasks table");
76
+ }
77
+
78
+ if (!columns.includes("lockedBy")) {
79
+ db.run("ALTER TABLE tasks ADD COLUMN lockedBy TEXT");
80
+ console.log("Migration: Added lockedBy column to tasks table");
81
+ }
82
+
83
+ if (!columns.includes("lockExpiresAt")) {
84
+ db.run("ALTER TABLE tasks ADD COLUMN lockExpiresAt INTEGER");
85
+ console.log("Migration: Added lockExpiresAt column to tasks table");
86
+ }
87
+
88
+ if (!columns.includes("sprintId")) {
89
+ db.run("ALTER TABLE tasks ADD COLUMN sprintId INTEGER");
90
+ console.log("Migration: Added sprintId column to tasks table");
91
+ }
92
+ } catch (err) {
93
+ console.error("Migration error:", err);
94
+ }
95
+
96
+ db.run(`
97
+ CREATE TABLE IF NOT EXISTS sprints (
98
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
99
+ name TEXT NOT NULL,
100
+ status TEXT NOT NULL DEFAULT 'PLANNED',
101
+ startDate INTEGER,
102
+ endDate INTEGER,
103
+ createdAt INTEGER NOT NULL
104
+ );`);
105
+
106
+ return db;
107
+ }
package/src/index.ts ADDED
@@ -0,0 +1,133 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { isAbsolute, join } from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ import cors from "cors";
5
+ import express from "express";
6
+ import { ArtifactController } from "./controllers/artifact.controller.js";
7
+ import { CiController } from "./controllers/ci.controller.js";
8
+ import { DocController } from "./controllers/doc.controller.js";
9
+ import { EventController } from "./controllers/event.controller.js";
10
+ import { SprintController } from "./controllers/sprint.controller.js";
11
+ // Controllers
12
+ import { TaskController } from "./controllers/task.controller.js";
13
+ import { initDb } from "./db.js";
14
+ // Middleware
15
+ import { errorHandler } from "./middleware/error.middleware.js";
16
+ import { ArtifactRepository } from "./repositories/artifact.repository.js";
17
+ import { CommentRepository } from "./repositories/comment.repository.js";
18
+ import { EventRepository } from "./repositories/event.repository.js";
19
+ import { SprintRepository } from "./repositories/sprint.repository.js";
20
+ // Repositories
21
+ import { TaskRepository } from "./repositories/task.repository.js";
22
+ import { createArtifactsRouter } from "./routes/artifacts.routes.js";
23
+ import { createCiRouter } from "./routes/ci.routes.js";
24
+ import { createDocsRouter } from "./routes/docs.routes.js";
25
+ import { createEventsRouter } from "./routes/events.routes.js";
26
+ import { createSprintsRouter } from "./routes/sprints.routes.js";
27
+ // Routes
28
+ import { createTaskRouter } from "./routes/tasks.routes.js";
29
+ import { CiService } from "./services/ci.service.js";
30
+ import { SprintService } from "./services/sprint.service.js";
31
+ // Services
32
+ import { TaskService } from "./services/task.service.js";
33
+ import { TaskProcessor } from "./task-processor.js";
34
+
35
+ const { values } = parseArgs({
36
+ args: Bun.argv,
37
+ options: {
38
+ project: { type: "string" },
39
+ },
40
+ strict: true,
41
+ allowPositionals: true,
42
+ });
43
+
44
+ if (!values.project) {
45
+ console.error("Usage: bun run dev -- --project <workspaceDir>");
46
+ process.exit(1);
47
+ }
48
+
49
+ const workspaceDir = isAbsolute(values.project)
50
+ ? values.project
51
+ : join(process.cwd(), values.project);
52
+ const configPath = join(workspaceDir, "workspace.config.json");
53
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
54
+
55
+ const db = initDb(workspaceDir);
56
+
57
+ // Initialize Repositories
58
+ const taskRepo = new TaskRepository(db);
59
+ const eventRepo = new EventRepository(db);
60
+ const commentRepo = new CommentRepository(db);
61
+ const artifactRepo = new ArtifactRepository(db);
62
+ const sprintRepo = new SprintRepository(db);
63
+
64
+ const processor = new TaskProcessor(db, {
65
+ ciPresetsPath: config.ciPresetsPath,
66
+ repoPath: workspaceDir,
67
+ });
68
+
69
+ // Initialize Services
70
+ const taskService = new TaskService(
71
+ taskRepo,
72
+ eventRepo,
73
+ commentRepo,
74
+ artifactRepo,
75
+ processor
76
+ );
77
+ const sprintService = new SprintService(sprintRepo);
78
+ const ciService = new CiService(artifactRepo, eventRepo, {
79
+ ciPresetsPath: config.ciPresetsPath,
80
+ repoPath: workspaceDir,
81
+ });
82
+
83
+ // Initialize Controllers
84
+ const taskController = new TaskController(taskService);
85
+ const sprintController = new SprintController(sprintService);
86
+ const ciController = new CiController(ciService);
87
+ const artifactController = new ArtifactController(artifactRepo, workspaceDir);
88
+ const docController = new DocController({
89
+ repoPath: config.repoPath,
90
+ docsPath: config.docsPath,
91
+ });
92
+ const eventController = new EventController(eventRepo);
93
+
94
+ const app = express();
95
+ app.use(cors());
96
+ app.use(express.json({ limit: "12mb" }));
97
+
98
+ // Routes Implementation
99
+ app.use("/api/tasks", createTaskRouter(taskController));
100
+ app.use("/api/docs", createDocsRouter(docController));
101
+ app.use("/api/sprints", createSprintsRouter(sprintController));
102
+ app.use("/api/ci", createCiRouter(ciController));
103
+ app.use("/api/events", createEventsRouter(eventController));
104
+ app.use("/api", createArtifactsRouter(artifactController));
105
+
106
+ // Health Check
107
+ app.get("/health", (_req, res) => res.json({ status: "ok" }));
108
+
109
+ // Error Handling
110
+ app.use(errorHandler);
111
+
112
+ const PORT = 3080;
113
+
114
+ // Serve Dashboard UI if it exists
115
+ const dashboardPaths = [
116
+ join(import.meta.dir, "../public/dashboard"), // Production (bundled)
117
+ join(process.cwd(), "packages/cli/public/dashboard"), // Dev (root)
118
+ ];
119
+
120
+ const dashboardPath = dashboardPaths.find((p) => existsSync(p));
121
+
122
+ if (dashboardPath) {
123
+ console.log(`Serving dashboard from ${dashboardPath}`);
124
+ app.use(express.static(dashboardPath));
125
+ // Client-side routing fallback
126
+ app.get("/[^api]*", (_req, res) => {
127
+ res.sendFile(join(dashboardPath, "index.html"));
128
+ });
129
+ }
130
+
131
+ app.listen(PORT, () =>
132
+ console.log(`Server running on http://localhost:${PORT}`)
133
+ );
@@ -0,0 +1,21 @@
1
+ import type { NextFunction, Request, Response } from "express";
2
+ import { ServiceError } from "../services/task.service.js";
3
+
4
+ export function errorHandler(
5
+ err: Error,
6
+ _req: Request,
7
+ res: Response,
8
+ _next: NextFunction
9
+ ) {
10
+ console.error("Server Error:", err);
11
+
12
+ const statusCode = err instanceof ServiceError ? err.statusCode : 500;
13
+ const message = err.message || "Internal Server Error";
14
+
15
+ res.status(statusCode).json({
16
+ error: {
17
+ message,
18
+ stack: process.env.NODE_ENV === "development" ? err.stack : undefined,
19
+ },
20
+ });
21
+ }
@@ -0,0 +1,44 @@
1
+ import { BaseRepository } from "./base.repository.js";
2
+
3
+ export interface DBArtifact {
4
+ id: number;
5
+ taskId: number;
6
+ type: string;
7
+ title: string;
8
+ contentText?: string;
9
+ filePath?: string;
10
+ createdBy: string;
11
+ createdAt: number;
12
+ }
13
+
14
+ export class ArtifactRepository extends BaseRepository {
15
+ findByTaskId(taskId: number | string): DBArtifact[] {
16
+ return this.db
17
+ .prepare(
18
+ "SELECT * FROM artifacts WHERE taskId = ? ORDER BY createdAt DESC"
19
+ )
20
+ .all(taskId) as DBArtifact[];
21
+ }
22
+
23
+ create(data: Omit<DBArtifact, "id" | "createdAt">): void {
24
+ const now = Date.now();
25
+ this.db
26
+ .prepare(`
27
+ INSERT INTO artifacts (taskId, type, title, contentText, filePath, createdBy, createdAt)
28
+ VALUES (?, ?, ?, ?, ?, ?, ?)
29
+ `)
30
+ .run(
31
+ data.taskId,
32
+ data.type,
33
+ data.title,
34
+ data.contentText ?? null,
35
+ data.filePath ?? null,
36
+ data.createdBy,
37
+ now
38
+ );
39
+ }
40
+
41
+ deleteByTaskId(taskId: number | string): void {
42
+ this.db.prepare("DELETE FROM artifacts WHERE taskId = ?").run(taskId);
43
+ }
44
+ }
@@ -0,0 +1,5 @@
1
+ import type { Database } from "bun:sqlite";
2
+
3
+ export abstract class BaseRepository {
4
+ constructor(protected db: Database) {}
5
+ }