2026-08-08 09:34:15 +01:00
import { describe , it , expect , beforeEach , afterEach , vi } from 'vitest' ;
fix(cli): close second review on prime-agent sync + switcher (auth, ownership, switching)
Addresses the second round of `config prime-agent` review (10 findings).
auth.json (config/prime-agent.ts) — the settings.json data-loss fix had a twin:
- loadPrimeAgentAuth now fails loudly on corrupt JSON instead of swallow-and-
rewrite, so one syntax error can no longer destroy the provider API key and
every other project's credential. hasPrimeAgentAuth shares that guarantee.
- writePrimeAgentAuth writes 0600 (preserving an existing file's mode) instead
of the default umask — bearer tokens are no longer world-readable on first
creation.
state ownership (commands/skills.ts) — the ownership model edge cases:
- orphan-removal guard now normalises a canonical scope (project name, or null
for globals; legacy undefined adopted to current scope) instead of comparing
null against undefined, so global-only syncs and pre-PR state can no longer
leave stale skills on disk forever.
- a same-named skill *tracked* to a different project is preserved (with a
warning) rather than silently overwritten in the shared flat tree.
- mcpServers auto-attach is gated behind !isPrimeAgent with an explicit warning
(prime-agent's HTTP gateway must not mutate shared mcpd project attachments);
this also makes the earlier dropped-attach concern explicit rather than silent.
single-active project + switcher (config/prime-agent.ts, prime-agent-extension.ts):
- registerPrimeAgentMcp tags the project's entry mcpctlManaged:true and removes
other mcpctl-managed entries, so prime-agent has one *active* mcpctl project
while preserving untagged servers (hand-configured sre, websearch, etc).
- the /mcpctl extension now reads that tag as the single source of truth for the
active project, fixing the false short-circuit / no-op switch.
config.ts command:
- an explicit -p now updates a differing up-tree .mcpctl-project marker (scope
no longer silently reverts on the next sync), no-ops when it matches, and
still never scopes $HOME.
- a project left with no usable credential now exits non-zero (the /mcpctl
extension checks child exit status, so it no longer reports a successful
switch after provisioning failed).
- skills sync is treated as best-effort: settings+auth determine switch success,
so a skills error no longer falsely fails the switch.
- token minting now revokes prior active `prime-agent` tokens before creating a
fresh one (no more never-expiring token litter / lost-credential duplication).
Tests (544 green): corrupt auth.json refusal, 0600 mode, mint-failure exit
code, single-active dedup preserving untagged sre, cross-project overwrite
preservation, and global-orphan removal on global-only sync.
2026-08-08 11:28:15 +01:00
import { writeFileSync , readFileSync , mkdtempSync , rmSync , existsSync , statSync } from 'node:fs' ;
2026-08-08 09:34:15 +01:00
import { join } from 'node:path' ;
fix(cli): harden `config prime-agent` sync + install /mcpctl switcher extension
Addresses a review of the `config prime-agent` feature and adds the in-app
project switcher.
Safety/correctness fixes (prime-agent's shared, hand-editable ~/.prime/agent
tree must never suffer silent data loss):
- config/prime-agent.ts: loadPrimeAgentSettings now fails loudly on corrupt
JSON instead of swallowing it and rewriting the file (which destroyed every
non-mcpServers setting). A project's mcpServers entry is merged (keeping
user-added fields) rather than replaced wholesale. Added writePrimeAgentAuth
/ hasPrimeAgentAuth helpers for auth provisioning.
- skills sync: unified the near-verbatim prime-agent copy into runSkillsSync
via a `target: 'claude' | 'prime-agent'` option (prime-agent-skills.ts is now
a thin wrapper). Under the prime-agent target it: preserves untracked
pre-existing skill dirs on first sync (no more rm -rf of hand-authored `sre`),
records per-project ownership so configuring a second project never deletes
the first project's skills, skips Claude-only hooks/postInstall, and keeps
the mcpServers auto-attach step.
- config.ts: `config prime-agent` now (a) provisions the bearer credential in
auth.json (--token, existing entry, or auto-mint via POST /api/v1/mcptokens),
(b) writes the .mcpctl-project marker only when none exists up-tree and never
from $HOME, and (c) propagates the skills sync exit code so auth failures are
reported instead of swallowing them.
- skills.ts: `--agent` is validated; an unknown value errors instead of
silently running the Claude sync.
New feature: `config prime-agent` installs a `/mcpctl` project-switcher
extension into ~/.prime/agent/extensions/ (skip with --skip-extension). It lists
mcpctl projects via `mcpctl get projects -o json`, lets you pick one from the
prime-agent TUI, applies the switch through the CLI, and reloads the session.
Regenerated shell completions. Tests: 538 pass (new coverage for settings
corruption, entry merge, auth provisioning, extension install/skip, marker
$HOME handling, untracked/cross-project skill preservation, --agent validation).
2026-08-08 10:22:46 +01:00
import { tmpdir , homedir } from 'node:os' ;
2026-08-08 09:34:15 +01:00
import { createConfigCommand } from '../../src/commands/config.js' ;
import type { ApiClient } from '../../src/api-client.js' ;
import { DEFAULT_MCPCTL_GATEWAY_URL } from '../../src/config/prime-agent.js' ;
function mockClient ( ) : ApiClient {
return {
get : vi . fn ( async ( ) = > ( { } ) ) ,
post : vi.fn ( async ( ) = > ( { token : 'impersonated-tok' , user : { email : 'other@test.com' } } ) ) ,
put : vi.fn ( async ( ) = > ( { } ) ) ,
delete : vi . fn ( async ( ) = > { } ) ,
} as unknown as ApiClient ;
}
describe ( 'config prime-agent' , ( ) = > {
let client : ReturnType < typeof mockClient > ;
let output : string [ ] ;
let tmpDir : string ;
const log = ( . . . args : string [ ] ) = > output . push ( args . join ( ' ' ) ) ;
let prevCwd : string ;
beforeEach ( ( ) = > {
client = mockClient ( ) ;
output = [ ] ;
tmpDir = mkdtempSync ( join ( tmpdir ( ) , 'mcpctl-config-prime-agent-' ) ) ;
// config prime-agent writes the .mcpctl-project marker into cwd, so run
// every test from an isolated temp dir to avoid polluting the repo.
prevCwd = process . cwd ( ) ;
process . chdir ( tmpDir ) ;
} ) ;
afterEach ( ( ) = > {
process . chdir ( prevCwd ) ;
process . exitCode = 0 ;
rmSync ( tmpDir , { recursive : true , force : true } ) ;
} ) ;
it ( 'requires --project' , async ( ) = > {
const cmd = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
await cmd . parseAsync ( [ 'prime-agent' , '--skip-skills' ] , { from : 'user' } ) ;
expect ( output . join ( '\n' ) ) . toContain ( '--project is required' ) ;
expect ( process . exitCode ) . toBe ( 1 ) ;
} ) ;
it ( 'writes proxy MCP entry into prime-agent settings.json' , async ( ) = > {
const settingsPath = join ( tmpDir , 'settings.json' ) ;
const cmd = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
await cmd . parseAsync ( [ 'prime-agent' , '--project' , 'homeautomation' , '-o' , settingsPath , '--skip-skills' ] , { from : 'user' } ) ;
const written = JSON . parse ( readFileSync ( settingsPath , 'utf-8' ) ) ;
expect ( written . mcpServers [ 'homeautomation' ] ) . toEqual ( {
type : 'http' ,
url : ` ${ DEFAULT_MCPCTL_GATEWAY_URL } /projects/homeautomation/mcp ` ,
fix(cli): close second review on prime-agent sync + switcher (auth, ownership, switching)
Addresses the second round of `config prime-agent` review (10 findings).
auth.json (config/prime-agent.ts) — the settings.json data-loss fix had a twin:
- loadPrimeAgentAuth now fails loudly on corrupt JSON instead of swallow-and-
rewrite, so one syntax error can no longer destroy the provider API key and
every other project's credential. hasPrimeAgentAuth shares that guarantee.
- writePrimeAgentAuth writes 0600 (preserving an existing file's mode) instead
of the default umask — bearer tokens are no longer world-readable on first
creation.
state ownership (commands/skills.ts) — the ownership model edge cases:
- orphan-removal guard now normalises a canonical scope (project name, or null
for globals; legacy undefined adopted to current scope) instead of comparing
null against undefined, so global-only syncs and pre-PR state can no longer
leave stale skills on disk forever.
- a same-named skill *tracked* to a different project is preserved (with a
warning) rather than silently overwritten in the shared flat tree.
- mcpServers auto-attach is gated behind !isPrimeAgent with an explicit warning
(prime-agent's HTTP gateway must not mutate shared mcpd project attachments);
this also makes the earlier dropped-attach concern explicit rather than silent.
single-active project + switcher (config/prime-agent.ts, prime-agent-extension.ts):
- registerPrimeAgentMcp tags the project's entry mcpctlManaged:true and removes
other mcpctl-managed entries, so prime-agent has one *active* mcpctl project
while preserving untagged servers (hand-configured sre, websearch, etc).
- the /mcpctl extension now reads that tag as the single source of truth for the
active project, fixing the false short-circuit / no-op switch.
config.ts command:
- an explicit -p now updates a differing up-tree .mcpctl-project marker (scope
no longer silently reverts on the next sync), no-ops when it matches, and
still never scopes $HOME.
- a project left with no usable credential now exits non-zero (the /mcpctl
extension checks child exit status, so it no longer reports a successful
switch after provisioning failed).
- skills sync is treated as best-effort: settings+auth determine switch success,
so a skills error no longer falsely fails the switch.
- token minting now revokes prior active `prime-agent` tokens before creating a
fresh one (no more never-expiring token litter / lost-credential duplication).
Tests (544 green): corrupt auth.json refusal, 0600 mode, mint-failure exit
code, single-active dedup preserving untagged sre, cross-project overwrite
preservation, and global-orphan removal on global-only sync.
2026-08-08 11:28:15 +01:00
mcpctlManaged : true ,
2026-08-08 09:34:15 +01:00
} ) ;
expect ( output . join ( '\n' ) ) . toContain ( 'homeautomation' ) ;
} ) ;
it ( 'merges with existing servers and preserves other settings' , async ( ) = > {
const settingsPath = join ( tmpDir , 'settings.json' ) ;
writeFileSync ( settingsPath , JSON . stringify ( {
defaultProvider : 'itaz' ,
mcpServers : {
sre : { type : 'http' , url : ` ${ DEFAULT_MCPCTL_GATEWAY_URL } /projects/sre/mcp ` } ,
} ,
} ) ) ;
const cmd = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
await cmd . parseAsync ( [ 'prime-agent' , '--project' , 'proj-1' , '-o' , settingsPath , '--skip-skills' ] , { from : 'user' } ) ;
const written = JSON . parse ( readFileSync ( settingsPath , 'utf-8' ) ) ;
expect ( written . defaultProvider ) . toBe ( 'itaz' ) ; // untouched
expect ( written . mcpServers [ 'sre' ] ) . toBeDefined ( ) ; // preserved
expect ( written . mcpServers [ 'proj-1' ] ) . toEqual ( {
type : 'http' ,
url : ` ${ DEFAULT_MCPCTL_GATEWAY_URL } /projects/proj-1/mcp ` ,
fix(cli): close second review on prime-agent sync + switcher (auth, ownership, switching)
Addresses the second round of `config prime-agent` review (10 findings).
auth.json (config/prime-agent.ts) — the settings.json data-loss fix had a twin:
- loadPrimeAgentAuth now fails loudly on corrupt JSON instead of swallow-and-
rewrite, so one syntax error can no longer destroy the provider API key and
every other project's credential. hasPrimeAgentAuth shares that guarantee.
- writePrimeAgentAuth writes 0600 (preserving an existing file's mode) instead
of the default umask — bearer tokens are no longer world-readable on first
creation.
state ownership (commands/skills.ts) — the ownership model edge cases:
- orphan-removal guard now normalises a canonical scope (project name, or null
for globals; legacy undefined adopted to current scope) instead of comparing
null against undefined, so global-only syncs and pre-PR state can no longer
leave stale skills on disk forever.
- a same-named skill *tracked* to a different project is preserved (with a
warning) rather than silently overwritten in the shared flat tree.
- mcpServers auto-attach is gated behind !isPrimeAgent with an explicit warning
(prime-agent's HTTP gateway must not mutate shared mcpd project attachments);
this also makes the earlier dropped-attach concern explicit rather than silent.
single-active project + switcher (config/prime-agent.ts, prime-agent-extension.ts):
- registerPrimeAgentMcp tags the project's entry mcpctlManaged:true and removes
other mcpctl-managed entries, so prime-agent has one *active* mcpctl project
while preserving untagged servers (hand-configured sre, websearch, etc).
- the /mcpctl extension now reads that tag as the single source of truth for the
active project, fixing the false short-circuit / no-op switch.
config.ts command:
- an explicit -p now updates a differing up-tree .mcpctl-project marker (scope
no longer silently reverts on the next sync), no-ops when it matches, and
still never scopes $HOME.
- a project left with no usable credential now exits non-zero (the /mcpctl
extension checks child exit status, so it no longer reports a successful
switch after provisioning failed).
- skills sync is treated as best-effort: settings+auth determine switch success,
so a skills error no longer falsely fails the switch.
- token minting now revokes prior active `prime-agent` tokens before creating a
fresh one (no more never-expiring token litter / lost-credential duplication).
Tests (544 green): corrupt auth.json refusal, 0600 mode, mint-failure exit
code, single-active dedup preserving untagged sre, cross-project overwrite
preservation, and global-orphan removal on global-only sync.
2026-08-08 11:28:15 +01:00
mcpctlManaged : true ,
2026-08-08 09:34:15 +01:00
} ) ;
} ) ;
it ( 'writes a project marker for later skills sync' , async ( ) = > {
const settingsPath = join ( tmpDir , 'settings.json' ) ;
const cmd = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
await cmd . parseAsync ( [ 'prime-agent' , '--project' , 'sre' , '-o' , settingsPath , '--skip-skills' ] , { from : 'user' } ) ;
const markerPath = join ( tmpDir , '.mcpctl-project' ) ;
expect ( readFileSync ( markerPath , 'utf-8' ) . trim ( ) ) . toBe ( 'sre' ) ;
} ) ;
it ( '--dry-run prints the change without writing' , async ( ) = > {
const settingsPath = join ( tmpDir , 'settings.json' ) ;
const cmd = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
await cmd . parseAsync ( [ 'prime-agent' , '--project' , 'proj-2' , '-o' , settingsPath , '--dry-run' ] , { from : 'user' } ) ;
expect ( output . join ( '\n' ) ) . toContain ( 'proj-2' ) ;
// No file should have been created.
expect ( exceptionSafeRead ( settingsPath ) ) . toBeNull ( ) ;
} ) ;
fix(cli): harden `config prime-agent` sync + install /mcpctl switcher extension
Addresses a review of the `config prime-agent` feature and adds the in-app
project switcher.
Safety/correctness fixes (prime-agent's shared, hand-editable ~/.prime/agent
tree must never suffer silent data loss):
- config/prime-agent.ts: loadPrimeAgentSettings now fails loudly on corrupt
JSON instead of swallowing it and rewriting the file (which destroyed every
non-mcpServers setting). A project's mcpServers entry is merged (keeping
user-added fields) rather than replaced wholesale. Added writePrimeAgentAuth
/ hasPrimeAgentAuth helpers for auth provisioning.
- skills sync: unified the near-verbatim prime-agent copy into runSkillsSync
via a `target: 'claude' | 'prime-agent'` option (prime-agent-skills.ts is now
a thin wrapper). Under the prime-agent target it: preserves untracked
pre-existing skill dirs on first sync (no more rm -rf of hand-authored `sre`),
records per-project ownership so configuring a second project never deletes
the first project's skills, skips Claude-only hooks/postInstall, and keeps
the mcpServers auto-attach step.
- config.ts: `config prime-agent` now (a) provisions the bearer credential in
auth.json (--token, existing entry, or auto-mint via POST /api/v1/mcptokens),
(b) writes the .mcpctl-project marker only when none exists up-tree and never
from $HOME, and (c) propagates the skills sync exit code so auth failures are
reported instead of swallowing them.
- skills.ts: `--agent` is validated; an unknown value errors instead of
silently running the Claude sync.
New feature: `config prime-agent` installs a `/mcpctl` project-switcher
extension into ~/.prime/agent/extensions/ (skip with --skip-extension). It lists
mcpctl projects via `mcpctl get projects -o json`, lets you pick one from the
prime-agent TUI, applies the switch through the CLI, and reloads the session.
Regenerated shell completions. Tests: 538 pass (new coverage for settings
corruption, entry merge, auth provisioning, extension install/skip, marker
$HOME handling, untracked/cross-project skill preservation, --agent validation).
2026-08-08 10:22:46 +01:00
it ( 'does not call the API when --skip-skills and --token are given' , async ( ) = > {
2026-08-08 09:34:15 +01:00
const settingsPath = join ( tmpDir , 'settings.json' ) ;
const cmd = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
fix(cli): harden `config prime-agent` sync + install /mcpctl switcher extension
Addresses a review of the `config prime-agent` feature and adds the in-app
project switcher.
Safety/correctness fixes (prime-agent's shared, hand-editable ~/.prime/agent
tree must never suffer silent data loss):
- config/prime-agent.ts: loadPrimeAgentSettings now fails loudly on corrupt
JSON instead of swallowing it and rewriting the file (which destroyed every
non-mcpServers setting). A project's mcpServers entry is merged (keeping
user-added fields) rather than replaced wholesale. Added writePrimeAgentAuth
/ hasPrimeAgentAuth helpers for auth provisioning.
- skills sync: unified the near-verbatim prime-agent copy into runSkillsSync
via a `target: 'claude' | 'prime-agent'` option (prime-agent-skills.ts is now
a thin wrapper). Under the prime-agent target it: preserves untracked
pre-existing skill dirs on first sync (no more rm -rf of hand-authored `sre`),
records per-project ownership so configuring a second project never deletes
the first project's skills, skips Claude-only hooks/postInstall, and keeps
the mcpServers auto-attach step.
- config.ts: `config prime-agent` now (a) provisions the bearer credential in
auth.json (--token, existing entry, or auto-mint via POST /api/v1/mcptokens),
(b) writes the .mcpctl-project marker only when none exists up-tree and never
from $HOME, and (c) propagates the skills sync exit code so auth failures are
reported instead of swallowing them.
- skills.ts: `--agent` is validated; an unknown value errors instead of
silently running the Claude sync.
New feature: `config prime-agent` installs a `/mcpctl` project-switcher
extension into ~/.prime/agent/extensions/ (skip with --skip-extension). It lists
mcpctl projects via `mcpctl get projects -o json`, lets you pick one from the
prime-agent TUI, applies the switch through the CLI, and reloads the session.
Regenerated shell completions. Tests: 538 pass (new coverage for settings
corruption, entry merge, auth provisioning, extension install/skip, marker
$HOME handling, untracked/cross-project skill preservation, --agent validation).
2026-08-08 10:22:46 +01:00
await cmd . parseAsync ( [ 'prime-agent' , '--project' , 'proj-3' , '-o' , settingsPath , '--skip-skills' , '--token' , 'mcpctl_pat_test' ] , { from : 'user' } ) ;
2026-08-08 09:34:15 +01:00
expect ( client . get ) . not . toHaveBeenCalled ( ) ;
fix(cli): harden `config prime-agent` sync + install /mcpctl switcher extension
Addresses a review of the `config prime-agent` feature and adds the in-app
project switcher.
Safety/correctness fixes (prime-agent's shared, hand-editable ~/.prime/agent
tree must never suffer silent data loss):
- config/prime-agent.ts: loadPrimeAgentSettings now fails loudly on corrupt
JSON instead of swallowing it and rewriting the file (which destroyed every
non-mcpServers setting). A project's mcpServers entry is merged (keeping
user-added fields) rather than replaced wholesale. Added writePrimeAgentAuth
/ hasPrimeAgentAuth helpers for auth provisioning.
- skills sync: unified the near-verbatim prime-agent copy into runSkillsSync
via a `target: 'claude' | 'prime-agent'` option (prime-agent-skills.ts is now
a thin wrapper). Under the prime-agent target it: preserves untracked
pre-existing skill dirs on first sync (no more rm -rf of hand-authored `sre`),
records per-project ownership so configuring a second project never deletes
the first project's skills, skips Claude-only hooks/postInstall, and keeps
the mcpServers auto-attach step.
- config.ts: `config prime-agent` now (a) provisions the bearer credential in
auth.json (--token, existing entry, or auto-mint via POST /api/v1/mcptokens),
(b) writes the .mcpctl-project marker only when none exists up-tree and never
from $HOME, and (c) propagates the skills sync exit code so auth failures are
reported instead of swallowing them.
- skills.ts: `--agent` is validated; an unknown value errors instead of
silently running the Claude sync.
New feature: `config prime-agent` installs a `/mcpctl` project-switcher
extension into ~/.prime/agent/extensions/ (skip with --skip-extension). It lists
mcpctl projects via `mcpctl get projects -o json`, lets you pick one from the
prime-agent TUI, applies the switch through the CLI, and reloads the session.
Regenerated shell completions. Tests: 538 pass (new coverage for settings
corruption, entry merge, auth provisioning, extension install/skip, marker
$HOME handling, untracked/cross-project skill preservation, --agent validation).
2026-08-08 10:22:46 +01:00
expect ( client . post ) . not . toHaveBeenCalled ( ) ;
2026-08-08 09:34:15 +01:00
} ) ;
it ( 'backward compat: prime-agent-generate still works' , async ( ) = > {
const settingsPath = join ( tmpDir , 'settings.json' ) ;
const cmd = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
await cmd . parseAsync ( [ 'prime-agent-generate' , '--project' , 'proj-1' , '-o' , settingsPath , '--skip-skills' ] , { from : 'user' } ) ;
const written = JSON . parse ( readFileSync ( settingsPath , 'utf-8' ) ) ;
expect ( written . mcpServers [ 'proj-1' ] ) . toBeDefined ( ) ;
} ) ;
fix(cli): harden `config prime-agent` sync + install /mcpctl switcher extension
Addresses a review of the `config prime-agent` feature and adds the in-app
project switcher.
Safety/correctness fixes (prime-agent's shared, hand-editable ~/.prime/agent
tree must never suffer silent data loss):
- config/prime-agent.ts: loadPrimeAgentSettings now fails loudly on corrupt
JSON instead of swallowing it and rewriting the file (which destroyed every
non-mcpServers setting). A project's mcpServers entry is merged (keeping
user-added fields) rather than replaced wholesale. Added writePrimeAgentAuth
/ hasPrimeAgentAuth helpers for auth provisioning.
- skills sync: unified the near-verbatim prime-agent copy into runSkillsSync
via a `target: 'claude' | 'prime-agent'` option (prime-agent-skills.ts is now
a thin wrapper). Under the prime-agent target it: preserves untracked
pre-existing skill dirs on first sync (no more rm -rf of hand-authored `sre`),
records per-project ownership so configuring a second project never deletes
the first project's skills, skips Claude-only hooks/postInstall, and keeps
the mcpServers auto-attach step.
- config.ts: `config prime-agent` now (a) provisions the bearer credential in
auth.json (--token, existing entry, or auto-mint via POST /api/v1/mcptokens),
(b) writes the .mcpctl-project marker only when none exists up-tree and never
from $HOME, and (c) propagates the skills sync exit code so auth failures are
reported instead of swallowing them.
- skills.ts: `--agent` is validated; an unknown value errors instead of
silently running the Claude sync.
New feature: `config prime-agent` installs a `/mcpctl` project-switcher
extension into ~/.prime/agent/extensions/ (skip with --skip-extension). It lists
mcpctl projects via `mcpctl get projects -o json`, lets you pick one from the
prime-agent TUI, applies the switch through the CLI, and reloads the session.
Regenerated shell completions. Tests: 538 pass (new coverage for settings
corruption, entry merge, auth provisioning, extension install/skip, marker
$HOME handling, untracked/cross-project skill preservation, --agent validation).
2026-08-08 10:22:46 +01:00
it ( 'provisions auth.json by minting a project token' , async ( ) = > {
const settingsPath = join ( tmpDir , 'settings.json' ) ;
const cmd = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
await cmd . parseAsync ( [ 'prime-agent' , '--project' , 'labctl' , '-o' , settingsPath , '--skip-skills' , '--skip-extension' ] , { from : 'user' } ) ;
expect ( client . post ) . toHaveBeenCalledWith ( '/api/v1/mcptokens' , expect . objectContaining ( { projectName : 'labctl' } ) ) ;
const auth = JSON . parse ( readFileSync ( join ( tmpDir , 'auth.json' ) , 'utf-8' ) ) ;
expect ( auth [ 'mcp:labctl' ] ) . toEqual ( { type : 'api_key' , key : 'impersonated-tok' } ) ;
} ) ;
it ( 'uses --token without calling the API' , async ( ) = > {
const settingsPath = join ( tmpDir , 'settings.json' ) ;
const cmd = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
await cmd . parseAsync ( [ 'prime-agent' , '--project' , 'docmost' , '-o' , settingsPath , '--skip-skills' , '--skip-extension' , '--token' , 'mcpctl_pat_custom' ] , { from : 'user' } ) ;
expect ( client . post ) . not . toHaveBeenCalled ( ) ;
const auth = JSON . parse ( readFileSync ( join ( tmpDir , 'auth.json' ) , 'utf-8' ) ) ;
expect ( auth [ 'mcp:docmost' ] ) . toEqual ( { type : 'api_key' , key : 'mcpctl_pat_custom' } ) ;
} ) ;
it ( 'keeps an existing credential and does not re-mint' , async ( ) = > {
const settingsPath = join ( tmpDir , 'settings.json' ) ;
writeFileSync ( join ( tmpDir , 'auth.json' ) , JSON . stringify ( { 'mcp:labctl' : { type : 'api_key' , key : 'existing' } } ) ) ;
const cmd = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
await cmd . parseAsync ( [ 'prime-agent' , '--project' , 'labctl' , '-o' , settingsPath , '--skip-skills' , '--skip-extension' ] , { from : 'user' } ) ;
expect ( client . post ) . not . toHaveBeenCalled ( ) ;
const auth = JSON . parse ( readFileSync ( join ( tmpDir , 'auth.json' ) , 'utf-8' ) ) ;
expect ( auth [ 'mcp:labctl' ] . key ) . toBe ( 'existing' ) ;
} ) ;
it ( 'installs the /mcpctl switcher extension by default, and skips with --skip-extension' , async ( ) = > {
const settingsPath = join ( tmpDir , 'settings.json' ) ;
const cmd = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
await cmd . parseAsync ( [ 'prime-agent' , '--project' , 'ha' , '-o' , settingsPath , '--skip-skills' , '--token' , 'mcpctl_pat_x' ] , { from : 'user' } ) ;
const extPath = join ( tmpDir , 'extensions' , 'mcpctl-switch.ts' ) ;
expect ( existsSync ( extPath ) ) . toBe ( true ) ;
expect ( readFileSync ( extPath , 'utf-8' ) ) . toContain ( "registerCommand('mcpctl'" ) ;
output . length = 0 ;
const cmd2 = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
await cmd2 . parseAsync ( [ 'prime-agent' , '--project' , 'ha' , '-o' , settingsPath , '--skip-skills' , '--skip-extension' , '--token' , 'mcpctl_pat_x' ] , { from : 'user' } ) ;
expect ( output . join ( '\n' ) ) . not . toContain ( 'switcher extension' ) ;
} ) ;
it ( 'does not write a .mcpctl-project marker when run from $HOME' , async ( ) = > {
const settingsPath = join ( tmpDir , 'settings.json' ) ;
const prevCwd = process . cwd ( ) ;
process . chdir ( homedir ( ) ) ;
const cmd = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
try {
await cmd . parseAsync ( [ 'prime-agent' , '--project' , 'proj-x' , '-o' , settingsPath , '--skip-skills' , '--skip-extension' , '--token' , 'mcpctl_pat_x' ] , { from : 'user' } ) ;
} finally {
process . chdir ( prevCwd ) ;
}
expect ( output . join ( '\n' ) ) . toContain ( 'Skipped .mcpctl-project marker' ) ;
expect ( exceptionSafeRead ( join ( homedir ( ) , '.mcpctl-project' ) ) ) . toBeNull ( ) ;
} ) ;
fix(cli): close second review on prime-agent sync + switcher (auth, ownership, switching)
Addresses the second round of `config prime-agent` review (10 findings).
auth.json (config/prime-agent.ts) — the settings.json data-loss fix had a twin:
- loadPrimeAgentAuth now fails loudly on corrupt JSON instead of swallow-and-
rewrite, so one syntax error can no longer destroy the provider API key and
every other project's credential. hasPrimeAgentAuth shares that guarantee.
- writePrimeAgentAuth writes 0600 (preserving an existing file's mode) instead
of the default umask — bearer tokens are no longer world-readable on first
creation.
state ownership (commands/skills.ts) — the ownership model edge cases:
- orphan-removal guard now normalises a canonical scope (project name, or null
for globals; legacy undefined adopted to current scope) instead of comparing
null against undefined, so global-only syncs and pre-PR state can no longer
leave stale skills on disk forever.
- a same-named skill *tracked* to a different project is preserved (with a
warning) rather than silently overwritten in the shared flat tree.
- mcpServers auto-attach is gated behind !isPrimeAgent with an explicit warning
(prime-agent's HTTP gateway must not mutate shared mcpd project attachments);
this also makes the earlier dropped-attach concern explicit rather than silent.
single-active project + switcher (config/prime-agent.ts, prime-agent-extension.ts):
- registerPrimeAgentMcp tags the project's entry mcpctlManaged:true and removes
other mcpctl-managed entries, so prime-agent has one *active* mcpctl project
while preserving untagged servers (hand-configured sre, websearch, etc).
- the /mcpctl extension now reads that tag as the single source of truth for the
active project, fixing the false short-circuit / no-op switch.
config.ts command:
- an explicit -p now updates a differing up-tree .mcpctl-project marker (scope
no longer silently reverts on the next sync), no-ops when it matches, and
still never scopes $HOME.
- a project left with no usable credential now exits non-zero (the /mcpctl
extension checks child exit status, so it no longer reports a successful
switch after provisioning failed).
- skills sync is treated as best-effort: settings+auth determine switch success,
so a skills error no longer falsely fails the switch.
- token minting now revokes prior active `prime-agent` tokens before creating a
fresh one (no more never-expiring token litter / lost-credential duplication).
Tests (544 green): corrupt auth.json refusal, 0600 mode, mint-failure exit
code, single-active dedup preserving untagged sre, cross-project overwrite
preservation, and global-orphan removal on global-only sync.
2026-08-08 11:28:15 +01:00
it ( 'refuses to overwrite a corrupt auth.json (and does not mint over it)' , async ( ) = > {
const settingsPath = join ( tmpDir , 'settings.json' ) ;
writeFileSync ( join ( tmpDir , 'auth.json' ) , '{ not valid json' ) ;
const cmd = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
await cmd . parseAsync ( [ 'prime-agent' , '--project' , 'x' , '-o' , settingsPath , '--skip-skills' , '--skip-extension' ] , { from : 'user' } ) ;
expect ( output . join ( '\n' ) ) . toContain ( 'refusing to overwrite' ) ;
expect ( readFileSync ( join ( tmpDir , 'auth.json' ) , 'utf-8' ) ) . toBe ( '{ not valid json' ) ;
expect ( process . exitCode ) . toBe ( 1 ) ;
} ) ;
it ( 'exits non-zero when a credential cannot be provisioned' , async ( ) = > {
// mockClient post returns { token: ... } by default; override to no token.
const badClient = { . . . client , post : vi.fn ( async ( ) = > ( { } ) ) } as typeof client ;
const settingsPath = join ( tmpDir , 'settings.json' ) ;
const cmd = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client : badClient , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
await cmd . parseAsync ( [ 'prime-agent' , '--project' , 'x' , '-o' , settingsPath , '--skip-skills' , '--skip-extension' ] , { from : 'user' } ) ;
expect ( process . exitCode ) . toBe ( 1 ) ;
// body of provisioning error surfaced
expect ( output . join ( '\n' ) ) . toContain ( 'no token returned' ) ;
} ) ;
it ( 'writes auth.json with mode 0600' , async ( ) = > {
const settingsPath = join ( tmpDir , 'settings.json' ) ;
const cmd = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
await cmd . parseAsync ( [ 'prime-agent' , '--project' , 'm' , '-o' , settingsPath , '--skip-skills' , '--skip-extension' ] , { from : 'user' } ) ; // mint path, mock post returns token
const mode = statSync ( join ( tmpDir , 'auth.json' ) ) . mode & 0 o777 ;
expect ( mode ) . toBe ( 0 o600 ) ;
} ) ;
fix(cli): harden `config prime-agent` sync + install /mcpctl switcher extension
Addresses a review of the `config prime-agent` feature and adds the in-app
project switcher.
Safety/correctness fixes (prime-agent's shared, hand-editable ~/.prime/agent
tree must never suffer silent data loss):
- config/prime-agent.ts: loadPrimeAgentSettings now fails loudly on corrupt
JSON instead of swallowing it and rewriting the file (which destroyed every
non-mcpServers setting). A project's mcpServers entry is merged (keeping
user-added fields) rather than replaced wholesale. Added writePrimeAgentAuth
/ hasPrimeAgentAuth helpers for auth provisioning.
- skills sync: unified the near-verbatim prime-agent copy into runSkillsSync
via a `target: 'claude' | 'prime-agent'` option (prime-agent-skills.ts is now
a thin wrapper). Under the prime-agent target it: preserves untracked
pre-existing skill dirs on first sync (no more rm -rf of hand-authored `sre`),
records per-project ownership so configuring a second project never deletes
the first project's skills, skips Claude-only hooks/postInstall, and keeps
the mcpServers auto-attach step.
- config.ts: `config prime-agent` now (a) provisions the bearer credential in
auth.json (--token, existing entry, or auto-mint via POST /api/v1/mcptokens),
(b) writes the .mcpctl-project marker only when none exists up-tree and never
from $HOME, and (c) propagates the skills sync exit code so auth failures are
reported instead of swallowing them.
- skills.ts: `--agent` is validated; an unknown value errors instead of
silently running the Claude sync.
New feature: `config prime-agent` installs a `/mcpctl` project-switcher
extension into ~/.prime/agent/extensions/ (skip with --skip-extension). It lists
mcpctl projects via `mcpctl get projects -o json`, lets you pick one from the
prime-agent TUI, applies the switch through the CLI, and reloads the session.
Regenerated shell completions. Tests: 538 pass (new coverage for settings
corruption, entry merge, auth provisioning, extension install/skip, marker
$HOME handling, untracked/cross-project skill preservation, --agent validation).
2026-08-08 10:22:46 +01:00
it ( 'refuses to overwrite a corrupt settings.json' , async ( ) = > {
const settingsPath = join ( tmpDir , 'settings.json' ) ;
writeFileSync ( settingsPath , '{ this is not valid json !!!' ) ;
const prevCwd = process . cwd ( ) ;
process . chdir ( tmpDir ) ;
const cmd = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
try {
await cmd . parseAsync ( [ 'prime-agent' , '--project' , 'proj-9' , '-o' , settingsPath , '--skip-skills' , '--skip-extension' , '--token' , 'mcpctl_pat_x' ] , { from : 'user' } ) ;
} finally {
process . chdir ( prevCwd ) ;
}
expect ( output . join ( '\n' ) ) . toContain ( 'refusing to overwrite' ) ;
// The corrupt file is untouched.
expect ( readFileSync ( settingsPath , 'utf-8' ) ) . toBe ( '{ this is not valid json !!!' ) ;
} ) ;
fix(cli): close second review on prime-agent sync + switcher (auth, ownership, switching)
Addresses the second round of `config prime-agent` review (10 findings).
auth.json (config/prime-agent.ts) — the settings.json data-loss fix had a twin:
- loadPrimeAgentAuth now fails loudly on corrupt JSON instead of swallow-and-
rewrite, so one syntax error can no longer destroy the provider API key and
every other project's credential. hasPrimeAgentAuth shares that guarantee.
- writePrimeAgentAuth writes 0600 (preserving an existing file's mode) instead
of the default umask — bearer tokens are no longer world-readable on first
creation.
state ownership (commands/skills.ts) — the ownership model edge cases:
- orphan-removal guard now normalises a canonical scope (project name, or null
for globals; legacy undefined adopted to current scope) instead of comparing
null against undefined, so global-only syncs and pre-PR state can no longer
leave stale skills on disk forever.
- a same-named skill *tracked* to a different project is preserved (with a
warning) rather than silently overwritten in the shared flat tree.
- mcpServers auto-attach is gated behind !isPrimeAgent with an explicit warning
(prime-agent's HTTP gateway must not mutate shared mcpd project attachments);
this also makes the earlier dropped-attach concern explicit rather than silent.
single-active project + switcher (config/prime-agent.ts, prime-agent-extension.ts):
- registerPrimeAgentMcp tags the project's entry mcpctlManaged:true and removes
other mcpctl-managed entries, so prime-agent has one *active* mcpctl project
while preserving untagged servers (hand-configured sre, websearch, etc).
- the /mcpctl extension now reads that tag as the single source of truth for the
active project, fixing the false short-circuit / no-op switch.
config.ts command:
- an explicit -p now updates a differing up-tree .mcpctl-project marker (scope
no longer silently reverts on the next sync), no-ops when it matches, and
still never scopes $HOME.
- a project left with no usable credential now exits non-zero (the /mcpctl
extension checks child exit status, so it no longer reports a successful
switch after provisioning failed).
- skills sync is treated as best-effort: settings+auth determine switch success,
so a skills error no longer falsely fails the switch.
- token minting now revokes prior active `prime-agent` tokens before creating a
fresh one (no more never-expiring token litter / lost-credential duplication).
Tests (544 green): corrupt auth.json refusal, 0600 mode, mint-failure exit
code, single-active dedup preserving untagged sre, cross-project overwrite
preservation, and global-orphan removal on global-only sync.
2026-08-08 11:28:15 +01:00
it ( 'keeps a single active mcpctl project, preserving untagged servers (sre)' , async ( ) = > {
const settingsPath = join ( tmpDir , 'settings.json' ) ;
writeFileSync ( settingsPath , JSON . stringify ( {
mcpServers : {
sre : { type : 'http' , url : ` ${ DEFAULT_MCPCTL_GATEWAY_URL } /projects/sre/mcp ` } , // untagged, hand-set
homeautomation : { type : 'http' , url : ` ${ DEFAULT_MCPCTL_GATEWAY_URL } /projects/homeautomation/mcp ` , mcpctlManaged : true } ,
} ,
} ) ) ;
// Active project is homeautomation (tagged). Switch to labctl.
const cmd = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
await cmd . parseAsync ( [ 'prime-agent' , '--project' , 'labctl' , '-o' , settingsPath , '--skip-skills' , '--skip-extension' , '--token' , 'mcpctl_pat_x' ] , { from : 'user' } ) ;
const written = JSON . parse ( readFileSync ( settingsPath , 'utf-8' ) ) ;
expect ( written . mcpServers [ 'labctl' ] . mcpctlManaged ) . toBe ( true ) ; // new active
expect ( written . mcpServers [ 'homeautomation' ] ) . toBeUndefined ( ) ; // old managed removed
expect ( written . mcpServers [ 'sre' ] ) . toBeDefined ( ) ; // untagged preserved
} ) ;
fix(cli): harden `config prime-agent` sync + install /mcpctl switcher extension
Addresses a review of the `config prime-agent` feature and adds the in-app
project switcher.
Safety/correctness fixes (prime-agent's shared, hand-editable ~/.prime/agent
tree must never suffer silent data loss):
- config/prime-agent.ts: loadPrimeAgentSettings now fails loudly on corrupt
JSON instead of swallowing it and rewriting the file (which destroyed every
non-mcpServers setting). A project's mcpServers entry is merged (keeping
user-added fields) rather than replaced wholesale. Added writePrimeAgentAuth
/ hasPrimeAgentAuth helpers for auth provisioning.
- skills sync: unified the near-verbatim prime-agent copy into runSkillsSync
via a `target: 'claude' | 'prime-agent'` option (prime-agent-skills.ts is now
a thin wrapper). Under the prime-agent target it: preserves untracked
pre-existing skill dirs on first sync (no more rm -rf of hand-authored `sre`),
records per-project ownership so configuring a second project never deletes
the first project's skills, skips Claude-only hooks/postInstall, and keeps
the mcpServers auto-attach step.
- config.ts: `config prime-agent` now (a) provisions the bearer credential in
auth.json (--token, existing entry, or auto-mint via POST /api/v1/mcptokens),
(b) writes the .mcpctl-project marker only when none exists up-tree and never
from $HOME, and (c) propagates the skills sync exit code so auth failures are
reported instead of swallowing them.
- skills.ts: `--agent` is validated; an unknown value errors instead of
silently running the Claude sync.
New feature: `config prime-agent` installs a `/mcpctl` project-switcher
extension into ~/.prime/agent/extensions/ (skip with --skip-extension). It lists
mcpctl projects via `mcpctl get projects -o json`, lets you pick one from the
prime-agent TUI, applies the switch through the CLI, and reloads the session.
Regenerated shell completions. Tests: 538 pass (new coverage for settings
corruption, entry merge, auth provisioning, extension install/skip, marker
$HOME handling, untracked/cross-project skill preservation, --agent validation).
2026-08-08 10:22:46 +01:00
it ( 'merges a re-configured project entry, preserving user-added fields' , async ( ) = > {
const settingsPath = join ( tmpDir , 'settings.json' ) ;
writeFileSync ( settingsPath , JSON . stringify ( {
mcpServers : {
ha : { type : 'http' , url : 'https://old/projects/ha/mcp' , headers : { Authorization : 'Bearer u' } } ,
} ,
} ) ) ;
const cmd = createConfigCommand (
{ configDeps : { configDir : tmpDir } , log } ,
{ client , credentialsDeps : { configDir : tmpDir } , log } ,
) ;
await cmd . parseAsync ( [ 'prime-agent' , '--project' , 'ha' , '-o' , settingsPath , '--skip-skills' , '--skip-extension' , '--token' , 'mcpctl_pat_x' ] , { from : 'user' } ) ;
const written = JSON . parse ( readFileSync ( settingsPath , 'utf-8' ) ) ;
expect ( written . mcpServers [ 'ha' ] ) . toEqual ( {
type : 'http' ,
url : ` ${ DEFAULT_MCPCTL_GATEWAY_URL } /projects/ha/mcp ` ,
headers : { Authorization : 'Bearer u' } , // user-added field preserved
fix(cli): close second review on prime-agent sync + switcher (auth, ownership, switching)
Addresses the second round of `config prime-agent` review (10 findings).
auth.json (config/prime-agent.ts) — the settings.json data-loss fix had a twin:
- loadPrimeAgentAuth now fails loudly on corrupt JSON instead of swallow-and-
rewrite, so one syntax error can no longer destroy the provider API key and
every other project's credential. hasPrimeAgentAuth shares that guarantee.
- writePrimeAgentAuth writes 0600 (preserving an existing file's mode) instead
of the default umask — bearer tokens are no longer world-readable on first
creation.
state ownership (commands/skills.ts) — the ownership model edge cases:
- orphan-removal guard now normalises a canonical scope (project name, or null
for globals; legacy undefined adopted to current scope) instead of comparing
null against undefined, so global-only syncs and pre-PR state can no longer
leave stale skills on disk forever.
- a same-named skill *tracked* to a different project is preserved (with a
warning) rather than silently overwritten in the shared flat tree.
- mcpServers auto-attach is gated behind !isPrimeAgent with an explicit warning
(prime-agent's HTTP gateway must not mutate shared mcpd project attachments);
this also makes the earlier dropped-attach concern explicit rather than silent.
single-active project + switcher (config/prime-agent.ts, prime-agent-extension.ts):
- registerPrimeAgentMcp tags the project's entry mcpctlManaged:true and removes
other mcpctl-managed entries, so prime-agent has one *active* mcpctl project
while preserving untagged servers (hand-configured sre, websearch, etc).
- the /mcpctl extension now reads that tag as the single source of truth for the
active project, fixing the false short-circuit / no-op switch.
config.ts command:
- an explicit -p now updates a differing up-tree .mcpctl-project marker (scope
no longer silently reverts on the next sync), no-ops when it matches, and
still never scopes $HOME.
- a project left with no usable credential now exits non-zero (the /mcpctl
extension checks child exit status, so it no longer reports a successful
switch after provisioning failed).
- skills sync is treated as best-effort: settings+auth determine switch success,
so a skills error no longer falsely fails the switch.
- token minting now revokes prior active `prime-agent` tokens before creating a
fresh one (no more never-expiring token litter / lost-credential duplication).
Tests (544 green): corrupt auth.json refusal, 0600 mode, mint-failure exit
code, single-active dedup preserving untagged sre, cross-project overwrite
preservation, and global-orphan removal on global-only sync.
2026-08-08 11:28:15 +01:00
mcpctlManaged : true ,
fix(cli): harden `config prime-agent` sync + install /mcpctl switcher extension
Addresses a review of the `config prime-agent` feature and adds the in-app
project switcher.
Safety/correctness fixes (prime-agent's shared, hand-editable ~/.prime/agent
tree must never suffer silent data loss):
- config/prime-agent.ts: loadPrimeAgentSettings now fails loudly on corrupt
JSON instead of swallowing it and rewriting the file (which destroyed every
non-mcpServers setting). A project's mcpServers entry is merged (keeping
user-added fields) rather than replaced wholesale. Added writePrimeAgentAuth
/ hasPrimeAgentAuth helpers for auth provisioning.
- skills sync: unified the near-verbatim prime-agent copy into runSkillsSync
via a `target: 'claude' | 'prime-agent'` option (prime-agent-skills.ts is now
a thin wrapper). Under the prime-agent target it: preserves untracked
pre-existing skill dirs on first sync (no more rm -rf of hand-authored `sre`),
records per-project ownership so configuring a second project never deletes
the first project's skills, skips Claude-only hooks/postInstall, and keeps
the mcpServers auto-attach step.
- config.ts: `config prime-agent` now (a) provisions the bearer credential in
auth.json (--token, existing entry, or auto-mint via POST /api/v1/mcptokens),
(b) writes the .mcpctl-project marker only when none exists up-tree and never
from $HOME, and (c) propagates the skills sync exit code so auth failures are
reported instead of swallowing them.
- skills.ts: `--agent` is validated; an unknown value errors instead of
silently running the Claude sync.
New feature: `config prime-agent` installs a `/mcpctl` project-switcher
extension into ~/.prime/agent/extensions/ (skip with --skip-extension). It lists
mcpctl projects via `mcpctl get projects -o json`, lets you pick one from the
prime-agent TUI, applies the switch through the CLI, and reloads the session.
Regenerated shell completions. Tests: 538 pass (new coverage for settings
corruption, entry merge, auth provisioning, extension install/skip, marker
$HOME handling, untracked/cross-project skill preservation, --agent validation).
2026-08-08 10:22:46 +01:00
} ) ;
} ) ;
2026-08-08 09:34:15 +01:00
} ) ;
function exceptionSafeRead ( path : string ) : string | null {
try {
return readFileSync ( path , 'utf-8' ) ;
} catch {
return null ;
}
}