From 2b0cbdbc84fc68e00f1d80de0411eca5a4f630dc Mon Sep 17 00:00:00 2001 From: Eyal Toledano Date: Thu, 18 Sep 2025 17:12:08 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20extends=20the=20tm=20context=20command?= =?UTF-8?q?=20to=20accept=20a=20brief=20ID=20directly=20or=E2=80=A6=20(#12?= =?UTF-8?q?19)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> --- .changeset/pre.json | 1 - .changeset/shiny-regions-teach.md | 2 +- apps/cli/src/commands/context.command.ts | 149 ++++++++++++++++++++++- package-lock.json | 4 +- package.json | 2 +- 5 files changed, 150 insertions(+), 8 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 0056288d..0932951b 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -13,7 +13,6 @@ "easy-deer-heal", "moody-oranges-slide", "odd-otters-tan", - "shiny-regions-teach", "wild-ears-look" ] } diff --git a/.changeset/shiny-regions-teach.md b/.changeset/shiny-regions-teach.md index d819bd09..59c38154 100644 --- a/.changeset/shiny-regions-teach.md +++ b/.changeset/shiny-regions-teach.md @@ -1,5 +1,5 @@ --- -"task-master-ai": major +"task-master-ai": minor --- @tm/cli: add auto-update functionality to every command diff --git a/apps/cli/src/commands/context.command.ts b/apps/cli/src/commands/context.command.ts index d8c441b2..e4c0a73f 100644 --- a/apps/cli/src/commands/context.command.ts +++ b/apps/cli/src/commands/context.command.ts @@ -6,7 +6,7 @@ import { Command } from 'commander'; import chalk from 'chalk'; import inquirer from 'inquirer'; -import ora from 'ora'; +import ora, { Ora } from 'ora'; import { AuthManager, AuthenticationError, @@ -49,8 +49,15 @@ export class ContextCommand extends Command { this.addClearCommand(); this.addSetCommand(); - // Default action shows current context - this.action(async () => { + // Accept optional positional argument for brief ID or Hamster URL + this.argument('[briefOrUrl]', 'Brief ID or Hamster brief URL'); + + // Default action: if an argument is provided, resolve and set context; else show + this.action(async (briefOrUrl?: string) => { + if (briefOrUrl && briefOrUrl.trim().length > 0) { + await this.executeSetFromBriefInput(briefOrUrl.trim()); + return; + } await this.executeShow(); }); } @@ -441,6 +448,142 @@ export class ContextCommand extends Command { } } + /** + * Execute setting context from a brief ID or Hamster URL + */ + private async executeSetFromBriefInput(briefOrUrl: string): Promise { + let spinner: Ora | undefined; + try { + // Check authentication + if (!this.authManager.isAuthenticated()) { + ui.displayError('Not authenticated. Run "tm auth login" first.'); + process.exit(1); + } + + spinner = ora('Resolving brief...'); + spinner.start(); + + // Extract brief ID + const briefId = this.extractBriefId(briefOrUrl); + if (!briefId) { + spinner.fail('Could not extract a brief ID from the provided input'); + ui.displayError( + `Provide a valid brief ID or a Hamster brief URL, e.g. https://${process.env.TM_PUBLIC_BASE_DOMAIN}/home/hamster/briefs/` + ); + process.exit(1); + } + + // Fetch brief and resolve its organization + const brief = await this.authManager.getBrief(briefId); + if (!brief) { + spinner.fail('Brief not found or you do not have access'); + process.exit(1); + } + + // Fetch org to get a friendly name (optional) + let orgName: string | undefined; + try { + const org = await this.authManager.getOrganization(brief.accountId); + orgName = org?.name; + } catch { + // Non-fatal if org lookup fails + } + + // Update context: set org and brief + const briefName = `Brief ${brief.id.slice(0, 8)}`; + await this.authManager.updateContext({ + orgId: brief.accountId, + orgName, + briefId: brief.id, + briefName + }); + + spinner.succeed('Context set from brief'); + console.log( + chalk.gray( + ` Organization: ${orgName || brief.accountId}\n Brief: ${briefName}` + ) + ); + + this.setLastResult({ + success: true, + action: 'set', + context: this.authManager.getContext() || undefined, + message: 'Context set from brief' + }); + } catch (error: any) { + try { + if (spinner?.isSpinning) spinner.stop(); + } catch {} + this.handleError(error); + process.exit(1); + } + } + + /** + * Extract a brief ID from raw input (ID or Hamster URL) + */ + private extractBriefId(input: string): string | null { + const raw = input?.trim() ?? ''; + if (!raw) return null; + + const parseUrl = (s: string): URL | null => { + try { + return new URL(s); + } catch {} + try { + return new URL(`https://${s}`); + } catch {} + return null; + }; + + const fromParts = (path: string): string | null => { + const parts = path.split('/').filter(Boolean); + const briefsIdx = parts.lastIndexOf('briefs'); + const candidate = + briefsIdx >= 0 && parts.length > briefsIdx + 1 + ? parts[briefsIdx + 1] + : parts[parts.length - 1]; + return candidate?.trim() || null; + }; + + // 1) URL (absolute or scheme‑less) + const url = parseUrl(raw); + if (url) { + const qId = url.searchParams.get('id') || url.searchParams.get('briefId'); + const candidate = (qId || fromParts(url.pathname)) ?? null; + if (candidate) { + // Light sanity check; let API be the final validator + if (this.isLikelyId(candidate) || candidate.length >= 8) + return candidate; + } + } + + // 2) Looks like a path without scheme + if (raw.includes('/')) { + const candidate = fromParts(raw); + if (candidate && (this.isLikelyId(candidate) || candidate.length >= 8)) { + return candidate; + } + } + + // 3) Fallback: raw token + return raw; + } + + /** + * Heuristic to check if a string looks like a brief ID (UUID-like) + */ + private isLikelyId(value: string): boolean { + const uuidRegex = + /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; // ULID + const slugRegex = /^[A-Za-z0-9_-]{16,}$/; // general token + return ( + uuidRegex.test(value) || ulidRegex.test(value) || slugRegex.test(value) + ); + } + /** * Set context directly from options */ diff --git a/package-lock.json b/package-lock.json index 00797ce7..5ce68fde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "task-master-ai", - "version": "0.27.0-rc.0", + "version": "1.0.0-rc.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "task-master-ai", - "version": "0.27.0-rc.0", + "version": "1.0.0-rc.2", "license": "MIT WITH Commons-Clause", "workspaces": [ "apps/*", diff --git a/package.json b/package.json index 06d73640..936186a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "task-master-ai", - "version": "1.0.0-rc.2", + "version": "0.27.0-rc.1", "description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.", "main": "index.js", "type": "module",