@umudik/task-bridge 0.0.1

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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +75 -0
  3. package/apps/backend/dist/config.js +26 -0
  4. package/apps/backend/dist/db/epic-workflow-db.js +125 -0
  5. package/apps/backend/dist/db/library-db.js +123 -0
  6. package/apps/backend/dist/db/projects-db.js +110 -0
  7. package/apps/backend/dist/db/tasks-db.js +282 -0
  8. package/apps/backend/dist/db/users-db.js +117 -0
  9. package/apps/backend/dist/db/workflow-db.js +186 -0
  10. package/apps/backend/dist/db/workflow-template-db.js +715 -0
  11. package/apps/backend/dist/domain/project-member.js +15 -0
  12. package/apps/backend/dist/domain/task-template-graph.js +63 -0
  13. package/apps/backend/dist/domain/task.js +93 -0
  14. package/apps/backend/dist/domain/work-status.js +30 -0
  15. package/apps/backend/dist/domain/workflow-stage.js +186 -0
  16. package/apps/backend/dist/domain/workflow-state.js +73 -0
  17. package/apps/backend/dist/domain/workflow-template-id.js +6 -0
  18. package/apps/backend/dist/errors/app-error.js +24 -0
  19. package/apps/backend/dist/index.js +67 -0
  20. package/apps/backend/dist/lib/bridge-project.js +24 -0
  21. package/apps/backend/dist/lib/inbox-cursor.js +34 -0
  22. package/apps/backend/dist/lib/strings.js +15 -0
  23. package/apps/backend/dist/logger.js +29 -0
  24. package/apps/backend/dist/mappers/task-response.js +261 -0
  25. package/apps/backend/dist/middleware/auth.js +29 -0
  26. package/apps/backend/dist/openapi.js +716 -0
  27. package/apps/backend/dist/routes/admin-users.js +79 -0
  28. package/apps/backend/dist/routes/auth.js +81 -0
  29. package/apps/backend/dist/routes/connect.js +1 -0
  30. package/apps/backend/dist/routes/docs.js +13 -0
  31. package/apps/backend/dist/routes/health.js +6 -0
  32. package/apps/backend/dist/routes/library.js +139 -0
  33. package/apps/backend/dist/routes/projects.js +95 -0
  34. package/apps/backend/dist/routes/tasks.js +522 -0
  35. package/apps/backend/dist/routes/web.js +79 -0
  36. package/apps/backend/dist/routes/workflow-templates.js +152 -0
  37. package/apps/backend/dist/routes/workflow.js +165 -0
  38. package/apps/backend/dist/services/connect-target.js +4 -0
  39. package/apps/backend/dist/services/epic-service.js +269 -0
  40. package/apps/backend/dist/services/library-service.js +222 -0
  41. package/apps/backend/dist/services/project-registry.js +122 -0
  42. package/apps/backend/dist/services/task-assignee-service.js +42 -0
  43. package/apps/backend/dist/services/task-claim-policy.js +310 -0
  44. package/apps/backend/dist/services/task-queue.js +105 -0
  45. package/apps/backend/dist/services/task-service.js +198 -0
  46. package/apps/backend/dist/services/workflow-rules.js +18 -0
  47. package/apps/backend/dist/services/workflow-service.js +418 -0
  48. package/apps/backend/dist/services/workflow-spawn-service.js +179 -0
  49. package/apps/backend/dist/services/workflow-state-service.js +157 -0
  50. package/apps/backend/dist/services/workflow-template-service.js +204 -0
  51. package/apps/backend/public/assets/index-Bl1ciVpY.js +409 -0
  52. package/apps/backend/public/assets/index-ByKECv-I.css +1 -0
  53. package/apps/backend/public/index.html +13 -0
  54. package/bin/task-bridge.mjs +86 -0
  55. package/package.json +41 -0
@@ -0,0 +1,15 @@
1
+ import { listProjectMemberRows } from "../db/workflow-db.js";
2
+ export function findProjectMember(projectId, memberName) {
3
+ const name = memberName;
4
+ if (name === "") {
5
+ return null;
6
+ }
7
+ const row = listProjectMemberRows({ projectId, id: "" }).find((entry) => entry.name === name);
8
+ if (!row) {
9
+ return null;
10
+ }
11
+ return {
12
+ name: row.name,
13
+ role: row.role,
14
+ };
15
+ }
@@ -0,0 +1,63 @@
1
+ export function flattenTemplateNodes(nodes) {
2
+ const result = [];
3
+ for (const node of nodes) {
4
+ result.push(node);
5
+ if (node.children.length > 0) {
6
+ for (const child of flattenTemplateNodes(node.children)) {
7
+ result.push(child);
8
+ }
9
+ }
10
+ }
11
+ return result;
12
+ }
13
+ export function countSpawnableTemplates(nodes) {
14
+ let count = 0;
15
+ for (const node of nodes) {
16
+ count += 1;
17
+ if (node.children.length > 0) {
18
+ count += countSpawnableTemplates(node.children);
19
+ }
20
+ }
21
+ return count;
22
+ }
23
+ function dependenciesMet(node, ctx) {
24
+ return node.dependsOn.every((templateId) => ctx.doneTemplateIds.has(templateId));
25
+ }
26
+ function stageIsReachable(ctx) {
27
+ return ctx.stagePosition <= ctx.activeStagePosition;
28
+ }
29
+ export function collectSpawnableTemplates(nodes, ctx) {
30
+ if (!stageIsReachable(ctx))
31
+ return [];
32
+ const result = [];
33
+ for (const node of nodes) {
34
+ if (!node)
35
+ continue;
36
+ if (!dependenciesMet(node, ctx))
37
+ continue;
38
+ if (!ctx.spawnedTemplateIds.has(node.id)) {
39
+ result.push(node);
40
+ continue;
41
+ }
42
+ if (node.children.length > 0) {
43
+ if (!ctx.doneTemplateIds.has(node.id))
44
+ continue;
45
+ for (const child of collectSpawnableTemplates(node.children, ctx)) {
46
+ result.push(child);
47
+ }
48
+ }
49
+ }
50
+ return result;
51
+ }
52
+ export function collectSiblingTemplateIds(nodes) {
53
+ const ids = [];
54
+ for (const node of nodes) {
55
+ ids.push(node.id);
56
+ if (node.children.length > 0) {
57
+ for (const childId of collectSiblingTemplateIds(node.children)) {
58
+ ids.push(childId);
59
+ }
60
+ }
61
+ }
62
+ return ids;
63
+ }
@@ -0,0 +1,93 @@
1
+ import { AppError } from "../errors/app-error.js";
2
+ import { isWorkDone } from "./work-status.js";
3
+ export const DONE_STAGE_ID = "done";
4
+ export function isDoneStage(stageId) {
5
+ return stageId === DONE_STAGE_ID;
6
+ }
7
+ export function isTaskClaimed(task) {
8
+ return task.claimedBy !== null && task.claimedBy !== "";
9
+ }
10
+ export function touchTask(task) {
11
+ task.updatedAt = new Date().toISOString();
12
+ }
13
+ export function sortTasks(tasks) {
14
+ return tasks.slice().sort((a, b) => {
15
+ const aTime = Date.parse(a.updatedAt || a.createdAt);
16
+ const bTime = Date.parse(b.updatedAt || b.createdAt);
17
+ if (!Number.isNaN(aTime) && !Number.isNaN(bTime) && aTime !== bTime) {
18
+ return bTime - aTime;
19
+ }
20
+ return b.id - a.id;
21
+ });
22
+ }
23
+ export function listSubtasks(tasks, parentId) {
24
+ return sortTasks(tasks.filter((task) => task.parentId === parentId));
25
+ }
26
+ export function listDescendantIds(tasks, parentId) {
27
+ const ids = [];
28
+ for (const child of listSubtasks(tasks, parentId)) {
29
+ ids.push(child.id);
30
+ for (const descendantId of listDescendantIds(tasks, child.id)) {
31
+ ids.push(descendantId);
32
+ }
33
+ }
34
+ return ids;
35
+ }
36
+ export function listEpicWorkflowTasks(tasks, epicId) {
37
+ return sortTasks(tasks.filter((task) => {
38
+ const resolved = task.epicId || resolveEpicId(tasks, task);
39
+ return resolved === epicId;
40
+ }));
41
+ }
42
+ export function resolveTaskStageId(tasks, task) {
43
+ if (task.stageId)
44
+ return task.stageId;
45
+ if (!task.parentId)
46
+ return null;
47
+ const parent = tasks.find((entry) => entry.id === task.parentId);
48
+ if (!parent)
49
+ return null;
50
+ return resolveTaskStageId(tasks, parent);
51
+ }
52
+ export function resolveEpicId(tasks, task) {
53
+ if (task.epicId !== null)
54
+ return task.epicId;
55
+ if (task.parentId === null)
56
+ return null;
57
+ const parent = tasks.find((entry) => entry.id === task.parentId);
58
+ if (!parent)
59
+ return task.parentId;
60
+ if (parent.parentId === null)
61
+ return parent.id;
62
+ return resolveEpicId(tasks, parent);
63
+ }
64
+ export function incompleteSubtasks(tasks, parentId) {
65
+ return listSubtasks(tasks, parentId).filter((task) => !isWorkDone(task));
66
+ }
67
+ export function assertCanAdvanceWorkStatus(tasks, task, nextStatus) {
68
+ if (nextStatus === "todo")
69
+ return;
70
+ if (!task.parentId)
71
+ return;
72
+ const parent = tasks.find((entry) => entry.id === task.parentId);
73
+ if (!parent || parent.parentId === null)
74
+ return;
75
+ if (!isWorkDone(parent)) {
76
+ throw new AppError("Parent task must be done first", 409);
77
+ }
78
+ }
79
+ export function assertCanCompleteTask(tasks, task) {
80
+ const blocked = incompleteSubtasks(tasks, task.id);
81
+ if (blocked.length === 0)
82
+ return;
83
+ throw new AppError(`Cannot complete: ${blocked.length} subtask(s) are not done`, 409, {
84
+ subtasks: blocked.map((entry) => ({
85
+ id: entry.id,
86
+ title: entry.title,
87
+ stageId: entry.stageId,
88
+ })),
89
+ });
90
+ }
91
+ export function canonicalDescription(task) {
92
+ return task.description;
93
+ }
@@ -0,0 +1,30 @@
1
+ import { isDoneStage } from "./task.js";
2
+ export const WORK_STATUSES = ["todo", "in_progress", "done"];
3
+ export function isWorkStatus(value) {
4
+ if (value === null) {
5
+ return false;
6
+ }
7
+ return WORK_STATUSES.includes(value);
8
+ }
9
+ export function resolveWorkStatus(task) {
10
+ if (task.workStatus && isWorkStatus(task.workStatus)) {
11
+ return task.workStatus;
12
+ }
13
+ if (task.parentId === null) {
14
+ return "todo";
15
+ }
16
+ if (isDoneStage(task.stageId)) {
17
+ return "done";
18
+ }
19
+ return "todo";
20
+ }
21
+ export function isWorkDone(task) {
22
+ return resolveWorkStatus(task) === "done";
23
+ }
24
+ export function workStatusLabel(status) {
25
+ if (status === "in_progress")
26
+ return "In progress";
27
+ if (status === "done")
28
+ return "Done";
29
+ return "Todo";
30
+ }
@@ -0,0 +1,186 @@
1
+ export const SUBTASK_SPAWN_STAGE_ID = "in-progress";
2
+ function isStringRecord(value) {
3
+ return value !== null && value instanceof Object && !Array.isArray(value);
4
+ }
5
+ export function parseStageRolesJson(raw) {
6
+ if (!raw)
7
+ return {};
8
+ try {
9
+ const parsed = JSON.parse(raw);
10
+ if (!isStringRecord(parsed))
11
+ return {};
12
+ const result = {};
13
+ for (const [key, value] of Object.entries(parsed)) {
14
+ const valueStr = value;
15
+ if (value !== null &&
16
+ String(valueStr) === valueStr &&
17
+ valueStr) {
18
+ result[key] = valueStr;
19
+ }
20
+ }
21
+ return result;
22
+ }
23
+ catch {
24
+ return {};
25
+ }
26
+ }
27
+ export function serializeStageRolesJson(roles) {
28
+ return JSON.stringify(roles);
29
+ }
30
+ export function parseRulesJson(raw) {
31
+ try {
32
+ const parsed = JSON.parse(raw);
33
+ if (!Array.isArray(parsed))
34
+ return [];
35
+ const result = [];
36
+ for (const item of parsed) {
37
+ if (item !== null &&
38
+ String(item) === item &&
39
+ item.length > 0) {
40
+ result.push(item);
41
+ }
42
+ }
43
+ return result;
44
+ }
45
+ catch {
46
+ return [];
47
+ }
48
+ }
49
+ export function resolveEpicDescription(input) {
50
+ const parts = [];
51
+ const description = input.description;
52
+ const purpose = input.purpose;
53
+ if (description)
54
+ parts.push(description);
55
+ if (purpose && purpose !== description)
56
+ parts.push(purpose);
57
+ const rules = parseRulesJson(input.rulesJson);
58
+ if (rules.length > 0)
59
+ parts.push(rules.join("\n\n"));
60
+ return parts.join("\n\n");
61
+ }
62
+ function resolveTaskDescription(row) {
63
+ const descCandidate = row.description;
64
+ if (row.description !== null && String(descCandidate) === descCandidate) {
65
+ return descCandidate;
66
+ }
67
+ return "";
68
+ }
69
+ function parseTemplateNode(item, index, fallbackTitle) {
70
+ let id;
71
+ const idCandidate = item.id;
72
+ if (item.id !== null && String(idCandidate) === idCandidate && idCandidate) {
73
+ id = idCandidate;
74
+ }
75
+ else {
76
+ id = `tpl-${index}`;
77
+ }
78
+ let title;
79
+ const titleCandidate = item.title;
80
+ if (item.title !== null && String(titleCandidate) === titleCandidate) {
81
+ title = titleCandidate;
82
+ }
83
+ else {
84
+ title = "";
85
+ }
86
+ if (!title)
87
+ return null;
88
+ const template = {
89
+ id,
90
+ title: title || fallbackTitle,
91
+ description: resolveTaskDescription(item),
92
+ assigneeRole: null,
93
+ dependsOn: [],
94
+ children: [],
95
+ };
96
+ const assigneeRoleCandidate = item.assigneeRole;
97
+ if (item.assigneeRole !== null &&
98
+ String(assigneeRoleCandidate) === assigneeRoleCandidate &&
99
+ assigneeRoleCandidate) {
100
+ template.assigneeRole = assigneeRoleCandidate;
101
+ }
102
+ if (Array.isArray(item.dependsOn)) {
103
+ const deps = [];
104
+ for (const entry of item.dependsOn) {
105
+ if (entry !== null &&
106
+ String(entry) === entry &&
107
+ entry.length > 0) {
108
+ deps.push(entry);
109
+ }
110
+ }
111
+ template.dependsOn = deps;
112
+ }
113
+ if (Array.isArray(item.children)) {
114
+ template.children = parseTaskTemplateNodes(item.children, "Task");
115
+ }
116
+ return template;
117
+ }
118
+ function parseTaskTemplateNodes(raw, fallbackTitle) {
119
+ const result = [];
120
+ for (let i = 0; i < raw.length; i += 1) {
121
+ const node = raw[i];
122
+ if (!node)
123
+ continue;
124
+ const parsed = parseTemplateNode(node, i, fallbackTitle);
125
+ if (parsed !== null)
126
+ result.push(parsed);
127
+ }
128
+ return result;
129
+ }
130
+ export function parseTaskTemplatesJson(raw) {
131
+ if (!raw)
132
+ return [];
133
+ try {
134
+ const parsed = JSON.parse(raw);
135
+ if (!Array.isArray(parsed))
136
+ return [];
137
+ return parseTaskTemplateNodes(parsed, "Task");
138
+ }
139
+ catch {
140
+ return [];
141
+ }
142
+ }
143
+ function serializeTemplateNode(template) {
144
+ let assigneeRole = null;
145
+ if (template.assigneeRole) {
146
+ assigneeRole = template.assigneeRole;
147
+ }
148
+ let children = null;
149
+ if (template.children.length > 0) {
150
+ children = template.children.map((child) => serializeTemplateNode(child));
151
+ }
152
+ return {
153
+ id: template.id,
154
+ title: template.title,
155
+ description: template.description,
156
+ assigneeRole,
157
+ dependsOn: template.dependsOn,
158
+ children,
159
+ };
160
+ }
161
+ export function serializeTaskTemplates(templates) {
162
+ return JSON.stringify(templates.map((template) => serializeTemplateNode(template)));
163
+ }
164
+ export function resolveStageTaskTemplates(input) {
165
+ return parseTaskTemplatesJson(input.taskTemplatesJson);
166
+ }
167
+ export function resolveStageTaskTemplateRoots(input) {
168
+ return resolveStageTaskTemplates(input);
169
+ }
170
+ function templateTreeHasTasks(nodes) {
171
+ return nodes.some((node) => node.title.length > 0);
172
+ }
173
+ export function stageHasActionableTemplates(input) {
174
+ const roots = resolveStageTaskTemplateRoots(input);
175
+ return templateTreeHasTasks(roots);
176
+ }
177
+ export function countSpawnableTemplates(templates) {
178
+ let count = 0;
179
+ for (const node of templates) {
180
+ count += 1;
181
+ if (node.children.length > 0) {
182
+ count += countSpawnableTemplates(node.children);
183
+ }
184
+ }
185
+ return count;
186
+ }
@@ -0,0 +1,73 @@
1
+ import { resolveStageTaskTemplateRoots, } from "./workflow-stage.js";
2
+ function walkTemplateNodes(nodes, stageId, parentTemplateId, out) {
3
+ for (const node of nodes) {
4
+ out[node.id] = {
5
+ templateId: node.id,
6
+ stageId,
7
+ parentTemplateId,
8
+ title: node.title,
9
+ description: node.description,
10
+ assigneeRole: node.assigneeRole,
11
+ workStatus: "todo",
12
+ comments: [],
13
+ taskId: null,
14
+ };
15
+ let children;
16
+ if (node.children !== null) {
17
+ children = node.children;
18
+ }
19
+ else {
20
+ children = [];
21
+ }
22
+ if (children.length > 0) {
23
+ walkTemplateNodes(children, stageId, node.id, out);
24
+ }
25
+ }
26
+ }
27
+ export function buildInitialWorkflowState(input) {
28
+ const nodes = {};
29
+ for (const stage of input.stages) {
30
+ const roots = resolveStageTaskTemplateRoots({
31
+ taskTemplatesJson: stage.taskTemplatesJson,
32
+ stageId: stage.id,
33
+ stageTitle: stage.title,
34
+ });
35
+ walkTemplateNodes(roots, stage.id, null, nodes);
36
+ }
37
+ return {
38
+ stageId: input.stageId,
39
+ nodes,
40
+ };
41
+ }
42
+ export function parseWorkflowStateData(raw) {
43
+ if (!raw)
44
+ return null;
45
+ try {
46
+ const row = JSON.parse(raw);
47
+ if (!row.nodes || !(row.nodes instanceof Object) || Array.isArray(row.nodes))
48
+ return null;
49
+ const nodes = row.nodes;
50
+ let stageId = null;
51
+ if (row.stageId !== null && String(row.stageId) === row.stageId) {
52
+ stageId = row.stageId || null;
53
+ }
54
+ return { stageId, nodes };
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ }
60
+ export function serializeWorkflowStateData(data) {
61
+ return JSON.stringify(data);
62
+ }
63
+ export function summarizeWorkflowStateNodes(data) {
64
+ return Object.values(data.nodes).map((node) => ({
65
+ templateId: node.templateId,
66
+ stageId: node.stageId,
67
+ parentTemplateId: node.parentTemplateId,
68
+ title: node.title,
69
+ taskId: node.taskId,
70
+ workStatus: node.workStatus,
71
+ commentCount: node.comments.length,
72
+ }));
73
+ }
@@ -0,0 +1,6 @@
1
+ export const DEFAULT_WORKFLOW_TEMPLATE_ID = "empty";
2
+ export function normalizeWorkflowTemplateId(value) {
3
+ if (value === "")
4
+ return DEFAULT_WORKFLOW_TEMPLATE_ID;
5
+ return value;
6
+ }
@@ -0,0 +1,24 @@
1
+ export class AppError extends Error {
2
+ statusCode;
3
+ details;
4
+ constructor(message, statusCode, details = null) {
5
+ super(message);
6
+ this.name = "AppError";
7
+ this.statusCode = statusCode;
8
+ this.details = details;
9
+ }
10
+ }
11
+ export function isAppError(error) {
12
+ return error instanceof AppError;
13
+ }
14
+ export function statusCodeFromError(error) {
15
+ if (isAppError(error))
16
+ return error.statusCode;
17
+ if (error instanceof Object && "statusCode" in error) {
18
+ const code = Number(error.statusCode);
19
+ if (Number.isFinite(code)) {
20
+ return code;
21
+ }
22
+ }
23
+ return 500;
24
+ }
@@ -0,0 +1,67 @@
1
+ import Fastify from "fastify";
2
+ import cors from "@fastify/cors";
3
+ import { ZodError } from "zod";
4
+ import { isAppError, statusCodeFromError } from "./errors/app-error.js";
5
+ import { createLogger } from "./logger.js";
6
+ import { config } from "./config.js";
7
+ import { healthRoutes } from "./routes/health.js";
8
+ import { projectRoutes } from "./routes/projects.js";
9
+ import { taskRoutes } from "./routes/tasks.js";
10
+ import { workflowRoutes } from "./routes/workflow.js";
11
+ import { workflowTemplateRoutes } from "./routes/workflow-templates.js";
12
+ import { libraryRoutes } from "./routes/library.js";
13
+ import { webRoutes } from "./routes/web.js";
14
+ import { authRoutes } from "./routes/auth.js";
15
+ import { adminUserRoutes } from "./routes/admin-users.js";
16
+ import { docsRoutes } from "./routes/docs.js";
17
+ import { refreshProjectRegistry, initProjectRegistry } from "./services/project-registry.js";
18
+ const logger = createLogger("backend");
19
+ async function main() {
20
+ const app = Fastify({ logger: false });
21
+ await app.register(cors, { origin: true });
22
+ initProjectRegistry();
23
+ refreshProjectRegistry();
24
+ healthRoutes(app);
25
+ await app.register((apiApp) => {
26
+ docsRoutes(apiApp);
27
+ authRoutes(apiApp);
28
+ adminUserRoutes(apiApp);
29
+ projectRoutes(apiApp);
30
+ taskRoutes(apiApp);
31
+ workflowRoutes(apiApp);
32
+ workflowTemplateRoutes(apiApp);
33
+ libraryRoutes(apiApp);
34
+ }, { prefix: "/api" });
35
+ await webRoutes(app);
36
+ app.setErrorHandler((error, _request, reply) => {
37
+ if (error instanceof ZodError) {
38
+ return reply.status(400).send({ error: error.message });
39
+ }
40
+ const handled = error;
41
+ const statusCode = statusCodeFromError(handled);
42
+ let message = "Internal error";
43
+ if (error instanceof Error) {
44
+ message = error.message;
45
+ }
46
+ if (statusCode >= 500) {
47
+ logger.error(message);
48
+ }
49
+ const body = { error: message, details: null };
50
+ if (isAppError(handled) && handled.details !== null) {
51
+ body.details = handled.details;
52
+ }
53
+ return reply.status(statusCode).send(body);
54
+ });
55
+ await app.listen({ port: config.port, host: "0.0.0.0" });
56
+ logger.info(`Server listening on port ${config.port}`);
57
+ logger.info(`Web UI: http://localhost:${config.port}/app/login`);
58
+ logger.info("Projects loaded from database");
59
+ }
60
+ main().catch((err) => {
61
+ let errMessage = String(err);
62
+ if (err instanceof Error) {
63
+ errMessage = err.message;
64
+ }
65
+ logger.error("Failed to start", { error: errMessage });
66
+ process.exit(1);
67
+ });
@@ -0,0 +1,24 @@
1
+ export const PROJECT_TAG_PATTERN = /^\[project:([^\]]+)\]\s*(?:\n\n?|\r\n\r\n?)?/;
2
+ export function withProjectMarker(projectId, description) {
3
+ const body = description;
4
+ if (body)
5
+ return `[project:${projectId}]\n\n${body}`;
6
+ return `[project:${projectId}]`;
7
+ }
8
+ export function extractProjectId(description) {
9
+ if (!description)
10
+ return null;
11
+ const match = description.match(PROJECT_TAG_PATTERN);
12
+ if (!match)
13
+ return null;
14
+ const id = match[1];
15
+ if (!id)
16
+ return null;
17
+ return id || null;
18
+ }
19
+ export function stripProjectMarker(description) {
20
+ if (!description)
21
+ return null;
22
+ const stripped = description.replace(PROJECT_TAG_PATTERN, "");
23
+ return stripped || null;
24
+ }
@@ -0,0 +1,34 @@
1
+ import { Buffer } from "node:buffer";
2
+ export function encodeInboxCursor(item) {
3
+ let activityAt = "";
4
+ if (item.activityAt !== null) {
5
+ activityAt = item.activityAt;
6
+ }
7
+ else if (item.createdAt !== null) {
8
+ activityAt = item.createdAt;
9
+ }
10
+ return Buffer.from(`${activityAt}|${item.taskId}`, "utf8").toString("base64url");
11
+ }
12
+ export function decodeInboxCursor(cursor) {
13
+ try {
14
+ const decoded = Buffer.from(cursor, "base64url").toString("utf8");
15
+ const lastPipe = decoded.lastIndexOf("|");
16
+ if (lastPipe < 0)
17
+ return null;
18
+ const activityAt = decoded.slice(0, lastPipe);
19
+ const taskId = Number(decoded.slice(lastPipe + 1));
20
+ if (!Number.isFinite(taskId) || taskId <= 0)
21
+ return null;
22
+ return { activityAt, taskId };
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ export function inboxItemBeforeCursor(item, cursorTime, cursorTaskId, parseActivityTime) {
29
+ const itemTime = parseActivityTime(item);
30
+ if (!Number.isNaN(itemTime) && !Number.isNaN(cursorTime) && itemTime !== cursorTime) {
31
+ return itemTime < cursorTime;
32
+ }
33
+ return item.taskId < cursorTaskId;
34
+ }
@@ -0,0 +1,15 @@
1
+ export function valueOrEmpty(value) {
2
+ if (value === null) {
3
+ return "";
4
+ }
5
+ return value;
6
+ }
7
+ export function emptyToNull(value) {
8
+ if (value === null) {
9
+ return null;
10
+ }
11
+ if (value === "") {
12
+ return null;
13
+ }
14
+ return value;
15
+ }
@@ -0,0 +1,29 @@
1
+ const LEVEL_ORDER = {
2
+ debug: 0,
3
+ info: 1,
4
+ warn: 2,
5
+ error: 3,
6
+ };
7
+ export function createLogger(name, minLevel = "info") {
8
+ const log = (level, message, meta = null) => {
9
+ if (LEVEL_ORDER[level] < LEVEL_ORDER[minLevel])
10
+ return;
11
+ let suffix = "";
12
+ if (meta !== null && Object.keys(meta).length > 0) {
13
+ suffix = ` ${JSON.stringify(meta)}`;
14
+ }
15
+ const line = `[${new Date().toISOString()}] [${level.toUpperCase()}] [${name}] ${message}${suffix}`;
16
+ if (level === "error" || level === "warn") {
17
+ process.stderr.write(`${line}\n`);
18
+ }
19
+ else {
20
+ process.stdout.write(`${line}\n`);
21
+ }
22
+ };
23
+ return {
24
+ debug: (message, meta = null) => log("debug", message, meta),
25
+ info: (message, meta = null) => log("info", message, meta),
26
+ warn: (message, meta = null) => log("warn", message, meta),
27
+ error: (message, meta = null) => log("error", message, meta),
28
+ };
29
+ }