baton-issue-tracker 1.5.0 → 1.7.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.
@@ -0,0 +1,10 @@
1
+ CREATE TABLE `agents` (
2
+ `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3
+ `name` text NOT NULL,
4
+ `type` text NOT NULL
5
+ );
6
+ --> statement-breakpoint
7
+ CREATE UNIQUE INDEX `agents_name_unique` ON `agents` (`name`);--> statement-breakpoint
8
+ ALTER TABLE `activity` ADD `actor_id` integer REFERENCES agents(id);--> statement-breakpoint
9
+ ALTER TABLE `issues` ADD `assignee_id` integer REFERENCES agents(id);--> statement-breakpoint
10
+ ALTER TABLE `issues` DROP COLUMN `assignees`;
@@ -0,0 +1,229 @@
1
+ {
2
+ "version": "6",
3
+ "dialect": "sqlite",
4
+ "id": "d89cefa6-9c90-4882-8fab-f6322fd1f45e",
5
+ "prevId": "af5467aa-0757-4c91-8afb-c6f0202dd90c",
6
+ "tables": {
7
+ "activity": {
8
+ "name": "activity",
9
+ "columns": {
10
+ "log_id": {
11
+ "name": "log_id",
12
+ "type": "integer",
13
+ "primaryKey": true,
14
+ "notNull": true,
15
+ "autoincrement": true
16
+ },
17
+ "issue_id": {
18
+ "name": "issue_id",
19
+ "type": "integer",
20
+ "primaryKey": false,
21
+ "notNull": false,
22
+ "autoincrement": false
23
+ },
24
+ "actor_id": {
25
+ "name": "actor_id",
26
+ "type": "integer",
27
+ "primaryKey": false,
28
+ "notNull": false,
29
+ "autoincrement": false
30
+ },
31
+ "created_at": {
32
+ "name": "created_at",
33
+ "type": "text",
34
+ "primaryKey": false,
35
+ "notNull": true,
36
+ "autoincrement": false,
37
+ "default": "CURRENT_TIMESTAMP"
38
+ },
39
+ "action": {
40
+ "name": "action",
41
+ "type": "text",
42
+ "primaryKey": false,
43
+ "notNull": true,
44
+ "autoincrement": false
45
+ },
46
+ "details": {
47
+ "name": "details",
48
+ "type": "text",
49
+ "primaryKey": false,
50
+ "notNull": false,
51
+ "autoincrement": false
52
+ }
53
+ },
54
+ "indexes": {},
55
+ "foreignKeys": {
56
+ "activity_actor_id_agents_id_fk": {
57
+ "name": "activity_actor_id_agents_id_fk",
58
+ "tableFrom": "activity",
59
+ "tableTo": "agents",
60
+ "columnsFrom": [
61
+ "actor_id"
62
+ ],
63
+ "columnsTo": [
64
+ "id"
65
+ ],
66
+ "onDelete": "no action",
67
+ "onUpdate": "no action"
68
+ }
69
+ },
70
+ "compositePrimaryKeys": {},
71
+ "uniqueConstraints": {},
72
+ "checkConstraints": {
73
+ "activity_action_check": {
74
+ "name": "activity_action_check",
75
+ "value": "\"activity\".\"action\" IN ('state_change', 'priority_change', 'edit', 'read', 'creation', 'deletion', 'rejection')"
76
+ }
77
+ }
78
+ },
79
+ "agents": {
80
+ "name": "agents",
81
+ "columns": {
82
+ "id": {
83
+ "name": "id",
84
+ "type": "integer",
85
+ "primaryKey": true,
86
+ "notNull": true,
87
+ "autoincrement": true
88
+ },
89
+ "name": {
90
+ "name": "name",
91
+ "type": "text",
92
+ "primaryKey": false,
93
+ "notNull": true,
94
+ "autoincrement": false
95
+ },
96
+ "type": {
97
+ "name": "type",
98
+ "type": "text",
99
+ "primaryKey": false,
100
+ "notNull": true,
101
+ "autoincrement": false
102
+ }
103
+ },
104
+ "indexes": {
105
+ "agents_name_unique": {
106
+ "name": "agents_name_unique",
107
+ "columns": [
108
+ "name"
109
+ ],
110
+ "isUnique": true
111
+ }
112
+ },
113
+ "foreignKeys": {},
114
+ "compositePrimaryKeys": {},
115
+ "uniqueConstraints": {},
116
+ "checkConstraints": {}
117
+ },
118
+ "issues": {
119
+ "name": "issues",
120
+ "columns": {
121
+ "id": {
122
+ "name": "id",
123
+ "type": "integer",
124
+ "primaryKey": true,
125
+ "notNull": true,
126
+ "autoincrement": true
127
+ },
128
+ "created_at": {
129
+ "name": "created_at",
130
+ "type": "text",
131
+ "primaryKey": false,
132
+ "notNull": true,
133
+ "autoincrement": false,
134
+ "default": "CURRENT_TIMESTAMP"
135
+ },
136
+ "last_updated": {
137
+ "name": "last_updated",
138
+ "type": "text",
139
+ "primaryKey": false,
140
+ "notNull": true,
141
+ "autoincrement": false,
142
+ "default": "CURRENT_TIMESTAMP"
143
+ },
144
+ "attempt_num": {
145
+ "name": "attempt_num",
146
+ "type": "integer",
147
+ "primaryKey": false,
148
+ "notNull": true,
149
+ "autoincrement": false,
150
+ "default": 0
151
+ },
152
+ "title": {
153
+ "name": "title",
154
+ "type": "text",
155
+ "primaryKey": false,
156
+ "notNull": true,
157
+ "autoincrement": false,
158
+ "default": "'PENDING'"
159
+ },
160
+ "status": {
161
+ "name": "status",
162
+ "type": "text",
163
+ "primaryKey": false,
164
+ "notNull": true,
165
+ "autoincrement": false,
166
+ "default": "'Open'"
167
+ },
168
+ "priority": {
169
+ "name": "priority",
170
+ "type": "text",
171
+ "primaryKey": false,
172
+ "notNull": false,
173
+ "autoincrement": false,
174
+ "default": "'Low'"
175
+ },
176
+ "token_limit": {
177
+ "name": "token_limit",
178
+ "type": "integer",
179
+ "primaryKey": false,
180
+ "notNull": false,
181
+ "autoincrement": false
182
+ },
183
+ "description": {
184
+ "name": "description",
185
+ "type": "text",
186
+ "primaryKey": false,
187
+ "notNull": false,
188
+ "autoincrement": false
189
+ },
190
+ "assignee_id": {
191
+ "name": "assignee_id",
192
+ "type": "integer",
193
+ "primaryKey": false,
194
+ "notNull": false,
195
+ "autoincrement": false
196
+ }
197
+ },
198
+ "indexes": {},
199
+ "foreignKeys": {
200
+ "issues_assignee_id_agents_id_fk": {
201
+ "name": "issues_assignee_id_agents_id_fk",
202
+ "tableFrom": "issues",
203
+ "tableTo": "agents",
204
+ "columnsFrom": [
205
+ "assignee_id"
206
+ ],
207
+ "columnsTo": [
208
+ "id"
209
+ ],
210
+ "onDelete": "no action",
211
+ "onUpdate": "no action"
212
+ }
213
+ },
214
+ "compositePrimaryKeys": {},
215
+ "uniqueConstraints": {},
216
+ "checkConstraints": {}
217
+ }
218
+ },
219
+ "views": {},
220
+ "enums": {},
221
+ "_meta": {
222
+ "schemas": {},
223
+ "tables": {},
224
+ "columns": {}
225
+ },
226
+ "internal": {
227
+ "indexes": {}
228
+ }
229
+ }
@@ -15,6 +15,13 @@
15
15
  "when": 1780114722528,
16
16
  "tag": "0001_salty_luke_cage",
17
17
  "breakpoints": true
18
+ },
19
+ {
20
+ "idx": 2,
21
+ "version": "6",
22
+ "when": 1780293597234,
23
+ "tag": "0002_breezy_wiccan",
24
+ "breakpoints": true
18
25
  }
19
26
  ]
20
27
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "baton-issue-tracker",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "A CLI issue tracker for AI agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,6 +37,7 @@
37
37
  "node": ">=22.0.0"
38
38
  },
39
39
  "dependencies": {
40
+ "@inquirer/prompts": "^8.5.1",
40
41
  "better-sqlite3": "^12.10.0",
41
42
  "drizzle-orm": "^0.45.2"
42
43
  }
package/source/cli.js CHANGED
@@ -23,7 +23,12 @@ import { run as runView} from './commands/view.js';
23
23
  import { run as runSearch } from './commands/search.js';
24
24
  import { run as runList } from './commands/list.js';
25
25
  import { run as runCreate } from './commands/create.js'
26
- import { run as runUpdate } from './commands/update.js'
26
+ import { run as runUpdate } from './commands/update.js';
27
+ import { run as runPriority } from './commands/priority.js';
28
+ import { run as runLog } from './commands/log.js';
29
+ import { run as runRegister } from './commands/register.js';
30
+
31
+ import { authenticateContext } from './services/authService.js';
27
32
 
28
33
  const HELP = `baton — AI agent issue tracker CLI
29
34
 
@@ -32,6 +37,7 @@ Usage:
32
37
 
33
38
  Commands:
34
39
  init Initialize storage and seed issues from product specs
40
+ register Register a new AI agent or human user
35
41
  next Work on the highest-priority open issue
36
42
  loop Run the agent autonomously for multiple steps
37
43
  status Show issue counts and overall progress
@@ -40,7 +46,9 @@ Commands:
40
46
  list Lists issues filtered by status and priority
41
47
  create Creates an issue with specified fields
42
48
  approve Move an issue from in-review to closed
43
- update Updates an issue's specified fields
49
+ priority Set an issue's priority level
50
+ update Updates an issue's specified fields
51
+ log Show activity history for an issue
44
52
 
45
53
  Options:
46
54
  init --force Re-initialize an existing tracker database
@@ -48,6 +56,8 @@ Options:
48
56
  init --json Output as JSON (for AI agents)
49
57
  init <path> Same as --specs <path> (positional)
50
58
  Default specs: docs/specs/project-requirements.md
59
+ register --name <name> Name of the agent or user
60
+ register --type <type> agent | human (default: agent)
51
61
  loop --steps <n> Number of autonomous steps (alias: -n)
52
62
  loop -n <n>
53
63
  loop --json Output as JSON (for AI agents)
@@ -66,19 +76,21 @@ Options:
66
76
  create --token-limit <n> Optional token budget for this issue
67
77
  create --json Output as JSON (for AI agents)
68
78
  approve <id> [--json]
79
+ priority <id> <level> [--json] low | medium | high
69
80
  update --title <text> New title
70
81
  update --description <text> New description
71
82
  update --token-limit <n> New token budget
72
83
  update --status <s> open | in-progress | closed
73
84
  update --priority <level> low | medium | high
74
85
  update --json Output as JSON (for AI agents)
75
-
86
+ log <id> [--json]
76
87
 
77
88
  Examples:
78
89
  baton init
79
90
  baton init --specs ./my-specs.md
80
91
  baton init ./my-specs.md
81
92
  baton init --force
93
+ baton register --name claude-dev --type agent
82
94
  baton next
83
95
  baton loop --steps 5
84
96
  baton status
@@ -90,8 +102,11 @@ Examples:
90
102
  baton create --title "Fix login bug" --priority high
91
103
  baton create --title "Refactor auth" --description "Clean up JWT logic" --token-limit 4000
92
104
  baton approve 5
105
+ baton priority 5 high
106
+ baton priority 3 low
93
107
  baton update 3 --title "Revised title"
94
108
  baton update 7 --status closed --priority medium
109
+ baton log 5
95
110
  `;
96
111
 
97
112
  /**
@@ -107,8 +122,12 @@ async function main() {
107
122
  return;
108
123
  }
109
124
 
125
+ // Authenticate the user and context before executing any command.
126
+ authenticateContext(command);
127
+
110
128
  const handlers = {
111
129
  init: () => runInit(args),
130
+ register: () => runRegister(args),
112
131
  next: () => runNext(args),
113
132
  loop: () => runLoop(args),
114
133
  status: () => runStatus(args),
@@ -116,12 +135,19 @@ async function main() {
116
135
  search: () => runSearch(args),
117
136
  list: () => runList(args),
118
137
  approve: () => runApprove(args),
138
+ priority: () => runPriority(args),
119
139
  create: () => runCreate(args),
120
- update: () => runUpdate(args)
140
+ update: () => runUpdate(args),
141
+ log: () => runLog(args),
121
142
  };
122
-
143
+
123
144
  const handler = handlers[command];
124
145
  if (!handler) {
146
+ if (wantsHelp(args)) {
147
+ console.log(HELP);
148
+ process.exit(0);
149
+ return;
150
+ }
125
151
  console.error(`Error: Unknown command "${command}".`);
126
152
  console.error('Run `baton --help` for usage.');
127
153
  process.exit(1);
@@ -1,52 +1,237 @@
1
1
  // create.js
2
2
  // AI was consulted for some portions of this file.
3
- // create command allows the user to create an issue
3
+ // create command allows the user to create an issue
4
4
  // Usage: baton create --title <text> --description <text> --priority <level> --token-limit <n>
5
- //
5
+ // baton create (launches interactive prompt mode)
6
+ //
6
7
  // Examples:
7
- // baton create --title "Fix login bug" --priority high
8
- // baton create --title "Refactor auth" --description "Clean up JWT logic" --token-limit 4000
8
+ // baton create --title "Fix login bug" --priority high
9
+ // baton create --title "Refactor auth" --description "Clean up JWT logic" --token-limit 4000
10
+ // baton create # interactive mode
11
+ //
9
12
  // Options:
10
- // --title <text> Issue title (defaults to "Issue #<id>" if omitted)
11
- // --description <text> Issue description
12
- // --priority <level> low | medium | high (default: low)
13
- // --token-limit <n> Optional token budget for this issue
14
- // -h, --help Show this help
13
+ // --title <text> Issue title (defaults to "Issue #<id>" if omitted)
14
+ // --description <text> Issue description
15
+ // --priority <level> low | medium | high (default: low)
16
+ // --token-limit <n> Optional token budget for this issue
17
+ // -h, --help Show this help
15
18
 
16
19
  import { createIssue } from "../services/issuesService.js";
17
- import { getFlagValue, getNumericFlag } from "../util.js";
18
- import { hasFlag, parseArgs, renderOutput, serializeIssue } from '../util.js';
20
+ import { hasFlag, parseArgs, renderOutput, serializeIssue } from "../util.js";
21
+ import { issueSchema } from "../models/schema.js";
22
+ import { input, select, confirm, editor } from "@inquirer/prompts";
23
+ import { spawnSync } from "child_process";
24
+ import { writeFileSync, readFileSync, unlinkSync } from "fs";
25
+ import { tmpdir } from "os";
26
+ import { join } from "path";
27
+
28
+ const ALLOWED_CREATE_FIELDS = ['title', 'priority', 'tokenLimit', 'description'];
29
+
30
+ const VALID_FLAGS = new Set([
31
+ ...ALLOWED_CREATE_FIELDS.map(key => issueSchema[key].flag),
32
+ "--json",
33
+ ]);
34
+
35
+ // Build select choices from issueSchema.priority.values so the list stays in
36
+ // sync with the Priority enum without any duplication.
37
+ const PRIORITY_HINTS = {
38
+ Low: "routine work, no urgency",
39
+ Medium: "should be resolved this week",
40
+ High: "blocking or time-sensitive",
41
+ };
42
+ const PRIORITY_CHOICES = issueSchema.priority.values.map((v) => ({
43
+ name: `${v.padEnd(6)} -- ${PRIORITY_HINTS[v] ?? v}`,
44
+ value: v,
45
+ }));
46
+ const DEFAULT_PRIORITY = issueSchema.priority.values[0];
47
+
48
+ const DESCRIPTION_PLACEHOLDER = [
49
+ "## What is the issue?",
50
+ "",
51
+ "## Steps to reproduce (if applicable)",
52
+ "",
53
+ "## Expected vs actual behaviour",
54
+ "",
55
+ "## Additional context",
56
+ "",
57
+ ].join("\n");
58
+
59
+ /**
60
+ * Opens the user's $EDITOR with an optional seed template and returns the
61
+ * saved contents, or null if the user left it unchanged / empty.
62
+ *
63
+ * Falls back gracefully: if no $EDITOR is set the inquirer `editor` prompt is
64
+ * used instead (which has its own built-in text area).
65
+ *
66
+ * @param {string} template Initial file content shown to the user.
67
+ * @returns {Promise<string|null>}
68
+ */
69
+ async function openEditorForDescription(template = DESCRIPTION_PLACEHOLDER) {
70
+ const editorBin = process.env.EDITOR || process.env.VISUAL;
71
+
72
+ if (!editorBin) {
73
+ // If no $EDITOR set, then fall back to @inquirer/prompts built-in editor widget
74
+ const result = await editor({
75
+ message:
76
+ "Description (opens in-terminal editor -- save & quit when done):",
77
+ default: template,
78
+ waitForUseInput: false,
79
+ });
80
+ const cleaned = result.trim();
81
+ return cleaned && cleaned !== template.trim() ? cleaned : null;
82
+ }
83
+
84
+ // Write template to a temp file, open $EDITOR, read back the result
85
+ const tmpPath = join(tmpdir(), `baton-issue-${Date.now()}.md`);
86
+ writeFileSync(tmpPath, template, "utf8");
87
+
88
+ const result = spawnSync(editorBin, [tmpPath], { stdio: "inherit" });
89
+
90
+ if (result.error) {
91
+ unlinkSync(tmpPath);
92
+ throw new Error(
93
+ `Could not open $EDITOR (${editorBin}): ${result.error.message}`,
94
+ );
95
+ }
96
+
97
+ const saved = readFileSync(tmpPath, "utf8");
98
+ unlinkSync(tmpPath);
99
+
100
+ const cleaned = saved.trim();
101
+ // Return null if the user saved without changing anything
102
+ return cleaned && cleaned !== template.trim() ? cleaned : null;
103
+ }
104
+
105
+ /**
106
+ * Guides the user through issue creation with interactive prompts.
107
+ * @returns {Promise<object>} Options object ready to pass to createIssue()
108
+ */
109
+ async function runInteractiveMode() {
110
+ console.log("\n Baton -- interactive issue creation\n");
111
+
112
+ // Title
113
+ const title = await input({
114
+ message: "Title:",
115
+ required: true,
116
+ validate: (val) => val.trim().length > 0 || "Title cannot be empty.",
117
+ });
118
+
119
+ // Priority
120
+ const priority = await select({
121
+ message: "Priority:",
122
+ choices: PRIORITY_CHOICES,
123
+ default: DEFAULT_PRIORITY,
124
+ });
125
+
126
+ // Token limit
127
+ const wantsTokenLimit = await confirm({
128
+ message: "Set a token limit for this issue?",
129
+ default: false,
130
+ });
131
+
132
+ let tokenLimit = null;
133
+ if (wantsTokenLimit) {
134
+ const raw = await input({
135
+ message: "Token limit (positive integer):",
136
+ validate: (val) => {
137
+ const n = Number(val);
138
+ return (Number.isInteger(n) && n > 0) || "Must be a positive integer.";
139
+ },
140
+ });
141
+ tokenLimit = Number(raw);
142
+ }
143
+
144
+ // Description
145
+ const wantsDescription = await confirm({
146
+ message: "Add a description?",
147
+ default: true,
148
+ });
149
+
150
+ let description = null;
151
+ if (wantsDescription) {
152
+ const editorBin = process.env.EDITOR || process.env.VISUAL;
153
+ const hint = editorBin ? `opens ${editorBin}` : "in-terminal editor";
154
+ console.log(
155
+ ` -> ${hint} -- fill in what's relevant, save and quit when done.\n`,
156
+ );
157
+ description = await openEditorForDescription();
158
+ }
159
+
160
+ // Preview & confirm
161
+ console.log("\n" + "-".repeat(48));
162
+ console.log(` Title : ${title}`);
163
+ console.log(` Priority : ${priority}`);
164
+ console.log(` Token limit: ${tokenLimit ?? "(none)"}`);
165
+ if (description) {
166
+ const preview = description.split("\n").slice(0, 3).join(" ").slice(0, 72);
167
+ console.log(
168
+ ` Description: ${preview}${description.length > 72 ? "..." : ""}`,
169
+ );
170
+ } else {
171
+ console.log(` Description: (none)`);
172
+ }
173
+ console.log("-".repeat(48) + "\n");
174
+
175
+ const confirmed = await confirm({
176
+ message: "Create this issue?",
177
+ default: true,
178
+ });
179
+ if (!confirmed) {
180
+ console.log("Aborted -- no issue created.");
181
+ process.exit(0);
182
+ }
183
+
184
+ return { title, priority, tokenLimit, description };
185
+ }
19
186
 
20
187
  /**
21
188
  * Initializes a new issue in the database with the specified fields
189
+ * Drops into interactive mode when no flags are provided
190
+ *
22
191
  * @param {string[]} args - The command line arguments
23
- * @returns {Promise<number>} The exit code: 0 is success, 1 is error.
192
+ * @returns {Promise<number>} The exit code: 0 is success, 1 is error
24
193
  */
25
194
  export async function run(args) {
26
- const isJson = hasFlag(args, '--json');
27
- const validFlags = ['--title', '--description', '--priority', '--token-limit', '--json'];
28
- // Check if user misspelled a flag
29
- for (const arg of args) {
30
- if (arg.startsWith('--')) {
31
- if (!validFlags.includes(arg)) {
32
- throw new Error(`Unknown flag provided: ${arg}. \nFlags: --title <text>, --description <text>, --priority <level>, --token-limit <n>`);
33
- }
34
- }
195
+ const isJson = hasFlag(args, "--json");
196
+
197
+ // Flag validation -- VALID_FLAGS is derived from issueSchema, so this stays
198
+ // current automatically when fields are added or renamed there
199
+ const providedFlags = args.filter((a) => a.startsWith("--"));
200
+ for (const flag of providedFlags) {
201
+ if (!VALID_FLAGS.has(flag)) {
202
+ const knownFlags = [...VALID_FLAGS].join(", ");
203
+ throw new Error(
204
+ `Unknown flag: ${flag}\nValid flags: ${knownFlags}\n` +
205
+ `Tip: run \`baton create\` with no flags to use interactive mode.`,
206
+ );
35
207
  }
208
+ }
209
+
210
+ try {
211
+ // Interactive mode: no flags provided (human at a keyboard)
212
+ // Flag mode: at least one flag present (scripts, CI, power users)
213
+ const nonJsonFlags = providedFlags.filter((f) => f !== "--json");
214
+ const isInteractive = nonJsonFlags.length === 0;
36
215
 
37
- try {
38
- const options = parseArgs(args);
216
+ const options = isInteractive
217
+ ? await runInteractiveMode()
218
+ : parseArgs(args);
39
219
 
40
- const issue = await createIssue(options);
41
- const envelope = { status: 'success', issue: serializeIssue(issue) };
220
+ const issue = await createIssue(options);
221
+ const envelope = { status: "success", issue: serializeIssue(issue) };
42
222
 
43
- renderOutput(isJson, envelope, () => {
44
- console.log(`Successfully created issue #${issue.id}: "${issue.title}"`);
45
- });
223
+ renderOutput(isJson, envelope, () => {
224
+ console.log(`\nCreated issue #${issue.id}: "${issue.title}"`);
225
+ });
46
226
 
47
- return 0;
48
- } catch (error) {
49
- console.error(`Failed to create issue: ${error.message}`);
50
- return 1;
227
+ return 0;
228
+ } catch (error) {
229
+ // when user hits Ctrl+C
230
+ if (error.name === "ExitPromptError") {
231
+ console.log("\nAborted.");
232
+ return 0;
51
233
  }
234
+ console.error(`Failed to create issue: ${error.message}`);
235
+ return 1;
236
+ }
52
237
  }