mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
feat: improve ham connection (#1451)
This commit is contained in:
@@ -504,8 +504,19 @@ export class TaskService {
|
||||
|
||||
/**
|
||||
* Get current active tag
|
||||
* For API storage, uses the brief ID from auth context
|
||||
* For file storage, uses the tag from local config/state
|
||||
*/
|
||||
getActiveTag(): string {
|
||||
// For API storage, use brief ID from auth context if available
|
||||
if (this.initialized && this.getStorageType() === 'api') {
|
||||
const briefName = this.storage.getCurrentBriefName();
|
||||
if (briefName) {
|
||||
return briefName;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to config-based tag resolution
|
||||
return this.configManager.getActiveTag();
|
||||
}
|
||||
|
||||
|
||||
13
packages/tm-core/src/modules/tasks/validation/index.ts
Normal file
13
packages/tm-core/src/modules/tasks/validation/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @fileoverview Task validation utilities and Zod schemas
|
||||
*/
|
||||
|
||||
export {
|
||||
TASK_ID_PATTERN,
|
||||
isValidTaskIdFormat,
|
||||
taskIdSchema,
|
||||
taskIdsSchema,
|
||||
parseTaskIds,
|
||||
extractParentId,
|
||||
isSubtaskId
|
||||
} from './task-id.js';
|
||||
165
packages/tm-core/src/modules/tasks/validation/task-id.spec.ts
Normal file
165
packages/tm-core/src/modules/tasks/validation/task-id.spec.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* @fileoverview Tests for task ID validation utilities
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
TASK_ID_PATTERN,
|
||||
isValidTaskIdFormat,
|
||||
taskIdSchema,
|
||||
taskIdsSchema,
|
||||
parseTaskIds,
|
||||
extractParentId,
|
||||
isSubtaskId
|
||||
} from './task-id.js';
|
||||
|
||||
describe('Task ID Validation', () => {
|
||||
describe('TASK_ID_PATTERN', () => {
|
||||
it('matches simple numeric IDs', () => {
|
||||
expect(TASK_ID_PATTERN.test('1')).toBe(true);
|
||||
expect(TASK_ID_PATTERN.test('15')).toBe(true);
|
||||
expect(TASK_ID_PATTERN.test('999')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches alphanumeric display IDs', () => {
|
||||
expect(TASK_ID_PATTERN.test('HAM-123')).toBe(true);
|
||||
expect(TASK_ID_PATTERN.test('PROJ-456')).toBe(true);
|
||||
expect(TASK_ID_PATTERN.test('TAS-1')).toBe(true);
|
||||
expect(TASK_ID_PATTERN.test('abc-999')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches subtask IDs', () => {
|
||||
expect(TASK_ID_PATTERN.test('1.2')).toBe(true);
|
||||
expect(TASK_ID_PATTERN.test('15.3')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches sub-subtask IDs', () => {
|
||||
expect(TASK_ID_PATTERN.test('1.2.3')).toBe(true);
|
||||
expect(TASK_ID_PATTERN.test('15.3.1')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects alphanumeric subtasks (not supported)', () => {
|
||||
expect(TASK_ID_PATTERN.test('HAM-123.2')).toBe(false);
|
||||
expect(TASK_ID_PATTERN.test('PROJ-456.1.2')).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects invalid formats', () => {
|
||||
expect(TASK_ID_PATTERN.test('')).toBe(false);
|
||||
expect(TASK_ID_PATTERN.test('abc')).toBe(false);
|
||||
expect(TASK_ID_PATTERN.test('1.a')).toBe(false);
|
||||
expect(TASK_ID_PATTERN.test('.1')).toBe(false);
|
||||
expect(TASK_ID_PATTERN.test('1.')).toBe(false);
|
||||
expect(TASK_ID_PATTERN.test('HAM')).toBe(false);
|
||||
expect(TASK_ID_PATTERN.test('123-HAM')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isValidTaskIdFormat', () => {
|
||||
it('returns true for valid IDs', () => {
|
||||
expect(isValidTaskIdFormat('1')).toBe(true);
|
||||
expect(isValidTaskIdFormat('1.2')).toBe(true);
|
||||
expect(isValidTaskIdFormat('1.2.3')).toBe(true);
|
||||
expect(isValidTaskIdFormat('HAM-123')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for invalid IDs', () => {
|
||||
expect(isValidTaskIdFormat('')).toBe(false);
|
||||
expect(isValidTaskIdFormat('abc')).toBe(false);
|
||||
expect(isValidTaskIdFormat('1.a')).toBe(false);
|
||||
expect(isValidTaskIdFormat('HAM-123.2')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('taskIdSchema', () => {
|
||||
it('parses valid single IDs', () => {
|
||||
expect(taskIdSchema.parse('1')).toBe('1');
|
||||
expect(taskIdSchema.parse('15.2')).toBe('15.2');
|
||||
expect(taskIdSchema.parse('HAM-123')).toBe('HAM-123');
|
||||
});
|
||||
|
||||
it('throws on invalid IDs', () => {
|
||||
expect(() => taskIdSchema.parse('')).toThrow();
|
||||
expect(() => taskIdSchema.parse('abc')).toThrow();
|
||||
expect(() => taskIdSchema.parse('HAM-123.2')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('taskIdsSchema', () => {
|
||||
it('parses single ID', () => {
|
||||
expect(taskIdsSchema.parse('1')).toBe('1');
|
||||
expect(taskIdsSchema.parse('HAM-123')).toBe('HAM-123');
|
||||
});
|
||||
|
||||
it('parses comma-separated IDs', () => {
|
||||
expect(taskIdsSchema.parse('1,2,3')).toBe('1,2,3');
|
||||
expect(taskIdsSchema.parse('1.2, 3.4')).toBe('1.2, 3.4');
|
||||
expect(taskIdsSchema.parse('HAM-123, PROJ-456')).toBe('HAM-123, PROJ-456');
|
||||
});
|
||||
|
||||
it('throws on invalid IDs', () => {
|
||||
expect(() => taskIdsSchema.parse('abc')).toThrow();
|
||||
expect(() => taskIdsSchema.parse('1,abc,3')).toThrow();
|
||||
expect(() => taskIdsSchema.parse('HAM-123.2')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseTaskIds', () => {
|
||||
it('parses single ID', () => {
|
||||
expect(parseTaskIds('1')).toEqual(['1']);
|
||||
expect(parseTaskIds('HAM-123')).toEqual(['HAM-123']);
|
||||
});
|
||||
|
||||
it('parses comma-separated IDs', () => {
|
||||
expect(parseTaskIds('1, 2, 3')).toEqual(['1', '2', '3']);
|
||||
expect(parseTaskIds('HAM-123, PROJ-456')).toEqual(['HAM-123', 'PROJ-456']);
|
||||
});
|
||||
|
||||
it('trims whitespace', () => {
|
||||
expect(parseTaskIds(' 1 , 2 ')).toEqual(['1', '2']);
|
||||
});
|
||||
|
||||
it('filters empty entries', () => {
|
||||
expect(parseTaskIds('1,,2')).toEqual(['1', '2']);
|
||||
});
|
||||
|
||||
it('throws on invalid IDs', () => {
|
||||
expect(() => parseTaskIds('abc')).toThrow(/Invalid task ID format/);
|
||||
expect(() => parseTaskIds('HAM-123.2')).toThrow(/Invalid task ID format/);
|
||||
});
|
||||
|
||||
it('throws on empty input', () => {
|
||||
expect(() => parseTaskIds('')).toThrow(/No valid task IDs/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractParentId', () => {
|
||||
it('extracts parent from numeric subtask ID', () => {
|
||||
expect(extractParentId('1.2')).toBe('1');
|
||||
expect(extractParentId('15.3.1')).toBe('15');
|
||||
});
|
||||
|
||||
it('returns same ID for main tasks', () => {
|
||||
expect(extractParentId('1')).toBe('1');
|
||||
expect(extractParentId('15')).toBe('15');
|
||||
expect(extractParentId('HAM-123')).toBe('HAM-123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSubtaskId', () => {
|
||||
it('returns true for numeric subtask IDs (local storage)', () => {
|
||||
expect(isSubtaskId('1.2')).toBe(true);
|
||||
expect(isSubtaskId('1.2.3')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for main task IDs', () => {
|
||||
expect(isSubtaskId('1')).toBe(false);
|
||||
expect(isSubtaskId('15')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for alphanumeric IDs (remote subtasks use separate IDs)', () => {
|
||||
// In remote mode, subtasks have their own alphanumeric IDs (HAM-2, HAM-3)
|
||||
// not dot notation, so HAM-123 is never a "subtask ID" in the dot-notation sense
|
||||
expect(isSubtaskId('HAM-123')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
161
packages/tm-core/src/modules/tasks/validation/task-id.ts
Normal file
161
packages/tm-core/src/modules/tasks/validation/task-id.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* @fileoverview Task ID validation utilities and Zod schemas
|
||||
* Provides validation for task IDs used in MCP tools and CLI
|
||||
*
|
||||
* Supported formats:
|
||||
* - Simple numeric: "1", "2", "15" (local file storage)
|
||||
* - Numeric subtask: "1.2", "15.3" (local file storage, dot notation)
|
||||
* - Numeric sub-subtask: "1.2.3", "15.3.1" (local file storage, dot notation)
|
||||
* - Alphanumeric display IDs: "HAM-123", "PROJ-456" (remote API storage)
|
||||
* Note: In remote mode, subtasks also use alphanumeric IDs (HAM-2, HAM-3),
|
||||
* they don't use dot notation like local storage.
|
||||
*
|
||||
* NOT supported:
|
||||
* - Alphanumeric with dot notation: "HAM-123.2" (doesn't exist in any mode)
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Pattern for validating a single task ID
|
||||
* Supports:
|
||||
* - Numeric: "1", "15", "999"
|
||||
* - Numeric subtasks: "1.2", "15.3.1"
|
||||
* - Alphanumeric display IDs: "HAM-123", "PROJ-456" (main tasks only, no subtask notation)
|
||||
*/
|
||||
export const TASK_ID_PATTERN = /^(\d+(\.\d+)*|[A-Za-z]+-\d+)$/;
|
||||
|
||||
/**
|
||||
* Validates a single task ID string
|
||||
*
|
||||
* @param id - The task ID to validate
|
||||
* @returns True if the ID is valid
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* isValidTaskIdFormat("1"); // true
|
||||
* isValidTaskIdFormat("15.2"); // true
|
||||
* isValidTaskIdFormat("1.2.3"); // true
|
||||
* isValidTaskIdFormat("HAM-123"); // true
|
||||
* isValidTaskIdFormat("HAM-123.2"); // false (alphanumeric subtasks not supported)
|
||||
* isValidTaskIdFormat("abc"); // false
|
||||
* isValidTaskIdFormat(""); // false
|
||||
* ```
|
||||
*/
|
||||
export function isValidTaskIdFormat(id: string): boolean {
|
||||
return TASK_ID_PATTERN.test(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zod schema for a single task ID
|
||||
* Validates format: numeric, alphanumeric display ID, or numeric subtask
|
||||
*/
|
||||
export const taskIdSchema = z
|
||||
.string()
|
||||
.min(1, 'Task ID cannot be empty')
|
||||
.refine(isValidTaskIdFormat, {
|
||||
message:
|
||||
"Invalid task ID format. Expected numeric (e.g., '15'), subtask (e.g., '15.2'), or display ID (e.g., 'HAM-123')"
|
||||
});
|
||||
|
||||
/**
|
||||
* Zod schema for comma-separated task IDs
|
||||
* Validates that each ID in the comma-separated list is valid
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* taskIdsSchema.parse("1"); // valid
|
||||
* taskIdsSchema.parse("1,2,3"); // valid
|
||||
* taskIdsSchema.parse("1.2, 3.4"); // valid (spaces trimmed)
|
||||
* taskIdsSchema.parse("HAM-123"); // valid
|
||||
* taskIdsSchema.parse("abc"); // throws
|
||||
* taskIdsSchema.parse("HAM-123.2"); // throws (alphanumeric subtasks not supported)
|
||||
* ```
|
||||
*/
|
||||
export const taskIdsSchema = z
|
||||
.string()
|
||||
.min(1, 'Task ID(s) cannot be empty')
|
||||
.refine(
|
||||
(value) => {
|
||||
const ids = value
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => id.length > 0);
|
||||
return ids.length > 0 && ids.every(isValidTaskIdFormat);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"Invalid task ID format. Expected numeric (e.g., '15'), subtask (e.g., '15.2'), or display ID (e.g., 'HAM-123'). Multiple IDs should be comma-separated."
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Parse and validate comma-separated task IDs
|
||||
*
|
||||
* @param input - Comma-separated task ID string
|
||||
* @returns Array of validated task IDs
|
||||
* @throws Error if any ID is invalid
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* parseTaskIds("1, 2, 3"); // ["1", "2", "3"]
|
||||
* parseTaskIds("1.2,3.4"); // ["1.2", "3.4"]
|
||||
* parseTaskIds("HAM-123"); // ["HAM-123"]
|
||||
* parseTaskIds("invalid"); // throws Error
|
||||
* parseTaskIds("HAM-123.2"); // throws Error (alphanumeric subtasks not supported)
|
||||
* ```
|
||||
*/
|
||||
export function parseTaskIds(input: string): string[] {
|
||||
const ids = input
|
||||
.split(',')
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => id.length > 0);
|
||||
|
||||
if (ids.length === 0) {
|
||||
throw new Error('No valid task IDs provided');
|
||||
}
|
||||
|
||||
const invalidIds = ids.filter((id) => !isValidTaskIdFormat(id));
|
||||
if (invalidIds.length > 0) {
|
||||
throw new Error(
|
||||
`Invalid task ID format: ${invalidIds.join(', ')}. Expected numeric (e.g., '15'), subtask (e.g., '15.2'), or display ID (e.g., 'HAM-123')`
|
||||
);
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract parent task ID from a subtask ID
|
||||
*
|
||||
* @param taskId - Task ID (e.g., "1.2.3")
|
||||
* @returns Parent ID (e.g., "1") or the original ID if not a subtask
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* extractParentId("1.2.3"); // "1"
|
||||
* extractParentId("1.2"); // "1"
|
||||
* extractParentId("1"); // "1"
|
||||
* ```
|
||||
*/
|
||||
export function extractParentId(taskId: string): string {
|
||||
const parts = taskId.split('.');
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a task ID represents a subtask
|
||||
*
|
||||
* @param taskId - Task ID to check
|
||||
* @returns True if the ID contains a dot (subtask notation)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* isSubtaskId("1.2"); // true
|
||||
* isSubtaskId("1.2.3"); // true
|
||||
* isSubtaskId("1"); // false
|
||||
* ```
|
||||
*/
|
||||
export function isSubtaskId(taskId: string): boolean {
|
||||
return taskId.includes('.');
|
||||
}
|
||||
Reference in New Issue
Block a user