Skip to content

feat: extends the tm context command to accept a brief ID directly or…#1219

Merged
Crunchyman-ralph merged 3 commits intonextfrom
context-brief-url
Sep 18, 2025
Merged

feat: extends the tm context command to accept a brief ID directly or…#1219
Crunchyman-ralph merged 3 commits intonextfrom
context-brief-url

Conversation

@eyaltoledano
Copy link
Owner

@eyaltoledano eyaltoledano commented Sep 18, 2025

extends the tm context command to accept a brief ID directly or a hamster brief URL from which to use the brief and sets the organization and brief automatically

Summary by CodeRabbit

  • New Features

    • Set CLI context in one step using a brief ID or Hamster brief URL; automatically fetches the brief and associated organization and shows success.
  • Improvements

    • Context command accepts an optional argument to set context directly; if omitted, it still displays the current context.
    • Provides user-friendly errors and progress feedback when setting context.
  • Chores

    • Updated release metadata and pre-release changeset entries; package version adjusted.

@changeset-bot
Copy link

changeset-bot bot commented Sep 18, 2025

⚠️ No Changeset found

Latest commit: fa34a87

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 18, 2025

Walkthrough

Adds an optional positional [briefOrUrl] to the CLI context command to set context from a brief ID or Hamster URL (parses ID, fetches brief and optional org, updates stored context). Also removes a pre-release changeset entry, adjusts a changeset tag, and updates package.json version metadata.

Changes

Cohort / File(s) Summary
CLI: context command
apps/cli/src/commands/context.command.ts
Adds optional positional [briefOrUrl] to root action. New private methods: executeSetFromBriefInput(briefOrUrl), extractBriefId(input), and isLikelyId(value) to parse IDs/URLs, fetch brief (and org), and update context/store with orgId, orgName, briefId, briefName; uses typed Ora spinner and existing error handling.
Changeset: pre-release list
.changeset/pre.json
Removed the \"shiny-regions-teach\" entry from the changesets array in pre-release metadata.
Changeset: single changeset file
.changeset/shiny-regions-teach.md
Changed task-master-ai changeset tag from majorminor (metadata only).
Package metadata
package.json
Version changed from \"1.0.0-rc.2\"\"0.27.0-rc.1\" (metadata only).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant CLI as CLI
  participant C as ContextCommand
  participant B as Brief API
  participant O as Org API
  participant S as Context Store

  U->>CLI: context [briefOrUrl]
  CLI->>C: run(briefOrUrl)
  C->>C: extractBriefId(input)
  alt briefId found
    C->>B: getBrief(briefId)
    B-->>C: brief (contains orgId?)
    opt orgId present
      C->>O: getOrg(orgId)
      O-->>C: org (optional)
    end
    C->>S: set({ orgId, orgName?, briefId, briefName })
    C-->>CLI: success / display result
  else no briefId
    C-->>CLI: error / guidance
  end
Loading
sequenceDiagram
  autonumber
  participant U as User
  participant CLI as CLI
  participant C as ContextCommand
  participant S as Context Store

  U->>CLI: context
  CLI->>C: run()
  C->>S: getCurrent()
  S-->>C: { org?, brief? }
  C-->>CLI: display current context
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • chore: fix pre-release CI #1213 — Modifies the same file (apps/cli/src/commands/context.command.ts) and alters brief/context selection UI logic; likely to overlap or conflict.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title accurately reflects the primary change—extending the tm context command to accept a brief ID or Hamster URL—and uses a conventional "feat:" prefix, so it is directly related to the changeset and PR objectives; the trailing ellipsis suggests truncation but does not make the title misleading.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch context-brief-url

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Collaborator

@Crunchyman-ralph Crunchyman-ralph left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f1cdf78 and 5b15336.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (1)
  • apps/cli/src/commands/context.command.ts (2 hunks)
🔇 Additional comments (3)
apps/cli/src/commands/context.command.ts (3)

52-54: Nice DX: positional arg for brief/URL.

Good, backward‑compatible way to fast‑path the common case.

Please quickly sanity‑check that subcommands still take precedence in Commander:


55-61: Default action flow reads well.

Early return into the new resolver keeps the default “show context” behavior intact when no arg is provided.


451-516: Happy path LGTM; spinner + context update are consistent with existing commands.

Flow mirrors the other executors (auth check, spinner, updateContext, lastResult). Good UX touches with org name fallback.

… a hamster brief URL from which to use the brief and sets the organization and brief automatically
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
apps/cli/src/commands/context.command.ts (3)

468-471: Avoid hard‑coding the host in the error hint.

Make the hint environment‑agnostic (placeholder or derive from config).

Apply:

- ui.displayError(
-   'Provide a valid brief ID or a Hamster brief URL, e.g. https://tux.tryhamster.com/home/hamster/briefs/<id>'
- );
+ ui.displayError(
+   'Provide a valid brief ID or a Hamster brief URL, e.g. https://<host>/.../briefs/<id>'
+ );

550-556: Broaden ID heuristic to cover ULIDs and general sluggy tokens.

Prevents false negatives when users paste non‑UUID IDs.

Apply:

   private isLikelyId(value: string): boolean {
-    // Accept standard UUIDs or reasonably long hex-with-dashes IDs
-    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 looseRegex = /^[0-9a-fA-F-]{16,}$/;
-    return uuidRegex.test(value) || looseRegex.test(value);
+    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 long token
+    return uuidRegex.test(value) || ulidRegex.test(value) || slugRegex.test(value);
   }

521-545: Relax brief ID extraction: accept scheme‑less URLs, query params, and path tokens; let the API be the validator.

Current logic rejects common inputs (e.g., tux.tryhamster.com/... or ULIDs).

Apply:

-  private extractBriefId(input: string): string | null {
-    // If it's a URL, try to parse and take the last path segment
-    try {
-      const url = new URL(input);
-      const parts = url.pathname.split('/').filter(Boolean);
-      // Look for a path segment after 'briefs', otherwise fallback to last segment
-      const briefsIndex = parts.lastIndexOf('briefs');
-      let candidate =
-        briefsIndex >= 0 && parts.length > briefsIndex + 1
-          ? parts[briefsIndex + 1]
-          : parts[parts.length - 1];
-      candidate = candidate?.trim();
-      if (candidate && this.isLikelyId(candidate)) {
-        return candidate;
-      }
-    } catch {
-      // Not a URL, fall through and treat as raw ID
-    }
-
-    // Treat as raw ID
-    if (input && this.isLikelyId(input)) {
-      return input;
-    }
-    return null;
-  }
+  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 && (this.isLikelyId(candidate) || candidate.length >= 8)) {
+        return candidate;
+      }
+    }
+
+    // 2) Path‑like input without scheme
+    if (raw.includes('/')) {
+      const candidate = fromParts(raw);
+      if (candidate && (this.isLikelyId(candidate) || candidate.length >= 8)) {
+        return candidate;
+      }
+    }
+
+    // 3) Fallback: raw token (API will validate)
+    return raw;
+  }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b15336 and ae0c52b.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (1)
  • apps/cli/src/commands/context.command.ts (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test
🔇 Additional comments (1)
apps/cli/src/commands/context.command.ts (1)

52-62: Nice addition: optional positional arg + default action wiring looks good.

Backwards compatible with existing subcommands; help text is clear.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/cli/src/commands/context.command.ts (1)

454-518: Spinner scope bug + exit path: spinner may keep spinning on errors; also guard example host when env is unset.

spinner is declared inside try but referenced in catch; if an error occurs, it may not stop. Also, the error hint can print “https://undefined/...”.

Apply:

-  private async executeSetFromBriefInput(briefOrUrl: string): Promise<void> {
-    try {
+  private async executeSetFromBriefInput(briefOrUrl: string): Promise<void> {
+    let spinner: ReturnType<typeof ora> | undefined;
+    try {
       // Check authentication
       if (!this.authManager.isAuthenticated()) {
         ui.displayError('Not authenticated. Run "tm auth login" first.');
         process.exit(1);
       }
 
-      let spinner = ora('Resolving brief...');
-      spinner.start();
+      spinner = ora('Resolving brief...').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/<id>`
-        );
+        const exampleHost = process.env.TM_PUBLIC_BASE_DOMAIN || '<host>';
+        ui.displayError(
+          `Provide a valid brief ID or a Hamster brief URL, e.g. https://${exampleHost}/home/hamster/briefs/<id>`
+        );
         process.exit(1);
       }
@@
-    } catch (error: any) {
-      try { if (spinner?.isSpinning) spinner.stop(); } catch {}
+    } catch (error: any) {
+      try { if (spinner?.isSpinning) spinner.stop(); } catch {}
       this.handleError(error);
       process.exit(1);
     }
   }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae0c52b and d7c5096.

📒 Files selected for processing (4)
  • .changeset/pre.json (0 hunks)
  • .changeset/shiny-regions-teach.md (1 hunks)
  • apps/cli/src/commands/context.command.ts (2 hunks)
  • package.json (1 hunks)
💤 Files with no reviewable changes (1)
  • .changeset/pre.json
🧰 Additional context used
📓 Path-based instructions (3)
package.json

📄 CodeRabbit inference engine (.cursor/rules/test_workflow.mdc)

Add and update test scripts in package.json to include test, test:watch, test:coverage, test:unit, test:integration, test:e2e, and test:ci

Files:

  • package.json
.changeset/*.md

📄 CodeRabbit inference engine (.cursor/rules/changeset.mdc)

.changeset/*.md: When running npm run changeset or npx changeset add, provide a concise summary of the changes for the CHANGELOG.md in imperative mood, typically a single line, and not a detailed Git commit message.
The changeset summary should be user-facing, describing what changed in the released version that is relevant to users or consumers of the package.
Do not use your detailed Git commit message body as the changeset summary.

Files:

  • .changeset/shiny-regions-teach.md
.changeset/*

📄 CodeRabbit inference engine (.cursor/rules/new_features.mdc)

Create appropriate changesets for new features, use semantic versioning, include tagged system information in release notes, and document breaking changes if any.

Files:

  • .changeset/shiny-regions-teach.md
🧠 Learnings (10)
📚 Learning: 2025-07-18T17:10:12.881Z
Learnt from: CR
PR: eyaltoledano/claude-task-master#0
File: .cursor/rules/dev_workflow.mdc:0-0
Timestamp: 2025-07-18T17:10:12.881Z
Learning: For CLI usage, install Taskmaster globally with `npm install -g task-master-ai` or use locally via `npx task-master-ai ...`.

Applied to files:

  • package.json
  • .changeset/shiny-regions-teach.md
📚 Learning: 2025-07-18T17:12:57.903Z
Learnt from: CR
PR: eyaltoledano/claude-task-master#0
File: .cursor/rules/new_features.mdc:0-0
Timestamp: 2025-07-18T17:12:57.903Z
Learning: Applies to .changeset/* : Create appropriate changesets for new features, use semantic versioning, include tagged system information in release notes, and document breaking changes if any.

Applied to files:

  • .changeset/shiny-regions-teach.md
📚 Learning: 2025-07-18T17:19:27.365Z
Learnt from: CR
PR: eyaltoledano/claude-task-master#0
File: assets/.windsurfrules:0-0
Timestamp: 2025-07-18T17:19:27.365Z
Learning: Use `task-master add-task` to add a new task to tasks.json using AI.

Applied to files:

  • .changeset/shiny-regions-teach.md
📚 Learning: 2025-07-18T17:19:27.365Z
Learnt from: CR
PR: eyaltoledano/claude-task-master#0
File: assets/.windsurfrules:0-0
Timestamp: 2025-07-18T17:19:27.365Z
Learning: Use the global `task-master` CLI command instead of directly invoking `node scripts/dev.js` for all task management operations.

Applied to files:

  • .changeset/shiny-regions-teach.md
📚 Learning: 2025-09-03T12:15:03.208Z
Learnt from: Crunchyman-ralph
PR: eyaltoledano/claude-task-master#1178
File: apps/cli/src/commands/auth.command.ts:222-224
Timestamp: 2025-09-03T12:15:03.208Z
Learning: The CLI can be invoked using both "task-master" and "tm" as aliases - both forms are valid and acceptable in help text, documentation, and examples.

Applied to files:

  • .changeset/shiny-regions-teach.md
📚 Learning: 2025-07-18T17:10:12.881Z
Learnt from: CR
PR: eyaltoledano/claude-task-master#0
File: .cursor/rules/dev_workflow.mdc:0-0
Timestamp: 2025-07-18T17:10:12.881Z
Learning: Use the Taskmaster command set (`task-master` CLI or MCP tools) for all task management operations: listing, expanding, updating, tagging, and status changes.

Applied to files:

  • .changeset/shiny-regions-teach.md
📚 Learning: 2025-07-18T17:10:53.657Z
Learnt from: CR
PR: eyaltoledano/claude-task-master#0
File: .cursor/rules/glossary.mdc:0-0
Timestamp: 2025-07-18T17:10:53.657Z
Learning: Guidelines for integrating new features into the Task Master CLI with tagged system considerations (new_features.mdc).

Applied to files:

  • .changeset/shiny-regions-teach.md
📚 Learning: 2025-07-31T22:07:49.716Z
Learnt from: CR
PR: eyaltoledano/claude-task-master#0
File: .cursor/rules/commands.mdc:0-0
Timestamp: 2025-07-31T22:07:49.716Z
Learning: Applies to scripts/modules/commands.js : Implement version checking to notify users of available updates, use non-blocking version checks, and display update notifications after command completion.

Applied to files:

  • .changeset/shiny-regions-teach.md
📚 Learning: 2025-07-31T22:07:49.716Z
Learnt from: CR
PR: eyaltoledano/claude-task-master#0
File: .cursor/rules/commands.mdc:0-0
Timestamp: 2025-07-31T22:07:49.716Z
Learning: Applies to scripts/modules/commands.js : Integrate version checking in the CLI run function, starting the update check in the background and displaying notifications after command execution.

Applied to files:

  • .changeset/shiny-regions-teach.md
📚 Learning: 2025-07-31T22:07:49.716Z
Learnt from: CR
PR: eyaltoledano/claude-task-master#0
File: .cursor/rules/commands.mdc:0-0
Timestamp: 2025-07-31T22:07:49.716Z
Learning: Applies to scripts/modules/commands.js : For AI-powered commands that benefit from project context, use the ContextGatherer utility for multi-source context extraction, support task IDs, file paths, custom context, and project tree, implement fuzzy search for automatic task discovery, and display detailed token breakdown for transparency.

Applied to files:

  • apps/cli/src/commands/context.command.ts
🧬 Code graph analysis (1)
apps/cli/src/commands/context.command.ts (1)
tests/unit/ui.test.js (1)
  • chalk (17-17)
🪛 GitHub Actions: CI
apps/cli/src/commands/context.command.ts

[error] 512-585: Biome format check failed. Formatting issues detected by the Biome formatter; the content would be reformatted. Run 'npm run format-check' to fix.

🔇 Additional comments (3)
package.json (1)

3-3: Pre-release flow correct — no version regression

package.json: 0.27.0-rc.1; .changeset/pre.json: mode="pre", tag="rc", initialVersions.task-master-ai="0.26.0". Bump 0.26.0 → 0.27.0-rc.1 matches the pre-release flow; the reported regression from 1.0.0-rc.2 → 0.27.0-rc.1 is incorrect. No action required.

apps/cli/src/commands/context.command.ts (2)

520-575: Formatting/lint and small robustness tweaks in extractBriefId (format aborted — Biome config error)

File: apps/cli/src/commands/context.command.ts (extractBriefId). Running npm run format failed: Biome errored on unknown keys ("ignore", "include") in biome.json — fix the config or run the formatter locally and re-run CI.

-    const parseUrl = (s: string): URL | null => {
-      try { return new URL(s); } catch {}
-      try { return new URL(`https://${s}`); } catch {}
-      return null;
-    };
+    const parseUrl = (s: string): URL | null => {
+      try {
+        return new URL(s);
+      } catch {
+        /* ignore */
+      }
+      try {
+        return new URL(`https://${s}`);
+      } catch {
+        /* ignore */
+      }
+      return null;
+    };
@@
-      const qId = url.searchParams.get('id') || url.searchParams.get('briefId');
+      const qId =
+        url.searchParams.get('id') ||
+        url.searchParams.get('briefId') ||
+        url.searchParams.get('brief');

52-61: Docs: document the new optional [briefOrUrl] positional in CLI help/examples and README

The command adds this.argument('[briefOrUrl]', 'Brief ID or Hamster brief URL') at apps/cli/src/commands/context.command.ts:52–56 — update docs/command-reference.md and README-task-master.md with usage examples like:

  • tm context
  • tm context https://<TM_PUBLIC_BASE_DOMAIN>/home/hamster/briefs/

"task-master-ai": minor
---

@tm/cli: add auto-update functionality to every command
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Changeset summary doesn’t match this PR; make it user-facing and about tm context brief/URL support.

Use a concise, imperative, user-facing line describing the new behavior.

Apply:

-@tm/cli: add auto-update functionality to every command
+Add support for passing a brief ID or Hamster brief URL to `tm context` to auto-set organization and brief
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@tm/cli: add auto-update functionality to every command
Add support for passing a brief ID or Hamster brief URL to `tm context` to auto-set organization and brief
🧰 Tools
🪛 markdownlint-cli2 (0.17.2)

5-5: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🤖 Prompt for AI Agents
.changeset/shiny-regions-teach.md around line 5: the changeset summary is not
user-facing and doesn't match the PR; replace the current terse note "@tm/cli:
add auto-update functionality to every command" with a concise, imperative,
user-facing summary that matches the PR scope — e.g. "tm: add brief/URL support
for context and enable auto-update for commands" (or adjust wording to exactly
reflect the PR), keeping it a single line and imperative.

@Crunchyman-ralph Crunchyman-ralph merged commit 2b0cbdb into next Sep 18, 2025
5 of 6 checks passed
@Crunchyman-ralph Crunchyman-ralph deleted the context-brief-url branch September 18, 2025 21:12
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/cli/src/commands/context.command.ts (1)

466-475: Avoid “https://undefined/…” in error hint; add a safe fallback host.

If TM_PUBLIC_BASE_DOMAIN isn’t set, the hint prints undefined. Use a placeholder fallback.

Apply:

-				ui.displayError(
-					`Provide a valid brief ID or a Hamster brief URL, e.g. https://${process.env.TM_PUBLIC_BASE_DOMAIN}/home/hamster/briefs/<id>`
-				);
+				const host = process.env.TM_PUBLIC_BASE_DOMAIN || '<host>';
+				ui.displayError(
+					`Provide a valid brief ID or a Hamster brief URL, e.g. https://${host}/.../briefs/<id>`
+				);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d7c5096 and fa34a87.

📒 Files selected for processing (1)
  • apps/cli/src/commands/context.command.ts (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test
🔇 Additional comments (3)
apps/cli/src/commands/context.command.ts (3)

9-9: Typed ora import is correct.

Using ora, { Ora } keeps the spinner typed and avoids any.


52-61: Confirm subcommand precedence with new positional arg.

Commander should prefer subcommands over the root action, but please verify:

  • tm context org still invokes the org flow, not executeSetFromBriefInput('org').
  • tm context set --org ... still routes to the set subcommand.

577-585: ID heuristic looks good.

Covers UUID, ULID, and long slug tokens; balanced without over‑restricting.

Comment on lines +526 to +572
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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

❓ Verification inconclusive

Don’t return the entire URL as an ID; percent‑decode extracted segments.

When no ID is found in a URL/path, extractBriefId falls back to returning the raw input, which can pass a full URL to getBrief(...) and produce misleading “not found” errors. Also, handle percent‑encoded path segments.

Apply:

 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 safeDecode = (s: string): string => {
+    try {
+      return decodeURIComponent(s);
+    } catch {
+      return s;
+    }
+  };
+
   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;
+    const trimmed = candidate?.trim();
+    return trimmed ? safeDecode(trimmed) : 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;
+    const qId = url.searchParams.get('id') || url.searchParams.get('briefId');
+    const rawCandidate = qId || fromParts(url.pathname);
+    const candidate = rawCandidate ? safeDecode(rawCandidate) : 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;
     }
-  }
+    // Looked like a path but no plausible ID
+    return null;
+  }

   // 3) Fallback: raw token
-  return raw;
+  return raw;
 }

Follow‑ups:

  • Consider also accepting brief as a query key if that’s used elsewhere.

Don't return the entire URL as the brief ID; percent‑decode extracted segments and return null when no plausible ID exists.

File: apps/cli/src/commands/context.command.ts — extractBriefId (lines 526–572)

extractBriefId can pass a full URL/domain to getBrief(...) when parsing fails. Add a safe decode (decodeURIComponent with try/catch) and apply it to query/path segments (qId and fromParts), decode before validation, and if raw.includes('/') but no plausible id was found, return null instead of falling back to the raw input.

  • Add safeDecode(s) and apply to qId and fromParts result.
  • Decode candidate before calling isLikelyId / length checks.
  • In the raw.includes('/') branch, return null when candidate isn't plausible (prevent returning a URL/domain).
  • Optional: accept query key 'brief' as well.
🤖 Prompt for AI Agents
In apps/cli/src/commands/context.command.ts around lines 526–572, extractBriefId
currently may return the full URL/domain and doesn't percent‑decode segments;
add a small helper safeDecode(s: string): string | null that uses try { return
decodeURIComponent(s) } catch { return null }, use it to decode the query values
(check url.searchParams for 'id', 'briefId', and 'brief') and the value returned
from fromParts before any validation, then run isLikelyId/length checks against
the decoded candidate; also change the raw.includes('/') branch so that if a
candidate exists but is not plausible you return null (do not fall back to
returning the raw input), leaving the final fallback to return raw only when no
slash was present and no decoding/parsing applied.

github-actions bot added a commit that referenced this pull request Sep 18, 2025
  This PR was automatically generated to update documentation based on recent changes.

  Original commit: feat: extends the tm context command to accept a brief ID directly or… (#1219)\n\nCo-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>\n\n

  Co-authored-by: Claude <claude-assistant@anthropic.com>
sfc-gh-dflippo pushed a commit to sfc-gh-dflippo/task-master-ai that referenced this pull request Dec 4, 2025
eyaltoledano#1219)

Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants