Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [22]
node: [20, 22]

steps:
- uses: actions/checkout@v4
Expand Down
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.7.7] - 2026-02-07

### Added

- `agents status --fast` to skip external CLI probes for quicker status checks.
- `agents doctor --fix-dry-run` to preview automatic fixes without mutating project files.
- Sync lock handling for `.agents/generated/sync.lock` to guard against concurrent sync runs.
- New shell-style argument tokenizer for interactive `agents mcp add` argument input.
- New test suites:
- `tests/status.command.test.ts`
- `tests/sync-lock.integration.test.ts`
- `tests/shell-words.test.ts`

### Changed

- npm package publishing is now stricter and reproducible via:
- `prepack` build hook
- explicit `files` whitelist in `package.json`
- CI matrix now validates Node.js `20` and `22` across Linux, macOS, and Windows.
- Interactive `agents mcp add` now parses quoted/escaped args reliably instead of naive whitespace splitting.
- README command reference updated for `status --fast` and `doctor --fix-dry-run`.
- CLI version bumped to `0.7.7`.

### Fixed

- Reduced sync race conditions by introducing project-local sync lock acquisition before writes.
- URL-based MCP import now uses timeout + retry behavior, improving resilience to transient network failures.

## [0.7.6] - 2026-02-05

### Added
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ agents connect [--path <dir>] [--llm codex,claude,gemini,copilot_vscode,cursor,a
agents disconnect [--path <dir>] [--llm codex,claude,gemini,copilot_vscode,cursor,antigravity] [--interactive]
agents sync [--path <dir>] [--check] [--verbose]
agents watch [--path <dir>] [--interval <ms>] [--once] [--quiet]
agents status [--path <dir>] [--json] [--verbose]
agents doctor [--path <dir>] [--fix]
agents status [--path <dir>] [--json] [--verbose] [--fast]
agents doctor [--path <dir>] [--fix] [--fix-dry-run]
agents reset [--path <dir>] [--local-only] [--hard]
agents mcp list [--path <dir>] [--json]
agents mcp add [name] [--path <dir>] [--transport stdio|http|sse] [--command <cmd>] [--arg <value>] [--url <url>] [--env KEY=VALUE] [--header KEY=VALUE] [--secret-env KEY=VALUE] [--secret-header KEY=VALUE] [--secret-arg index=value] [--target <integration>] [--replace]
Expand All @@ -73,7 +73,7 @@ agents mcp test [name] [--path <dir>] [--json] [--runtime] [--runtime-timeout-ms
agents mcp doctor [name] [--path <dir>] [--json] [--runtime] [--runtime-timeout-ms <ms>]
```

## MCP toolkit (0.7.6)
## MCP toolkit (0.7.7)
- `agents mcp add`: add one server via flags or interactive prompts; if `[name]` is an `http(s)` URL, it auto-runs import flow.
- `agents mcp import`: import strict JSON/JSONC snippets (`--file`, `--json`, `--url`, or stdin).
- Interactive import now prompts for template secret values (tokens/keys) and lets you skip with Enter; skipped values can be added later in `.agents/local.json`.
Expand Down Expand Up @@ -106,6 +106,8 @@ agents status --verbose
## Output UX
- `agents status` prints a compact summary by default.
- `agents status --verbose` prints full files/probes details.
- `agents status --fast` skips external CLI probes for faster local checks.
- `agents doctor --fix-dry-run` previews automatic fixes without mutating files.

## Project layout
```text
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
{
"name": "agents-cli-dev",
"version": "0.7.6",
"version": "0.7.7",
"description": "Onboarding-first CLI for AGENTS.md, MCP, and skills across LLM tools",
"main": "dist/cli.js",
"scripts": {
"build": "tsc -p tsconfig.json",
"dev": "tsx src/cli.ts",
"lint": "eslint .",
"test": "vitest run"
"test": "vitest run",
"prepack": "npm run build"
},
"bin": {
"agents": "bin/agents"
},
"files": [
"bin/",
"dist/",
"templates/",
"docs/",
"README.md",
"CHANGELOG.md",
"LICENSE"
],
"keywords": [
"agents",
"mcp",
Expand Down
14 changes: 9 additions & 5 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async function main(): Promise<void> {
program
.name('agents')
.description('Onboarding-first CLI for AGENTS.md + MCP + skills across LLM coding tools')
.version('0.7.6')
.version('0.7.7')

program
.command('start')
Expand Down Expand Up @@ -121,11 +121,13 @@ async function main(): Promise<void> {
.option('--path <dir>', 'Target project directory', process.cwd())
.option('--json', 'Output machine-readable JSON', false)
.option('--verbose', 'Show full files/probes breakdown', false)
.action(async (opts: { path: string; json: boolean; verbose: boolean }) => {
.option('--fast', 'Skip external CLI probes for quicker output', false)
.action(async (opts: { path: string; json: boolean; verbose: boolean; fast: boolean }) => {
await runStatus({
projectRoot: resolvePath(opts.path),
json: Boolean(opts.json),
verbose: Boolean(opts.verbose)
verbose: Boolean(opts.verbose),
fast: Boolean(opts.fast)
})
})

Expand All @@ -134,10 +136,12 @@ async function main(): Promise<void> {
.description('Validate setup and detect configuration problems')
.option('--path <dir>', 'Target project directory', process.cwd())
.option('--fix', 'Apply safe automatic fixes', false)
.action(async (opts: { path: string; fix: boolean }) => {
.option('--fix-dry-run', 'Preview what --fix would change without applying it', false)
.action(async (opts: { path: string; fix: boolean; fixDryRun: boolean }) => {
await runDoctor({
projectRoot: resolvePath(opts.path),
fix: Boolean(opts.fix)
fix: Boolean(opts.fix),
fixDryRun: Boolean(opts.fixDryRun)
})
})

Expand Down
77 changes: 72 additions & 5 deletions src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { IntegrationName } from '../types.js'
export interface DoctorOptions {
projectRoot: string
fix: boolean
fixDryRun?: boolean
}

interface Issue {
Expand All @@ -26,8 +27,16 @@ interface Issue {
}

export async function runDoctor(options: DoctorOptions): Promise<void> {
if (options.fix && options.fixDryRun) {
throw new Error('Use either --fix or --fix-dry-run, not both.')
}

const applyFixes = options.fix === true
const previewFixes = options.fixDryRun === true

const paths = getProjectPaths(options.projectRoot)
const issues: Issue[] = []
const actions: string[] = []

if (!(await pathExists(paths.agentsConfig))) {
issues.push({ level: 'error', message: 'Missing .agents/agents.json (run agents start)' })
Expand Down Expand Up @@ -119,9 +128,11 @@ export async function runDoctor(options: DoctorOptions): Promise<void> {
}

const codexEnabled = config.integrations.enabled.includes('codex')
let codexTrustNeedsFix = false
if (codexEnabled) {
const codexTrust = await getCodexTrustState(options.projectRoot)
if (codexTrust !== 'trusted' && !options.fix) {
codexTrustNeedsFix = codexTrust !== 'trusted'
if (codexTrustNeedsFix && !applyFixes && !previewFixes) {
issues.push({
level: 'warning',
message: 'Codex project trust is not set; project .codex/config.toml may be ignored.'
Expand All @@ -146,40 +157,56 @@ export async function runDoctor(options: DoctorOptions): Promise<void> {
for (const candidate of trackedChecks) {
if (!isGitTracked(options.projectRoot, candidate)) continue
trackedByGit.push(candidate)
if (!options.fix) {
if (!applyFixes && !previewFixes) {
issues.push({
level: 'warning',
message: `"${candidate}" is tracked by git. Ignore rules do not affect tracked files; use "git rm --cached ${candidate}" if needed.`
})
}
}

if (options.fix) {
if (previewFixes) {
if (trackedByGit.length > 0) {
actions.push(`Would untrack git paths: ${trackedByGit.join(', ')}`)
}
if (codexEnabled && codexTrustNeedsFix) {
actions.push('Would set Codex project trust for this repo.')
}
actions.push('Would run agents sync after fixes.')
}

if (applyFixes) {
for (const trackedPath of trackedByGit) {
const removed = untrackGitPath(options.projectRoot, trackedPath)
if (!removed) {
issues.push({ level: 'warning', message: `Failed to untrack "${trackedPath}" automatically.` })
} else {
actions.push(`Untracked "${trackedPath}" from git index.`)
}
}
if (codexEnabled) {
if (codexEnabled && codexTrustNeedsFix) {
try {
await ensureCodexProjectTrusted(options.projectRoot)
actions.push('Set Codex project trust.')
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
issues.push({ level: 'warning', message: `Failed to set Codex trust automatically: ${message}` })
}
}
await performSync({ projectRoot: options.projectRoot, check: false, verbose: false })
actions.push('Ran agents sync.')
}

if (config.integrations.enabled.includes('antigravity') && !(await pathExists(paths.antigravityProjectMcp)) && !options.fix) {
if (config.integrations.enabled.includes('antigravity') && !(await pathExists(paths.antigravityProjectMcp)) && !applyFixes) {
issues.push({
level: 'warning',
message: 'Antigravity project MCP file missing: .antigravity/mcp.json (run agents sync).'
})
}

report(issues)
reportActions(actions, previewFixes, applyFixes)
reportNextSteps(issues, previewFixes, applyFixes)

const hasErrors = issues.some((issue) => issue.level === 'error')
if (hasErrors) {
Expand All @@ -199,6 +226,46 @@ function report(issues: Issue[]): void {
}
}

function reportActions(actions: string[], previewFixes: boolean, applyFixes: boolean): void {
if (actions.length === 0) return

if (previewFixes) {
process.stdout.write('Doctor fix dry-run:\n')
} else if (applyFixes) {
process.stdout.write('Doctor applied fixes:\n')
} else {
return
}

for (const action of actions) {
process.stdout.write(`- ${action}\n`)
}
}

function reportNextSteps(issues: Issue[], previewFixes: boolean, applyFixes: boolean): void {
const hasErrors = issues.some((issue) => issue.level === 'error')
const hasWarnings = issues.some((issue) => issue.level === 'warning')

if (hasErrors) {
process.stdout.write('Next: resolve errors and rerun "agents doctor".\n')
return
}

if (applyFixes) {
process.stdout.write('Next: run "agents status --verbose" to verify runtime state.\n')
return
}

if (previewFixes) {
process.stdout.write('Next: run "agents doctor --fix" to apply these changes.\n')
return
}

if (hasWarnings) {
process.stdout.write('Next: run "agents doctor --fix-dry-run" to preview automatic fixes.\n')
}
}

function isGitTracked(projectRoot: string, relativePath: string): boolean {
if (!commandExists('git')) return false
const result = runCommand('git', ['ls-files', '--error-unmatch', relativePath], projectRoot)
Expand Down
5 changes: 2 additions & 3 deletions src/commands/mcp-add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from '../core/mcpValidation.js'
import { performSync } from '../core/sync.js'
import { formatWarnings } from '../core/warnings.js'
import { parseShellWords } from '../core/shellWords.js'

export interface McpAddOptions {
projectRoot: string
Expand Down Expand Up @@ -95,9 +96,7 @@ export async function runMcpAdd(options: McpAddOptions): Promise<void> {
}
if (transport === 'stdio' && args.length === 0) {
const raw = await promptOptionalText('Args (space-separated, optional)')
if (raw.trim().length > 0) {
args = raw.split(/\s+/).filter(Boolean)
}
args = parseShellWords(raw)
}
}

Expand Down
Loading