baton-issue-tracker 1.3.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.
@@ -0,0 +1,171 @@
1
+ // init.js
2
+ // AI was consulted for large portions of this file.
3
+ // init command for the issue tracker
4
+ // usage: baton init [options] [<specs-path>]
5
+ // options:
6
+ // --force: force initialization even if the tracker is already initialized
7
+ // --specs <path>: path to the specs file (default: docs/specs/project-requirements.md)
8
+ // <specs-path>: optional positional path to specs (same as --specs)
9
+ //
10
+ // examples usage of specs flag:
11
+ // baton init --specs ./path/to/my-specs.md
12
+ // baton init --specs C:\full\path\to\specs.md
13
+
14
+ import { readFileSync, existsSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import { initDB } from '../db/index.js';
17
+ import { Priority } from '../models/issue.js';
18
+ import {
19
+ createIssue,
20
+ isTrackerReady,
21
+ clearAllIssues,
22
+ } from '../services/issuesService.js';
23
+ import {
24
+ getFlagValue,
25
+ getFirstPositionalArg,
26
+ hasFlag,
27
+ resolvePath,
28
+ } from '../util.js';
29
+
30
+ /**
31
+ * Default path to the specs file
32
+ */
33
+ const DEFAULT_SPECS_PATH = join('docs', 'specs', 'project-requirements.md');
34
+
35
+ /**
36
+ * Represents the parsed command-line flags for initialization.
37
+ * @typedef {Object} InitFlags
38
+ * @property {boolean} force - Whether to force initialization even if already initialized.
39
+ * @property {string | null} specs - The resolved path to the specifications file.
40
+ */
41
+
42
+ /**
43
+ * Parses the initialization flags.
44
+ * @param {string[]} args
45
+ * @returns {InitFlags}
46
+ */
47
+ function parseInitFlags(args) {
48
+ const specsFromFlag = getFlagValue(args, '--specs');
49
+ const positionalSpecs = getFirstPositionalArg(args, {
50
+ valueFlags: ['--specs'],
51
+ ignoreFlags: ['--force'],
52
+ });
53
+
54
+ if (specsFromFlag && positionalSpecs) {
55
+ throw new Error('Pass specs path with either --specs <path> or a positional path, not both.');
56
+ }
57
+
58
+ return {
59
+ force: hasFlag(args, '--force'),
60
+ specs: specsFromFlag ?? positionalSpecs,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Represents a discrete requirement parsed from the markdown specs.
66
+ * @typedef {Object} Requirement
67
+ * @property {string} title - The title or ID of the requirement (e.g., FR-1).
68
+ * @property {string} description - The detailed breakdown of the requirement.
69
+ * @property {string} priority - The assigned urgency level.
70
+ */
71
+
72
+ /**
73
+ * Parses the must requirements from the markdown file.
74
+ * Likely to be changed in future iterations when AI implementation is added.
75
+ * @param {string} markdown
76
+ * @returns {Requirement[]}
77
+ */
78
+ function parseMustRequirements(markdown) {
79
+ const issues = [];
80
+ const lines = markdown.split('\n');
81
+
82
+ for (const line of lines) {
83
+ if (!line.includes('| Must |')) {
84
+ continue;
85
+ }
86
+ const cells = line.split('|').map((cell) => cell.trim()).filter(Boolean);
87
+ if (cells.length < 3) {
88
+ continue;
89
+ }
90
+ const id = cells[0];
91
+ const requirement = cells[2];
92
+ if (!/^FR-/.test(id) || !requirement) {
93
+ continue;
94
+ }
95
+ issues.push({
96
+ title: id,
97
+ description: requirement,
98
+ priority: Priority.MEDIUM,
99
+ });
100
+ }
101
+
102
+ return issues;
103
+ }
104
+
105
+ /**
106
+ * Generates issues from the specs file.
107
+ * @param {string | null} specsPath
108
+ * @returns {Issue[]}
109
+ */
110
+ function generateIssuesFromSpecs(specsPath) {
111
+ const resolvedPath = resolvePath(specsPath, DEFAULT_SPECS_PATH);
112
+
113
+ if (!existsSync(resolvedPath)) {
114
+ throw new Error(
115
+ `Specs file not found at ${resolvedPath}. Pass --specs <path> or provide a positional path.`,
116
+ );
117
+ }
118
+
119
+ const markdown = readFileSync(resolvedPath, 'utf8');
120
+ const requirements = parseMustRequirements(markdown);
121
+
122
+ if (requirements.length === 0) {
123
+ return [];
124
+ }
125
+
126
+ return requirements.map((req) => createIssue(req));
127
+ }
128
+
129
+ /**
130
+ * Runs the init command
131
+ * @param {string[]} args - The command line arguments
132
+ * @returns {Promise<number>} The exit code: 0 is success, 1 is error.
133
+ */
134
+ export async function run(args = []) {
135
+ let flags;
136
+ try {
137
+ flags = parseInitFlags(args);
138
+ } catch (error) {
139
+ console.error(`Error: ${error.message}`);
140
+ console.error('Usage: baton init [--force] [--specs <path>] [<specs-path>]');
141
+ return 1;
142
+ }
143
+
144
+ if (isTrackerReady() && !flags.force) {
145
+ console.error('Error: Tracker already initialized in this directory.');
146
+ console.error('Run `baton init --force` to re-initialize.');
147
+ return 1;
148
+ }
149
+
150
+ initDB();
151
+
152
+ if (flags.force) {
153
+ clearAllIssues();
154
+ }
155
+
156
+ const resolvedSpecsPath = resolvePath(flags.specs, DEFAULT_SPECS_PATH);
157
+ const createdIssues = generateIssuesFromSpecs(flags.specs);
158
+
159
+ console.log(`Tracker initialized at ${join(process.cwd(), 'data', 'issues.db')}`);
160
+ console.log(`Specs: ${resolvedSpecsPath}`);
161
+ console.log(`Created ${createdIssues.length} issue(s) from product specs.`);
162
+ if (createdIssues.length > 0) {
163
+ console.log('Issues:');
164
+ for (const issue of createdIssues) {
165
+ console.log(` #${issue.id} [${issue.priority}] ${issue.title}`);
166
+ }
167
+ }
168
+ console.log('Run `baton status` to review progress or `baton next` to start work.');
169
+
170
+ return 0;
171
+ }
@@ -0,0 +1,67 @@
1
+ // list.js
2
+ // AI was consulted for some portions of this file.
3
+ // list command which allows the user to view a list of issues matching the filter flags
4
+ // Usage: baton list [--status <s>] [--priority <p>] [--limit <n>] [--offset <n>]
5
+ //
6
+ // Options:
7
+ // --status <s> Filter by status: open | in-progress | closed
8
+ // --priority <p> Filter by priority: low | medium | high
9
+ // --limit <n> Max results (default: 50)
10
+ // --offset <n> Skip first n results (default: 0)
11
+ // -h, --help Show this help
12
+
13
+ import { listIssues } from '../services/issuesService.js';
14
+ import { getFlagValue, getNumericFlag } from '../util.js';
15
+ import { parseArgs, printIssueTable, printTableHeader } from '../util.js';
16
+
17
+ /**
18
+ * Lists issues matching the filters and pagination settings
19
+ * @param {string[]} args - The command line arguments
20
+ * @returns {Promise<number>} The exit code: 0 is success, 1 is error
21
+ */
22
+
23
+ export async function run(args) {
24
+ const validFlags = ['--status', '--priority', '--limit', '--offset'];
25
+ // Check if user misspelled a flag
26
+ for (const arg of args) {
27
+ if (arg.startsWith('--')) {
28
+ if (!validFlags.includes(arg)) {
29
+ throw new Error(`Unknown flag provided: ${arg}. \nFlags: --status <s>, --priority <p>, --limit <n>, --offset <n>`);
30
+ }
31
+ }
32
+ }
33
+
34
+ try {
35
+ const options = parseArgs(args);
36
+
37
+ const result = await listIssues(options);
38
+
39
+ if (result.length == 0) {
40
+ console.log("No issues matching those filters were found.");
41
+ return 0;
42
+ } else {
43
+ //Logic for table formatting
44
+ const activeFilters = Object.entries(options)
45
+ .map(([key, value]) => `${key}: ${value}`)
46
+ .join(', ');
47
+
48
+ const filterLog = activeFilters ? `matching filters: [${activeFilters}]` : "with no filters applied";
49
+ console.log(`\nFound ${result.length} issue(s) ${filterLog}.`);
50
+ console.log("");
51
+
52
+ printTableHeader();
53
+ result.forEach(issue => printIssueTable(issue));
54
+ console.log("");
55
+ return 0;
56
+ }
57
+ } catch (error) {
58
+ // Separate error message for missing value
59
+ if (error.message.includes('Missing value')) {
60
+ console.error(`Usage Error: ${error.message}`);
61
+ } else {
62
+ console.error("Error: Failed to retrieve data.");
63
+ console.error(error.message);
64
+ }
65
+ return 1;
66
+ }
67
+ }
@@ -0,0 +1,63 @@
1
+ // loop.js
2
+ // AI was consulted for some portions of this file.
3
+ // loop command for the tracker which allows the AI agent to work autonomously for multiple steps.
4
+ // usage: baton loop [options]
5
+ // options:
6
+ // --steps <n>: number of steps to run (default: 1)
7
+ // -n <n>: alias for --steps <n>
8
+ // <n>: same as --steps <n>
9
+ // examples usage of steps flag:
10
+ // baton loop --steps 5
11
+ // baton loop -n 5
12
+
13
+ import { isTrackerReady } from '../services/issuesService.js';
14
+ import { getNumericFlag, reportTrackerNotReady } from '../util.js';
15
+ import { run as runNext } from './next.js';
16
+
17
+ /**
18
+ * Parses the flags in the command line argument
19
+ * @param {string[]} args - The command line arguments
20
+ * @returns {{ steps: number }}
21
+ */
22
+ function parseLoopFlags(args) {
23
+ const stepsFlag = getNumericFlag(args, '--steps') ?? getNumericFlag(args, '-n');
24
+ return { steps: stepsFlag ?? 1 };
25
+ }
26
+
27
+ /**
28
+ * Runs the loop command
29
+ * @param {string[]} args
30
+ * @returns {Promise<number>}
31
+ */
32
+ export async function run(args = []) {
33
+ const { steps } = parseLoopFlags(args);
34
+
35
+ if (!isTrackerReady()) {
36
+ reportTrackerNotReady();
37
+ return 1;
38
+ }
39
+
40
+ if (!Number.isInteger(steps) || steps < 1) {
41
+ console.error('Error: --steps must be a positive integer.');
42
+ console.error('Usage: baton loop --steps <n>');
43
+ return 1;
44
+ }
45
+
46
+ console.log(`Running baton for ${steps} step(s)...\n`);
47
+
48
+ let completed = 0;
49
+ for (let step = 1; step <= steps; step += 1) {
50
+ console.log(`--- Step ${step}/${steps} ---`);
51
+ const code = await runNext();
52
+ if (code !== 0) {
53
+ return code;
54
+ }
55
+ completed += 1;
56
+ if (step < steps) {
57
+ console.log('');
58
+ }
59
+ }
60
+
61
+ console.log(`\nCompleted ${completed} autonomous step(s).`);
62
+ return 0;
63
+ }
@@ -0,0 +1,46 @@
1
+ // next.js
2
+ // AI was consulted for some portions of this file.
3
+ // next command for the issue tracker which allows the user to manually move the AI from issue to issue.
4
+ // usage: baton next
5
+
6
+ import {
7
+ isTrackerReady,
8
+ selectNextIssue,
9
+ workOnIssue,
10
+ } from '../services/issuesService.js';
11
+ import { formatTimestamp, reportTrackerNotReady } from '../util.js';
12
+
13
+ /**
14
+ * Moves the AI agent to work on the next issue.
15
+ * Checks if the tracker is ready and if there are any open issues.
16
+ * Prompts user to initialize the tracker if it is not ready.
17
+ * Stats are updated on the issue through workOnIssue function from init.js.
18
+ * @returns {Promise<number>} The exit code: 0 is success, 1 is error.
19
+ */
20
+ export async function run() {
21
+ if (!isTrackerReady()) {
22
+ reportTrackerNotReady();
23
+ return 1;
24
+ }
25
+
26
+ const issue = selectNextIssue();
27
+ if (!issue) {
28
+ console.log('No open issues available. All work is complete or the backlog is empty.');
29
+ return 0;
30
+ }
31
+
32
+ const updated = workOnIssue(issue.id);
33
+
34
+ console.log('Working on next issue:');
35
+ console.log(` ID: #${updated.id}`);
36
+ console.log(` Title: ${updated.title}`);
37
+ console.log(` Priority: ${updated.priority}`);
38
+ console.log(` Status: ${updated.status}`);
39
+ console.log(` Attempts: ${updated.attemptNum}`);
40
+ console.log(` Created: ${formatTimestamp(updated.createdAt)}`);
41
+ if (updated.description) {
42
+ console.log(` Description: ${updated.description}`);
43
+ }
44
+
45
+ return 0;
46
+ }
@@ -0,0 +1,44 @@
1
+ // search.js
2
+ // AI was consulted for some portions of this file.
3
+ // search command which allows the user to search for issues by keywords (case insensitive).
4
+ // Usage: baton search "login bug"
5
+
6
+ import { searchIssues } from "../services/issuesService.js";
7
+ import { printIssueTable, printTableHeader } from '../util.js';
8
+
9
+ /**
10
+ * Searches for a title or description matching the command line argument
11
+ * @param {string[]} args - The command line arguments
12
+ * @returns {Promise<number>} The exit code: 0 is success, 1 is error.
13
+ */
14
+ export async function run(args) {
15
+ if (args.length === 0 || args === '') {
16
+ throw new Error("Invalid input: No search term entered.\nUsage: baton search <query>");
17
+ }
18
+
19
+ try {
20
+ const query = args.join(' ');
21
+ const result = await searchIssues(query);
22
+
23
+ // If no issues with matching title or desc was found
24
+ if (result.length == 0) {
25
+ console.log(`No issues containing "${query}" were found.`);
26
+ return 0;
27
+ }
28
+
29
+ console.log(`\nFound ${result.length} issue(s) containing "${query}":\n`);
30
+
31
+ // Logic for table formatting
32
+ printTableHeader();
33
+ result.forEach(issue => printIssueTable(issue));
34
+
35
+ console.log("");
36
+ return 0;
37
+ } catch (error) {
38
+ // Failed to query database
39
+ console.error("Error: Failed to retrieve data.");
40
+ console.error(error.message);
41
+ return 1;
42
+ }
43
+ }
44
+
@@ -0,0 +1,55 @@
1
+ // status.js
2
+ // AI was consulted for some portions of this file.
3
+ // status command for the issue tracker which allows the user to see the status of the tracker.
4
+ // usage: baton status
5
+
6
+ import {
7
+ isTrackerReady,
8
+ getIssueStats,
9
+ getAllIssues,
10
+ } from '../services/issuesService.js';
11
+ import { reportTrackerNotReady } from '../util.js';
12
+
13
+ /**
14
+ * Main function that runs the status command.
15
+ * @returns {Promise<number>} The exit code: 0 is success, 1 is error.
16
+ */
17
+ export async function run() {
18
+ if (!isTrackerReady()) {
19
+ reportTrackerNotReady();
20
+ return 1;
21
+ }
22
+
23
+ const stats = getIssueStats();
24
+ const issues = getAllIssues();
25
+ let progress;
26
+ if (stats.total === 0) {
27
+ progress = 0;
28
+ } else {
29
+ progress = Math.round((stats.closed / stats.total) * 100);
30
+ }
31
+
32
+ console.log('Issue Tracker Status');
33
+ console.log('──────────────────────────────────────────');
34
+ console.log(`Total issues: ${stats.total}`);
35
+ console.log(`Open: ${stats.open}`);
36
+ console.log(`In progress: ${stats.inProgress}`);
37
+ console.log(`In review: ${stats.inReview}`);
38
+ console.log(`Closed: ${stats.closed}`);
39
+ console.log(`Overall progress: ${progress}% complete`);
40
+
41
+ if (issues.length > 0) {
42
+ console.log('\nOpen issues by priority:');
43
+ const byPriority = { High: 0, Medium: 0, Low: 0 };
44
+ for (const issue of issues) {
45
+ if (issue.status === 'Open') {
46
+ byPriority[issue.priority] += 1;
47
+ }
48
+ }
49
+ console.log(` High: ${byPriority.High}`);
50
+ console.log(` Medium: ${byPriority.Medium}`);
51
+ console.log(` Low: ${byPriority.Low}`);
52
+ }
53
+
54
+ return 0;
55
+ }
@@ -0,0 +1,74 @@
1
+ // update.js
2
+ // AI was consulted for some portions of this file.
3
+ // update command allows the user to update data fields for an issue
4
+ // Usage: baton update <id> [options]
5
+ //
6
+ // Options:
7
+ // --title <text> New title
8
+ // --description <text> New description
9
+ // --token-limit <n> New token budget
10
+ // --status <s> open | in-progress | closed
11
+ // --priority <p> low | medium | high
12
+ // -h, --help Show this help
13
+ //
14
+ // Examples:
15
+ // baton update 3 --title "Revised title"
16
+ // baton update 7 --status closed --priority medium
17
+
18
+ import { updateIssue, getIssue } from "../services/issuesService.js";
19
+ import { parseArgs } from '../util.js';
20
+
21
+
22
+ /**
23
+ * Updates the specified fields for a given issue ID
24
+ * @param {string[]} args - The command line arguments
25
+ * @returns {Promise<number>} The exit code: 0 is success, 1 is error.
26
+ */
27
+ export async function run(args) {
28
+ if (args.length === 0 || args === '') {
29
+ throw new Error("Invalid input: No arguments entered.\nUsage: baton update <id> [options] \nOptions: --title <text>, --description <text>, --token-limit <n>, --status <s>, --priority <level>");
30
+ }
31
+
32
+ // Convert id argument from string to base-10 integer
33
+ const id = parseInt(args[0], 10);
34
+
35
+ if (isNaN(id)) {
36
+ throw new Error ("Invalid input: No ID entered. \nUsage: baton update <id> [options]\nOptions: --title <text>, --description <text>, --token-limit <n>, --status <s>, --priority <level>");
37
+ }
38
+
39
+ const validFlags = ['--title', '--description', '--token-limit', '--status', '--priority'];
40
+ // Check if user misspelled a flag
41
+ for (const arg of args) {
42
+ if (arg.startsWith('--')) {
43
+ if (!validFlags.includes(arg)) {
44
+ throw new Error(`Unknown flag provided: ${arg}. \nFlags: --title <text>, --description <text>, --token-limit <n>, --status <s>, --priority <level>`);
45
+ }
46
+ }
47
+ }
48
+
49
+ try {
50
+ const options = parseArgs(args.slice(1));
51
+
52
+ // Store the old issue fields for displaying purposes:
53
+ const oldIssue = getIssue(id);
54
+
55
+ const newIssue = await updateIssue(id, oldIssue, options);
56
+
57
+ console.log("");
58
+ // Compare and print the changes:
59
+ console.log(`Successfully updated issue #${id}:`);
60
+ for (const key in options) {
61
+ if (oldIssue[key] !== newIssue[key]) {
62
+ console.log(` ${key}: "${oldIssue[key]}" -> "${newIssue[key]}"`);
63
+ } else {
64
+ // If the entered argument matches the old data
65
+ console.log(` ${key}: No change (already set to "${newIssue[key]}")`);
66
+ }
67
+ }
68
+ console.log("");
69
+ return 0;
70
+ } catch (error) {
71
+ console.error(`Failed to update issue: ${error.message}`);
72
+ return 1;
73
+ }
74
+ }
@@ -0,0 +1,46 @@
1
+ // list.js
2
+ // AI was consulted for some portions of this file.
3
+ // view command which allows user to view all data fields for an issue
4
+ // Usage: baton view <id>>
5
+
6
+ import { getIssue } from '../services/issuesService.js';
7
+
8
+ /**
9
+ * Displays all issue fields for a given id #
10
+ * @param {string[]} args - The issue ID #
11
+ * @returns {Promise<number>} The exit code: 0 is success, 1 is error.
12
+ */
13
+ export async function run(args) {
14
+ // checks if id # argument is empty
15
+ if (args.length == 0) {
16
+ throw new Error(`Invalid input: Missing Issue ID.\nUsage: baton view <id>`);
17
+ }
18
+
19
+ const id = args.join(' ');
20
+
21
+ // checks if ID # argument isn't a number
22
+ if (isNaN(id)) {
23
+ throw new Error(`Invalid input: ID must be a number.\nUsage: baton view <id>`);
24
+ }
25
+
26
+ try {
27
+ const issue = await getIssue(id);
28
+
29
+ if (!issue) {
30
+ console.log(`No issue with ID #"${issue}" was found.`);
31
+ return 0;
32
+ }
33
+
34
+ console.log("");
35
+ Object.entries(issue).forEach(([key, value]) => {
36
+ console.log(`${key}: ${value}`);
37
+ });
38
+
39
+ console.log("");
40
+ return 0;
41
+ } catch (error) {
42
+ console.error("Error: Failed to retrieve data.");
43
+ console.error(error.message);
44
+ return 1;
45
+ }
46
+ }
@@ -0,0 +1,72 @@
1
+ import Database from "better-sqlite3";
2
+ import { drizzle } from "drizzle-orm/better-sqlite3";
3
+ import { migrate } from "drizzle-orm/better-sqlite3/migrator";
4
+ import fs from "fs";
5
+ import path from "path";
6
+
7
+ import * as schema from "../models/schema.js";
8
+
9
+ const dataDir = path.join(process.cwd(), ".baton");
10
+ const dbPath = path.join(dataDir, "baton.db");
11
+
12
+ if (!fs.existsSync(dataDir)) {
13
+ fs.mkdirSync(dataDir);
14
+ }
15
+
16
+ // Initialize better-sqlite3 connection
17
+ const sqliteConnection = new Database(dbPath);
18
+
19
+ // Initialize Drizzle ORM, passing in the schema for relational queries
20
+ // Using let here so we can reassign it in tests with an in-memory database instance.
21
+ let db = drizzle(sqliteConnection, { schema });
22
+
23
+ export function initDB() {
24
+ // Apply Migrations dynamically on CLI startup
25
+ // This looks for a folder named "drizzle" in your project root
26
+ const migrationsFolder = path.join(process.cwd(), "drizzle");
27
+
28
+ if (fs.existsSync(migrationsFolder)) {
29
+ migrate(db, { migrationsFolder });
30
+ } else {
31
+ console.warn("No migrations folder found. Tables may not be created.");
32
+ }
33
+
34
+ // Execute custom SQLite logic (Triggers)
35
+ // Drizzle does not manage triggers in its schema file, so we keep this raw.
36
+ sqliteConnection.exec(`
37
+ CREATE TRIGGER IF NOT EXISTS set_default_issue_title
38
+ AFTER INSERT ON issues
39
+ FOR EACH ROW
40
+ WHEN NEW.title = 'PENDING' OR NEW.title IS NULL
41
+ BEGIN
42
+ UPDATE issues
43
+ SET title = 'Issue #' || NEW.id
44
+ WHERE id = NEW.id;
45
+ END;
46
+ `);
47
+
48
+ sqliteConnection.exec(`
49
+ CREATE TRIGGER IF NOT EXISTS update_last_updated
50
+ AFTER UPDATE ON issues
51
+ FOR EACH ROW
52
+ WHEN NEW.last_updated IS OLD.last_updated
53
+ BEGIN
54
+ UPDATE issues
55
+ SET last_updated = CURRENT_TIMESTAMP
56
+ WHERE id = OLD.id;
57
+ END;
58
+ `);
59
+ }
60
+
61
+ export function getDB() {
62
+ return db;
63
+ }
64
+
65
+ export function getSQLiteDB() {
66
+ return sqliteConnection;
67
+ }
68
+
69
+ // function for the test suite mocking
70
+ export function setTestDB(testDbInstance) {
71
+ db = testDbInstance;
72
+ }
@@ -0,0 +1 @@
1
+ // temp file for linter to pass
@@ -0,0 +1,23 @@
1
+ export const Action = Object.freeze({
2
+ STATE_CHANGE: "state_change",
3
+ PRIORITY_CHANGE: "priority_change",
4
+ EDIT: "edit",
5
+ READ: "read",
6
+ CREATION: "creation",
7
+ DELETION: "deletion",
8
+ REJECT: "rejection",
9
+ });
10
+
11
+ export class ActivityLog {
12
+ constructor({
13
+ logId = null,
14
+ issueId,
15
+ action,
16
+ createdAt = new Date().toISOString(),
17
+ } = {}) {
18
+ this.logId = logId;
19
+ this.issueId = issueId;
20
+ this.action = action;
21
+ this.createdAt = createdAt;
22
+ }
23
+ }