@proletariat/cli 0.3.34 → 0.3.35

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 (118) hide show
  1. package/dist/commands/agent/auth.d.ts +3 -1
  2. package/dist/commands/agent/auth.js +8 -11
  3. package/dist/commands/agent/index.js +11 -2
  4. package/dist/commands/agent/staff/add.d.ts +1 -0
  5. package/dist/commands/agent/staff/add.js +1 -0
  6. package/dist/commands/agent/staff/index.d.ts +15 -0
  7. package/dist/commands/agent/staff/index.js +83 -0
  8. package/dist/commands/agent/staff/list.d.ts +1 -0
  9. package/dist/commands/agent/staff/list.js +1 -0
  10. package/dist/commands/agent/staff/remove.d.ts +1 -0
  11. package/dist/commands/agent/staff/remove.js +1 -0
  12. package/dist/commands/agent/themes/add-names.d.ts +1 -0
  13. package/dist/commands/agent/themes/add-names.js +1 -0
  14. package/dist/commands/agent/themes/create.d.ts +1 -0
  15. package/dist/commands/agent/themes/create.js +1 -0
  16. package/dist/commands/agent/themes/index.d.ts +10 -0
  17. package/dist/commands/agent/themes/index.js +144 -0
  18. package/dist/commands/agent/themes/list.d.ts +1 -0
  19. package/dist/commands/agent/themes/list.js +1 -0
  20. package/dist/commands/agent/themes/set.d.ts +1 -0
  21. package/dist/commands/agent/themes/set.js +1 -0
  22. package/dist/commands/agents/themes/add-names.d.ts +1 -0
  23. package/dist/commands/agents/themes/add-names.js +1 -0
  24. package/dist/commands/agents/themes/create.d.ts +1 -0
  25. package/dist/commands/agents/themes/create.js +1 -0
  26. package/dist/commands/agents/themes/list.d.ts +1 -0
  27. package/dist/commands/agents/themes/list.js +1 -0
  28. package/dist/commands/category/list.js +1 -1
  29. package/dist/commands/label/create.d.ts +20 -0
  30. package/dist/commands/label/create.js +56 -0
  31. package/dist/commands/label/delete.d.ts +17 -0
  32. package/dist/commands/label/delete.js +31 -0
  33. package/dist/commands/label/group/create.d.ts +20 -0
  34. package/dist/commands/label/group/create.js +54 -0
  35. package/dist/commands/label/group/list.d.ts +14 -0
  36. package/dist/commands/label/group/list.js +51 -0
  37. package/dist/commands/label/index.d.ts +15 -0
  38. package/dist/commands/label/index.js +58 -0
  39. package/dist/commands/label/list.d.ts +16 -0
  40. package/dist/commands/label/list.js +82 -0
  41. package/dist/commands/link/list.js +3 -2
  42. package/dist/commands/mcp-server.js +2 -1
  43. package/dist/commands/phase/template/apply.d.ts +26 -0
  44. package/dist/commands/phase/template/apply.js +14 -0
  45. package/dist/commands/phase/template/create.d.ts +23 -0
  46. package/dist/commands/phase/template/create.js +14 -0
  47. package/dist/commands/phase/template/delete.d.ts +18 -0
  48. package/dist/commands/phase/template/delete.js +61 -0
  49. package/dist/commands/phase/template/list.d.ts +17 -0
  50. package/dist/commands/phase/template/list.js +88 -0
  51. package/dist/commands/phase/template/update.d.ts +1 -0
  52. package/dist/commands/phase/template/update.js +1 -0
  53. package/dist/commands/priority/add.js +1 -1
  54. package/dist/commands/project/update.js +0 -2
  55. package/dist/commands/roadmap/generate.js +1 -2
  56. package/dist/commands/session/health.js +1 -1
  57. package/dist/commands/spec/link/depends.d.ts +18 -0
  58. package/dist/commands/spec/link/depends.js +86 -0
  59. package/dist/commands/spec/link/index.d.ts +17 -0
  60. package/dist/commands/spec/link/index.js +92 -0
  61. package/dist/commands/spec/link/remove.d.ts +18 -0
  62. package/dist/commands/spec/link/remove.js +90 -0
  63. package/dist/commands/support/logs.js +2 -2
  64. package/dist/commands/template/apply.js +5 -4
  65. package/dist/commands/template/create.js +1 -1
  66. package/dist/commands/ticket/link/block.d.ts +15 -0
  67. package/dist/commands/ticket/link/block.js +95 -0
  68. package/dist/commands/ticket/link/index.d.ts +14 -0
  69. package/dist/commands/ticket/link/index.js +96 -0
  70. package/dist/commands/ticket/list.d.ts +1 -0
  71. package/dist/commands/ticket/list.js +6 -0
  72. package/dist/commands/ticket/resolve.js +1 -1
  73. package/dist/commands/ticket/template/apply.d.ts +26 -0
  74. package/dist/commands/ticket/template/apply.js +14 -0
  75. package/dist/commands/ticket/template/delete.d.ts +18 -0
  76. package/dist/commands/ticket/template/delete.js +61 -0
  77. package/dist/commands/ticket/template/list.d.ts +17 -0
  78. package/dist/commands/ticket/template/list.js +77 -0
  79. package/dist/commands/ticket/template/save.d.ts +17 -0
  80. package/dist/commands/ticket/template/save.js +97 -0
  81. package/dist/commands/ticket/view.d.ts +1 -0
  82. package/dist/commands/ticket/view.js +1 -0
  83. package/dist/commands/work/ready.js +17 -0
  84. package/dist/commands/work/resolve.js +1 -1
  85. package/dist/commands/work/spawn.js +4 -4
  86. package/dist/commands/work/start.d.ts +1 -0
  87. package/dist/commands/work/start.js +52 -17
  88. package/dist/lib/database/index.d.ts +1 -1
  89. package/dist/lib/database/index.js +20 -0
  90. package/dist/lib/execution/devcontainer.js +3 -1
  91. package/dist/lib/execution/runners.d.ts +7 -2
  92. package/dist/lib/execution/runners.js +18 -10
  93. package/dist/lib/execution/types.d.ts +1 -0
  94. package/dist/lib/flags/resolver.js +1 -0
  95. package/dist/lib/mcp/helpers.d.ts +1 -2
  96. package/dist/lib/mcp/tools/diet.js +1 -0
  97. package/dist/lib/mcp/tools/index.d.ts +1 -0
  98. package/dist/lib/mcp/tools/index.js +1 -0
  99. package/dist/lib/mcp/tools/label.d.ts +6 -0
  100. package/dist/lib/mcp/tools/label.js +338 -0
  101. package/dist/lib/mcp/tools/ticket.js +53 -17
  102. package/dist/lib/multiline-input.js +6 -18
  103. package/dist/lib/pmo/base-command.d.ts +0 -1
  104. package/dist/lib/pmo/base-command.js +0 -1
  105. package/dist/lib/pmo/schema.d.ts +6 -0
  106. package/dist/lib/pmo/schema.js +44 -0
  107. package/dist/lib/pmo/storage/base.d.ts +6 -0
  108. package/dist/lib/pmo/storage/base.js +116 -2
  109. package/dist/lib/pmo/storage/index.d.ts +23 -1
  110. package/dist/lib/pmo/storage/index.js +59 -1
  111. package/dist/lib/pmo/storage/labels.d.ts +55 -0
  112. package/dist/lib/pmo/storage/labels.js +346 -0
  113. package/dist/lib/pmo/storage/tickets.js +17 -0
  114. package/dist/lib/pmo/storage/types.d.ts +24 -0
  115. package/dist/lib/pmo/types.d.ts +44 -0
  116. package/dist/lib/pmo/utils.js +1 -1
  117. package/oclif.manifest.json +5702 -3660
  118. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines -- runner implementations require cohesive logic */
1
2
  /**
2
3
  * Execution Runners
3
4
  *
@@ -102,19 +103,23 @@ export function credentialsVolumeExists() {
102
103
  }
103
104
  }
104
105
  /**
105
- * Check if valid Claude credentials exist in the Docker volume.
106
- * Returns true if credentials exist and are not expired.
106
+ * Check if valid Claude OAuth credentials exist in the Docker volume.
107
+ * Returns true if OAuth credentials are stored (even if access token is expired,
108
+ * since Claude Code handles refresh internally using stored refresh tokens).
109
+ *
110
+ * NOTE: This intentionally does NOT check for ANTHROPIC_API_KEY. If the user
111
+ * has an API key but no OAuth credentials, we want to prompt them to set up
112
+ * OAuth (which uses their Max subscription) rather than silently burning API credits.
107
113
  */
108
114
  export function dockerCredentialsExist() {
109
115
  try {
110
116
  const result = execSync(`docker run --rm -v ${CLAUDE_CREDENTIALS_VOLUME}:/data alpine cat /data/.credentials.json 2>/dev/null`, { stdio: 'pipe', encoding: 'utf-8' });
111
117
  const creds = JSON.parse(result);
112
- if (creds.claudeAiOauth?.accessToken && creds.claudeAiOauth?.expiresAt) {
113
- // Check if expired
114
- const expiresAt = creds.claudeAiOauth.expiresAt;
115
- if (expiresAt > Date.now()) {
116
- return true;
117
- }
118
+ // Check if OAuth credentials exist. Don't check expiration because
119
+ // access tokens are short-lived but Claude Code handles token refresh
120
+ // internally using stored refresh tokens.
121
+ if (creds.claudeAiOauth?.accessToken) {
122
+ return true;
118
123
  }
119
124
  return false;
120
125
  }
@@ -767,7 +772,10 @@ function createDockerContainer(context, containerName, imageName, config) {
767
772
  `-e PRLT_HQ_PATH=/hq`,
768
773
  `-e PRLT_AGENT_NAME="${context.agentName}"`,
769
774
  `-e PRLT_HOST_PATH="${context.agentDir}"`,
770
- ...(process.env.ANTHROPIC_API_KEY ? [`-e ANTHROPIC_API_KEY="${process.env.ANTHROPIC_API_KEY}"`] : []),
775
+ // Only pass ANTHROPIC_API_KEY if the user explicitly chose to use it (no OAuth creds).
776
+ // Claude Code prefers API key over OAuth, so passing it would cause agents to burn
777
+ // API credits instead of using Max subscription.
778
+ ...(context.useApiKey && process.env.ANTHROPIC_API_KEY ? [`-e ANTHROPIC_API_KEY="${process.env.ANTHROPIC_API_KEY}"`] : []),
771
779
  ...(process.env.GITHUB_TOKEN ? [`-e GITHUB_TOKEN="${process.env.GITHUB_TOKEN}"`] : []),
772
780
  ...(process.env.GH_TOKEN ? [`-e GH_TOKEN="${process.env.GH_TOKEN}"`] : []),
773
781
  // NOTE: Do NOT pass CLAUDE_CODE_OAUTH_TOKEN - it overrides credentials file
@@ -1509,7 +1517,7 @@ exec bash
1509
1517
  try {
1510
1518
  execSync(`docker exec ${actualContainerId} tmux has-session -t "${sessionName}" 2>&1`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
1511
1519
  }
1512
- catch (err) {
1520
+ catch {
1513
1521
  return {
1514
1522
  success: false,
1515
1523
  error: `Failed to verify tmux session "${sessionName}" inside container. The session may not have started correctly.`,
@@ -87,6 +87,7 @@ export interface ExecutionContext {
87
87
  actionEndPrompt?: string;
88
88
  modifiesCode?: boolean;
89
89
  customMessage?: string;
90
+ useApiKey?: boolean;
90
91
  prFeedback?: string;
91
92
  isRevision?: boolean;
92
93
  }
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-await-in-loop */
1
2
  /**
2
3
  * FlagResolver - Unified flag resolution for human and machine interactive modes
3
4
  *
@@ -3,9 +3,8 @@
3
3
  */
4
4
  import { z } from 'zod';
5
5
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
- import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
6
+ import type { CallToolResult, ServerRequest, ServerNotification } from '@modelcontextprotocol/sdk/types.js';
7
7
  import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
8
- import type { ServerRequest, ServerNotification } from '@modelcontextprotocol/sdk/types.js';
9
8
  import type { Ticket } from '../pmo/types.js';
10
9
  import type { McpToolResult } from './types.js';
11
10
  /**
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-await-in-loop */
1
2
  /**
2
3
  * MCP Diet & Pull Tools
3
4
  */
@@ -16,4 +16,5 @@ export { registerCategoryTools } from './category.js';
16
16
  export { registerTemplateTools } from './template.js';
17
17
  export { registerViewTools } from './view.js';
18
18
  export { registerDietTools } from './diet.js';
19
+ export { registerLabelTools } from './label.js';
19
20
  export { registerAgentTools, registerDockerTools, registerRepoTools, registerBranchTools, registerGitHubTools, registerInitTools, registerUtilityTools, } from './cli-passthrough.js';
@@ -16,5 +16,6 @@ export { registerCategoryTools } from './category.js';
16
16
  export { registerTemplateTools } from './template.js';
17
17
  export { registerViewTools } from './view.js';
18
18
  export { registerDietTools } from './diet.js';
19
+ export { registerLabelTools } from './label.js';
19
20
  // CLI passthrough tools
20
21
  export { registerAgentTools, registerDockerTools, registerRepoTools, registerBranchTools, registerGitHubTools, registerInitTools, registerUtilityTools, } from './cli-passthrough.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * MCP Label Tools
3
+ */
4
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ import type { McpToolContext } from '../types.js';
6
+ export declare function registerLabelTools(server: McpServer, ctx: McpToolContext): void;
@@ -0,0 +1,338 @@
1
+ /**
2
+ * MCP Label Tools
3
+ */
4
+ import { z } from 'zod';
5
+ import { errorResponse, strictTool } from '../helpers.js';
6
+ export function registerLabelTools(server, ctx) {
7
+ // ===========================================================================
8
+ // Label Group Tools
9
+ // ===========================================================================
10
+ strictTool(server, 'label_group_list', 'List all label groups', {
11
+ search: z.string().optional().describe('Search in name/description'),
12
+ }, async (params) => {
13
+ try {
14
+ const groups = await ctx.storage.listLabelGroups({
15
+ search: params.search,
16
+ });
17
+ return {
18
+ content: [{
19
+ type: 'text',
20
+ text: JSON.stringify({
21
+ success: true,
22
+ count: groups.length,
23
+ groups: groups.map(g => ({
24
+ id: g.id,
25
+ name: g.name,
26
+ description: g.description,
27
+ isExclusive: g.isExclusive,
28
+ isRequired: g.isRequired,
29
+ position: g.position,
30
+ })),
31
+ }, null, 2),
32
+ }],
33
+ };
34
+ }
35
+ catch (error) {
36
+ return errorResponse(error);
37
+ }
38
+ });
39
+ strictTool(server, 'label_group_create', 'Create a new label group', {
40
+ name: z.string().describe('Group name (must be unique)'),
41
+ description: z.string().optional().describe('Group description'),
42
+ is_exclusive: z.boolean().optional().describe('Only one label from this group per ticket (default: true)'),
43
+ is_required: z.boolean().optional().describe('Must have one label from this group (default: false)'),
44
+ }, async (params) => {
45
+ try {
46
+ const group = await ctx.storage.createLabelGroup({
47
+ name: params.name,
48
+ description: params.description,
49
+ isExclusive: params.is_exclusive,
50
+ isRequired: params.is_required,
51
+ });
52
+ return {
53
+ content: [{
54
+ type: 'text',
55
+ text: JSON.stringify({
56
+ success: true,
57
+ group: {
58
+ id: group.id,
59
+ name: group.name,
60
+ description: group.description,
61
+ isExclusive: group.isExclusive,
62
+ isRequired: group.isRequired,
63
+ },
64
+ }, null, 2),
65
+ }],
66
+ };
67
+ }
68
+ catch (error) {
69
+ return errorResponse(error);
70
+ }
71
+ });
72
+ strictTool(server, 'label_group_update', 'Update a label group', {
73
+ id: z.string().describe('Group ID'),
74
+ name: z.string().optional().describe('New name'),
75
+ description: z.string().optional().describe('New description'),
76
+ is_exclusive: z.boolean().optional().describe('Only one label from this group per ticket'),
77
+ is_required: z.boolean().optional().describe('Must have one label from this group'),
78
+ }, async (params) => {
79
+ try {
80
+ const changes = {};
81
+ if (params.name !== undefined)
82
+ changes.name = params.name;
83
+ if (params.description !== undefined)
84
+ changes.description = params.description;
85
+ if (params.is_exclusive !== undefined)
86
+ changes.isExclusive = params.is_exclusive;
87
+ if (params.is_required !== undefined)
88
+ changes.isRequired = params.is_required;
89
+ const group = await ctx.storage.updateLabelGroup(params.id, changes);
90
+ return {
91
+ content: [{
92
+ type: 'text',
93
+ text: JSON.stringify({
94
+ success: true,
95
+ group: {
96
+ id: group.id,
97
+ name: group.name,
98
+ description: group.description,
99
+ isExclusive: group.isExclusive,
100
+ isRequired: group.isRequired,
101
+ },
102
+ }, null, 2),
103
+ }],
104
+ };
105
+ }
106
+ catch (error) {
107
+ return errorResponse(error);
108
+ }
109
+ });
110
+ strictTool(server, 'label_group_delete', 'Delete a label group (labels in the group become ungrouped)', {
111
+ id: z.string().describe('Group ID'),
112
+ }, async (params) => {
113
+ try {
114
+ await ctx.storage.deleteLabelGroup(params.id);
115
+ return {
116
+ content: [{
117
+ type: 'text',
118
+ text: JSON.stringify({ success: true, message: `Deleted label group ${params.id}` }, null, 2),
119
+ }],
120
+ };
121
+ }
122
+ catch (error) {
123
+ return errorResponse(error);
124
+ }
125
+ });
126
+ // ===========================================================================
127
+ // Label Tools
128
+ // ===========================================================================
129
+ strictTool(server, 'label_list', 'List all labels, optionally filtered by group', {
130
+ group: z.string().optional().describe('Filter by group ID'),
131
+ search: z.string().optional().describe('Search in name/description'),
132
+ }, async (params) => {
133
+ try {
134
+ const labels = await ctx.storage.listLabels({
135
+ groupId: params.group,
136
+ search: params.search,
137
+ });
138
+ return {
139
+ content: [{
140
+ type: 'text',
141
+ text: JSON.stringify({
142
+ success: true,
143
+ count: labels.length,
144
+ labels: labels.map(l => ({
145
+ id: l.id,
146
+ name: l.name,
147
+ color: l.color,
148
+ description: l.description,
149
+ groupId: l.groupId,
150
+ groupName: l.groupName,
151
+ isBuiltin: l.isBuiltin,
152
+ })),
153
+ }, null, 2),
154
+ }],
155
+ };
156
+ }
157
+ catch (error) {
158
+ return errorResponse(error);
159
+ }
160
+ });
161
+ strictTool(server, 'label_create', 'Create a new label', {
162
+ name: z.string().describe('Label name'),
163
+ color: z.string().optional().describe('Label color (hex, e.g. #ff0000)'),
164
+ description: z.string().optional().describe('Label description'),
165
+ group_id: z.string().optional().describe('Label group ID to add this label to'),
166
+ }, async (params) => {
167
+ try {
168
+ const label = await ctx.storage.createLabel({
169
+ name: params.name,
170
+ color: params.color,
171
+ description: params.description,
172
+ groupId: params.group_id,
173
+ });
174
+ return {
175
+ content: [{
176
+ type: 'text',
177
+ text: JSON.stringify({
178
+ success: true,
179
+ label: {
180
+ id: label.id,
181
+ name: label.name,
182
+ color: label.color,
183
+ description: label.description,
184
+ groupId: label.groupId,
185
+ groupName: label.groupName,
186
+ },
187
+ }, null, 2),
188
+ }],
189
+ };
190
+ }
191
+ catch (error) {
192
+ return errorResponse(error);
193
+ }
194
+ });
195
+ strictTool(server, 'label_update', 'Update a label (rename, recolor, regroup)', {
196
+ id: z.string().describe('Label ID'),
197
+ name: z.string().optional().describe('New name'),
198
+ color: z.string().optional().describe('New color (hex)'),
199
+ description: z.string().optional().describe('New description'),
200
+ group_id: z.string().optional().describe('Move to different group'),
201
+ }, async (params) => {
202
+ try {
203
+ const changes = {};
204
+ if (params.name !== undefined)
205
+ changes.name = params.name;
206
+ if (params.color !== undefined)
207
+ changes.color = params.color;
208
+ if (params.description !== undefined)
209
+ changes.description = params.description;
210
+ if (params.group_id !== undefined)
211
+ changes.groupId = params.group_id;
212
+ const label = await ctx.storage.updateLabel(params.id, changes);
213
+ return {
214
+ content: [{
215
+ type: 'text',
216
+ text: JSON.stringify({
217
+ success: true,
218
+ label: {
219
+ id: label.id,
220
+ name: label.name,
221
+ color: label.color,
222
+ description: label.description,
223
+ groupId: label.groupId,
224
+ groupName: label.groupName,
225
+ },
226
+ }, null, 2),
227
+ }],
228
+ };
229
+ }
230
+ catch (error) {
231
+ return errorResponse(error);
232
+ }
233
+ });
234
+ strictTool(server, 'label_delete', 'Delete a label (removes from all tickets)', {
235
+ id: z.string().describe('Label ID'),
236
+ }, async (params) => {
237
+ try {
238
+ await ctx.storage.deleteLabel(params.id);
239
+ return {
240
+ content: [{
241
+ type: 'text',
242
+ text: JSON.stringify({ success: true, message: `Deleted label ${params.id}` }, null, 2),
243
+ }],
244
+ };
245
+ }
246
+ catch (error) {
247
+ return errorResponse(error);
248
+ }
249
+ });
250
+ // ===========================================================================
251
+ // Ticket-Label Association Tools
252
+ // ===========================================================================
253
+ strictTool(server, 'ticket_add_label', 'Add a label to a ticket. For exclusive groups, replaces existing label from same group.', {
254
+ ticket_id: z.string().describe('Ticket ID'),
255
+ label: z.string().describe('Label ID or name (supports group:name format)'),
256
+ }, async (params) => {
257
+ try {
258
+ // Try by ID first, then by name
259
+ const labelById = await ctx.storage.getLabel(params.label);
260
+ if (labelById) {
261
+ await ctx.storage.addLabelToTicket(params.ticket_id, labelById.id);
262
+ }
263
+ else {
264
+ await ctx.storage.addLabelToTicketByName(params.ticket_id, params.label);
265
+ }
266
+ const labels = await ctx.storage.getLabelsForTicket(params.ticket_id);
267
+ return {
268
+ content: [{
269
+ type: 'text',
270
+ text: JSON.stringify({
271
+ success: true,
272
+ message: `Label added to ${params.ticket_id}`,
273
+ labels: labels.map(l => ({
274
+ id: l.id,
275
+ name: l.name,
276
+ groupName: l.groupName,
277
+ })),
278
+ }, null, 2),
279
+ }],
280
+ };
281
+ }
282
+ catch (error) {
283
+ return errorResponse(error);
284
+ }
285
+ });
286
+ strictTool(server, 'ticket_remove_label', 'Remove a label from a ticket', {
287
+ ticket_id: z.string().describe('Ticket ID'),
288
+ label: z.string().describe('Label ID or name'),
289
+ }, async (params) => {
290
+ try {
291
+ const labelById = await ctx.storage.getLabel(params.label);
292
+ if (labelById) {
293
+ await ctx.storage.removeLabelFromTicket(params.ticket_id, labelById.id);
294
+ }
295
+ else {
296
+ await ctx.storage.removeLabelFromTicketByName(params.ticket_id, params.label);
297
+ }
298
+ return {
299
+ content: [{
300
+ type: 'text',
301
+ text: JSON.stringify({
302
+ success: true,
303
+ message: `Label removed from ${params.ticket_id}`,
304
+ }, null, 2),
305
+ }],
306
+ };
307
+ }
308
+ catch (error) {
309
+ return errorResponse(error);
310
+ }
311
+ });
312
+ strictTool(server, 'ticket_labels', 'List all labels on a ticket', {
313
+ ticket_id: z.string().describe('Ticket ID'),
314
+ }, async (params) => {
315
+ try {
316
+ const labels = await ctx.storage.getLabelsForTicket(params.ticket_id);
317
+ return {
318
+ content: [{
319
+ type: 'text',
320
+ text: JSON.stringify({
321
+ success: true,
322
+ count: labels.length,
323
+ labels: labels.map(l => ({
324
+ id: l.id,
325
+ name: l.name,
326
+ color: l.color,
327
+ groupId: l.groupId,
328
+ groupName: l.groupName,
329
+ })),
330
+ }, null, 2),
331
+ }],
332
+ };
333
+ }
334
+ catch (error) {
335
+ return errorResponse(error);
336
+ }
337
+ });
338
+ }
@@ -14,6 +14,8 @@ export function registerTicketTools(server, ctx) {
14
14
  owner: z.string().optional().describe('Filter by owner'),
15
15
  search: z.string().optional().describe('Search in title/description'),
16
16
  epic: z.string().optional().describe('Filter by epic ID'),
17
+ label: z.string().optional().describe('Filter by label name'),
18
+ label_group: z.string().optional().describe('Filter by label group name'),
17
19
  all_projects: z.boolean().optional().describe('List from all projects'),
18
20
  }, async (params) => {
19
21
  try {
@@ -25,6 +27,8 @@ export function registerTicketTools(server, ctx) {
25
27
  owner: params.owner,
26
28
  search: params.search,
27
29
  epic: params.epic,
30
+ label: params.label,
31
+ labelGroup: params.label_group,
28
32
  allProjects: params.all_projects,
29
33
  });
30
34
  return {
@@ -33,22 +37,26 @@ export function registerTicketTools(server, ctx) {
33
37
  text: JSON.stringify({
34
38
  success: true,
35
39
  count: tickets.length,
36
- tickets: tickets.map((t) => ({
37
- id: t.id,
38
- title: t.title,
39
- description: t.description,
40
- priority: t.priority,
41
- category: t.category,
42
- statusName: t.statusName,
43
- statusCategory: t.statusCategory,
44
- projectId: t.projectId,
45
- assignee: t.assignee,
46
- owner: t.owner,
47
- epicId: t.epicId,
48
- branch: t.branch,
49
- position: t.position,
50
- createdAt: t.createdAt.toISOString(),
51
- updatedAt: t.updatedAt.toISOString(),
40
+ tickets: await Promise.all(tickets.map(async (t) => {
41
+ const ticketLabels = await ctx.storage.getLabelsForTicket(t.id);
42
+ return {
43
+ id: t.id,
44
+ title: t.title,
45
+ description: t.description,
46
+ priority: t.priority,
47
+ category: t.category,
48
+ statusName: t.statusName,
49
+ statusCategory: t.statusCategory,
50
+ projectId: t.projectId,
51
+ assignee: t.assignee,
52
+ owner: t.owner,
53
+ epicId: t.epicId,
54
+ branch: t.branch,
55
+ position: t.position,
56
+ labels: ticketLabels.map(l => ({ id: l.id, name: l.name, groupName: l.groupName })),
57
+ createdAt: t.createdAt.toISOString(),
58
+ updatedAt: t.updatedAt.toISOString(),
59
+ };
52
60
  })),
53
61
  }, null, 2),
54
62
  }],
@@ -91,6 +99,24 @@ export function registerTicketTools(server, ctx) {
91
99
  labels: params.labels,
92
100
  subtasks: params.subtasks?.map((title) => ({ id: '', title, done: false })),
93
101
  });
102
+ // Add structured labels from the labels param via junction table
103
+ if (params.labels && params.labels.length > 0) {
104
+ for (const labelName of params.labels) {
105
+ try {
106
+ // Try to add as structured label (by name or ID)
107
+ const labelById = await ctx.storage.getLabel(labelName);
108
+ if (labelById) {
109
+ await ctx.storage.addLabelToTicket(ticket.id, labelById.id);
110
+ }
111
+ else {
112
+ await ctx.storage.addLabelToTicketByName(ticket.id, labelName);
113
+ }
114
+ }
115
+ catch {
116
+ // If label doesn't exist in the label system, it stays only in the legacy JSON array
117
+ }
118
+ }
119
+ }
94
120
  return {
95
121
  content: [{
96
122
  type: 'text',
@@ -107,10 +133,20 @@ export function registerTicketTools(server, ctx) {
107
133
  const ticket = await ctx.storage.getTicket(params.id);
108
134
  if (!ticket)
109
135
  throw new Error(`Ticket not found: ${params.id}`);
136
+ const ticketLabels = await ctx.storage.getLabelsForTicket(params.id);
137
+ const ticketData = formatTicketFull(ticket);
138
+ // Override labels with structured label data from junction table
139
+ ticketData.labels = ticketLabels.map(l => ({
140
+ id: l.id,
141
+ name: l.name,
142
+ color: l.color,
143
+ groupId: l.groupId,
144
+ groupName: l.groupName,
145
+ }));
110
146
  return {
111
147
  content: [{
112
148
  type: 'text',
113
- text: JSON.stringify({ success: true, ticket: formatTicketFull(ticket) }, null, 2),
149
+ text: JSON.stringify({ success: true, ticket: ticketData }, null, 2),
114
150
  }],
115
151
  };
116
152
  }
@@ -13,16 +13,16 @@
13
13
  * });
14
14
  * ```
15
15
  */
16
- import * as readline from 'readline';
16
+ import * as readline from 'node:readline';
17
17
  import chalk from 'chalk';
18
18
  // ANSI escape codes for terminal control
19
- const ESC = '\x1b';
19
+ const ESC = '\u001B';
20
20
  const CSI = `${ESC}[`;
21
21
  // Control characters
22
- const CTRL_C = '\x03';
23
- const CTRL_D = '\x04';
24
- const BACKSPACE = '\x7f';
25
- const DELETE = '\x1b[3~';
22
+ const CTRL_C = '\u0003';
23
+ const CTRL_D = '\u0004';
24
+ const BACKSPACE = '\u007F';
25
+ const DELETE = '\u001B[3~';
26
26
  const ENTER = '\r';
27
27
  const NEWLINE = '\n';
28
28
  // Arrow keys (CSI sequences)
@@ -60,18 +60,6 @@ function moveDown(n) {
60
60
  function moveToColumn(col) {
61
61
  process.stdout.write(`${CSI}${col + 1}G`);
62
62
  }
63
- /**
64
- * Clear from cursor to end of screen
65
- */
66
- function clearToEnd() {
67
- process.stdout.write(`${CSI}J`);
68
- }
69
- /**
70
- * Hide cursor
71
- */
72
- function hideCursor() {
73
- process.stdout.write(`${CSI}?25l`);
74
- }
75
63
  /**
76
64
  * Show cursor
77
65
  */
@@ -192,7 +192,6 @@ export declare abstract class PMOCommand extends PromptCommand {
192
192
  * @param code - Error code for JSON output (e.g., 'NOT_FOUND', 'DOCKER_NOT_RUNNING')
193
193
  * @param message - Human-readable error message (used in both modes)
194
194
  * @param options - Configuration for error handling
195
- * @returns never - always throws or exits
196
195
  *
197
196
  * @example
198
197
  * ```typescript
@@ -323,7 +323,6 @@ export class PMOCommand extends PromptCommand {
323
323
  * @param code - Error code for JSON output (e.g., 'NOT_FOUND', 'DOCKER_NOT_RUNNING')
324
324
  * @param message - Human-readable error message (used in both modes)
325
325
  * @param options - Configuration for error handling
326
- * @returns never - always throws or exits
327
326
  *
328
327
  * @example
329
328
  * ```typescript
@@ -35,6 +35,9 @@ export declare const PMO_TABLES: {
35
35
  readonly ticket_templates: "pmo_ticket_templates";
36
36
  readonly roadmaps: "pmo_roadmaps";
37
37
  readonly roadmap_projects: "pmo_roadmap_projects";
38
+ readonly label_groups: "pmo_label_groups";
39
+ readonly labels: "pmo_labels";
40
+ readonly ticket_labels: "pmo_ticket_labels";
38
41
  readonly columns: "pmo_columns";
39
42
  readonly board_tickets: "pmo_board_tickets";
40
43
  readonly statuses: "pmo_statuses";
@@ -71,6 +74,9 @@ export declare const PMO_TABLE_SCHEMAS: {
71
74
  readonly phase_templates: "\n CREATE TABLE IF NOT EXISTS pmo_phase_templates (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n description TEXT,\n is_builtin INTEGER NOT NULL DEFAULT 0,\n phases TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n )";
72
75
  readonly actions: "\n CREATE TABLE IF NOT EXISTS pmo_actions (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n description TEXT,\n prompt TEXT NOT NULL,\n end_prompt TEXT,\n suggested_for_categories TEXT,\n default_move_to_category TEXT,\n modifies_code INTEGER NOT NULL DEFAULT 1,\n is_builtin INTEGER NOT NULL DEFAULT 0,\n position INTEGER NOT NULL DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n )";
73
76
  readonly ticket_templates: "\n CREATE TABLE IF NOT EXISTS pmo_ticket_templates (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n description TEXT,\n is_builtin INTEGER NOT NULL DEFAULT 0,\n title_pattern TEXT,\n description_template TEXT,\n default_priority TEXT,\n default_category TEXT,\n default_status_id TEXT,\n default_assignee TEXT,\n default_owner TEXT,\n default_labels TEXT NOT NULL DEFAULT '[]',\n suggested_subtasks TEXT NOT NULL DEFAULT '[]',\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n )";
77
+ readonly label_groups: "\n CREATE TABLE IF NOT EXISTS pmo_label_groups (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n description TEXT,\n is_exclusive INTEGER NOT NULL DEFAULT 1,\n is_required INTEGER NOT NULL DEFAULT 0,\n position INTEGER NOT NULL DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n )";
78
+ readonly labels: "\n CREATE TABLE IF NOT EXISTS pmo_labels (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n color TEXT,\n description TEXT,\n group_id TEXT REFERENCES pmo_label_groups(id) ON DELETE SET NULL,\n position INTEGER NOT NULL DEFAULT 0,\n is_builtin INTEGER NOT NULL DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n UNIQUE(name, group_id)\n )";
79
+ readonly ticket_labels: "\n CREATE TABLE IF NOT EXISTS pmo_ticket_labels (\n ticket_id TEXT NOT NULL REFERENCES pmo_tickets(id) ON DELETE CASCADE,\n label_id TEXT NOT NULL REFERENCES pmo_labels(id) ON DELETE CASCADE,\n PRIMARY KEY (ticket_id, label_id)\n )";
74
80
  readonly roadmaps: "\n CREATE TABLE IF NOT EXISTS pmo_roadmaps (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n description TEXT,\n is_default INTEGER NOT NULL DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n )";
75
81
  readonly roadmap_projects: "\n CREATE TABLE IF NOT EXISTS pmo_roadmap_projects (\n roadmap_id TEXT NOT NULL REFERENCES pmo_roadmaps(id) ON DELETE CASCADE,\n project_id TEXT NOT NULL REFERENCES pmo_projects(id) ON DELETE CASCADE,\n position INTEGER NOT NULL DEFAULT 0,\n created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (roadmap_id, project_id)\n )";
76
82
  };