3fa41e4d46647295c9ca90a94187e4ad93ffa471
12 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8b006263de |
fix(skills): write the frontmatter every Agent Skills host requires
Some checks failed
CI/CD / lint (pull_request) Successful in 1m18s
CI/CD / typecheck (pull_request) Successful in 1m15s
CI/CD / test (pull_request) Successful in 1m27s
CI/CD / build (pull_request) Successful in 2m17s
CI/CD / smoke (pull_request) Failing after 3m8s
CI/CD / publish (pull_request) Has been skipped
mcpd keeps `name` and `description` as columns, and a skill's `content` is
often just the body — `propose-learnings` starts straight at its `#` heading.
The sync wrote `full.content` verbatim, so the SKILL.md landed with no YAML
frontmatter and the host rejected it. Observed live:
[Skill warning]
~/.prime/agent/skills/propose-learnings/SKILL.md
description is required
Claude Code and pi require the same keys, so this affected every target; it
only surfaced now because prime-agent prints the warning at startup.
`ensureSkillFrontmatter` synthesises the block from the columns when it is
absent, and fills in only a missing `name`/`description` when the author
already supplied a header — an existing complete header is never rewritten.
Values are emitted as JSON strings (valid YAML double-quoted scalars) because
descriptions routinely contain `:` and `#`, which break a bare scalar.
Also: `--force` now re-fetches skills whose server content is unchanged.
Without it a skill already on disk could never be repaired by a client-side
fix — the content hash still matched, so it was skipped forever, which is
exactly what happened on the first attempt to repair the file above.
Three existing assertions compared SKILL.md to the raw server content; they
now assert the body survives rather than exact bytes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB
|
||
|
|
f1d84b0952 |
fix(config pi): embed extension sources so installed binary is self-contained
- add scripts/generate-pi-extension.ts to embed src/pi-ext sources as string constants (mirrors prime-agent's embedded switcher pattern). - config pi now writes the embedded sources by default, so it works from an /usr/bin install with no source tree (--extension-dir overrides for dev). Verified installed binary writes files byte-identical to source. - add installEmbeddedExtension test. |
||
|
|
f90e23d000 |
Merge remote-tracking branch 'origin/main' into feat/pi-extension
# Conflicts: # README.md # src/cli/src/commands/config.ts # src/cli/src/commands/skills.ts |
||
|
|
28f1a411fd |
feat(pi): add native pi integration — extension, config pi, skills --agent pi
- src/pi-ext/: self-contained pi extension (mcpctl-pi.ts + vendored mcp-http client) that talks JSON-RPC directly to mcplocal and registers project MCP tools as native pi tools. No MCP client, no ~/.claude. - Persistent per-project session so gated projects ungate on begin_session. - /mcpctl command: status, switch project (GUI), refresh tools, sync skills. - mcpctl config pi: installs extension, wires pi settings, persists active project, syncs skills into ~/.pi/agent/skills. - skills sync: add --agent pi (target install root). - docs + tests. |
||
|
|
170dc06496 |
fix(cli): close third review — token collision, migration, ownership
Some checks failed
CI/CD / typecheck (pull_request) Successful in 1m5s
CI/CD / lint (pull_request) Successful in 2m13s
CI/CD / test (pull_request) Successful in 1m20s
CI/CD / build (pull_request) Successful in 2m9s
CI/CD / smoke (pull_request) Failing after 2m44s
CI/CD / publish (pull_request) Has been skipped
Round 2 fixed the first review but introduced regressions of its own, all of which only bite against state written by the previously installed build. `config prime-agent`: - Mint each credential under a unique `prime-agent-<stamp>` name again. `McpToken` is unique on (name, projectId) and revoke is a soft delete, so round 2's fixed `prime-agent` name could only ever be minted once per project — and the revoke-first ordering destroyed the working credential before discovering the mint would fail. - Provision the credential BEFORE touching settings.json. Registering the new project unmounts the previously active one, so a failed mint must not be able to leave prime-agent with no working project at all. The command now aborts with settings.json untouched. - Retire only the token this auth.json actually held, once its replacement is stored. Sweeping every `prime-agent*` token for the project would revoke the credential another install (or a custom --output run) is using; anything else that looks orphaned is reported, not deleted. - Validate a pre-existing credential instead of trusting its presence: a revoked or expired token used to short-circuit provisioning and leave prime-agent broken while the command reported success. Matched by tokenPrefix against the project's active tokens, so the secret is never sent. Fails open when the API can't be consulted. - Actually write auth.json 0600. `writeFile`'s mode is ignored for an existing file and prime-agent creates auth.json itself at 0644, so chmod after writing. - Recognise the untagged mcpServers entries older CLIs wrote (canonical proxy URL + an `mcp:<name>` mcpctl PAT in auth.json) so a switch unmounts them instead of leaving two gateways live. Hand-configured servers have no such credential and are still preserved. Same rule in the `/mcpctl` switcher's active-project lookup. - Add `--skip-marker`, and pass it from the `/mcpctl` switcher: the extension runs from whatever directory prime-agent was started in, and was silently re-scoping that repo's `.mcpctl-project`. `skills sync --agent prime-agent`: - Record ownership from the skill's own scope, not the syncing project's. Globals were being pinned to whichever project happened to sync them, after which every other project refused to update them forever. - Never adopt legacy, ownership-less state into the current scope. Round 2 did, which deleted the other project's skills on the first sync after upgrading. Such entries are attributed to the project that last wrote the state file, and left alone when that isn't the project syncing now. - Close the overwrite-guard bypass: a sync with no project, or a global landing on a project-owned name, could still clobber and re-own a tracked skill. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017BMXdb2qZbPSh8Q7XpTyjB |
||
|
|
fa7055ac5e |
fix(cli): close second review on prime-agent sync + switcher (auth, ownership, switching)
Some checks failed
CI/CD / lint (pull_request) Successful in 1m4s
CI/CD / typecheck (pull_request) Successful in 1m5s
CI/CD / test (pull_request) Successful in 3m7s
CI/CD / smoke (pull_request) Failing after 3m13s
CI/CD / build (pull_request) Successful in 2m12s
CI/CD / publish (pull_request) Has been skipped
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. |
||
|
|
eb1642ab1a |
fix(cli): harden config prime-agent sync + install /mcpctl switcher extension
Some checks failed
CI/CD / lint (pull_request) Successful in 1m4s
CI/CD / typecheck (pull_request) Successful in 1m6s
CI/CD / test (pull_request) Successful in 3m9s
CI/CD / build (pull_request) Successful in 2m16s
CI/CD / smoke (pull_request) Failing after 3m27s
CI/CD / publish (pull_request) Has been skipped
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). |
||
|
|
582f6f185b |
feat(cli): add mcpctl config prime-agent — proxy MCP + skills sync for prime-agent
Some checks failed
CI/CD / lint (pull_request) Successful in 1m6s
CI/CD / typecheck (pull_request) Successful in 2m10s
CI/CD / test (pull_request) Successful in 1m19s
CI/CD / smoke (pull_request) Failing after 1m53s
CI/CD / build (pull_request) Successful in 4m14s
CI/CD / publish (pull_request) Has been skipped
Mirror `mcpctl config claude` for prime-agent (which talks to the same mcpctl
proxy MCP gateway over HTTP instead of stdio):
`mcpctl config prime-agent --project X`:
- registers the proxy MCP gateway in ~/.prime/agent/settings.json as
mcpServers.X = { type: "http", url: <gateway>/projects/X/mcp }, merging with
any existing servers (e.g. the bundled `sre` project) and preserving all other
settings
- writes a .mcpctl-project marker so later syncs resolve the project
- syncs the project's skills into ~/.prime/agent/skills/<name>/ as markdown
skills (prime-agent auto-discovers them at session start)
New `mcpctl skills sync --agent prime-agent` target re-syncs the tree later.
- src/cli/src/config/prime-agent.ts: settings.json read/merge/write helpers
- src/cli/src/utils/prime-agent-skills.ts: prime-agent sync (reuses
installSkillAtomic + skills-state; skips Claude-only hooks/postInstall)
- completes config.ts/skills.ts wiring; regenerated shell completions
- tests: commands/prime-agent.test.ts + utils/prime-agent-skills.test.ts
|
||
|
|
180e50a978 |
feat(cli): metadata.mcpServers auto-attach in mcpctl skills sync
Closes the third deferred item from PR-5: skills can declare upstream
MCP server dependencies via `metadata.mcpServers` and `mcpctl skills
sync` now attaches them to the active project. Same trust model as
postInstall/hooks: the publisher is responsible, the client just
asks mcpd to attach.
## Behaviour
- For each `{ name, fromTemplate?, project? }` entry:
- If the project already has the server attached → record as
`alreadyAttached`, no-op.
- If the server doesn't exist on mcpd → warn + skip (we don't
auto-create from template; that's a separate explicit decision
for the operator). The warning surfaces the suggested template
so the operator can decide.
- Otherwise → POST `/api/v1/projects/:id/servers { server: <name> }`.
- 409 from POST → treat as alreadyAttached (server-side idempotency).
- 404 from POST → treat as missing (race: server vanished mid-sync).
- Other errors collected per-server; sync continues.
- A dep with `project:` set to a non-active project is skipped during
this sync — keeps the active sync from making collateral changes
to other projects.
- Global skill syncs (no project context) skip mcpServers entirely
with a warning — there's no project to attach to.
## Files
### Added
- src/cli/src/utils/mcpservers-materialiser.ts (~140 LOC)
- src/cli/tests/utils/mcpservers-materialiser.test.ts (~190 LOC,
10 tests covering: parse-tolerance, fresh attach, alreadyAttached
short-circuit, missing-server warn+skip, missing-project errors-
out, 409→alreadyAttached, 404→missing, cross-project skip,
per-server error collection, empty-deps no-op)
### Edited
- src/cli/src/commands/skills.ts: applyOne calls
attachSkillMcpServers after hooks. Tracks per-skill attachments in
result.mcpServersAttached. Summary line surfaces "N mcpServers
attached".
## Verification
165 test files / 2193 tests green (up from 2182).
Real-world flow:
```yaml
# skill metadata.yaml
mcpServers:
- name: my-grafana
fromTemplate: grafana
project: monitoring
- name: my-loki
fromTemplate: loki
```
```bash
# As operator: ensure the named servers exist on mcpd first
mcpctl create server my-grafana --from-template grafana --env-from-secret grafana-creds
mcpctl create server my-loki --from-template loki
# Now publish the skill that declares them as deps. Sync will attach:
mcpctl skills sync
# → mcpctl: 1 installed, 2 mcpServers attached
mcpctl describe project monitoring # both servers now attached
```
## What's intentionally NOT in this PR
- Auto-creating servers from `fromTemplate` when they don't exist.
Provisioning infra from a skill push is a separate decision needing
explicit operator policy. v1 warns + skips; the warning includes
the suggested template name so the operator can act manually.
- Detaching a server when a skill drops it from mcpServers. Detach is
destructive (project loses access) and we can't tell whether the
detach is intentional vs. accidental drop. PR-7 can revisit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
7ebc8b22d1 |
feat(cli): metadata.hooks materialisation in mcpctl skills sync
Closes the second-biggest deferred item from PR-5: skills can declare
PreToolUse / PostToolUse / SessionStart / Stop / SubagentStop /
Notification hooks in metadata.hooks, and `mcpctl skills sync` now
registers them in ~/.claude/settings.json with proper source-tagging.
## Why this needs source-tagging
mcpctl already managed ONE entry in settings.json (the SessionStart
hook for `mcpctl skills sync --quiet`, set up by PR-5 via
`installManagedSessionHook`). That entry uses `_mcpctl_managed: true`
to identify itself for idempotent updates.
Skill-declared hooks need finer scoping: skill A and skill B could
both register PreToolUse hooks, and removing skill A must not touch
skill B's. So entries written by `applyManagedHooks` carry both:
_mcpctl_managed: true ← same flag the SessionStart hook uses
_mcpctl_source: "<skill-name>" ← differentiates per-skill
Removing a skill (orphan cleanup) drops every entry tagged with
that skill's name. User-added entries (no marker) are preserved
verbatim. The earlier session-hook installer keeps working: its
`_mcpctl_managed: true` lacks the source tag, so the hooks-materialiser
ignores it on per-skill operations.
## Behaviour
- Skill installs/updates: applyManagedHooks(skillName, metadata.hooks)
is called after files are atomically materialised. The function
reads settings.json, drops this skill's previous entries from every
declared event (and from any event it previously had entries in but
no longer declares — so a skill can shrink scope), then re-inserts
the new tagged set.
- Skill orphan removal: removeManagedHooks(skillName) drops every
entry owned by the skill. Other skills + user hooks unaffected.
- Failure handling: hooks errors are logged via the warn() callback
but do not fail the sync. Same shape as postInstall — scoped, not
fatal.
## Files
### Added
- src/cli/src/utils/hooks-materialiser.ts (~140 LOC)
- src/cli/tests/utils/hooks-materialiser.test.ts (~165 LOC, 11 tests
covering: write-from-scratch, multi-source coexistence, user-hook
preservation, update replaces (not duplicates), shrink drops events,
remove targets only the named source, multiple events independent,
idempotent, empty/JSONC settings.json tolerance)
### Edited
- src/cli/src/commands/skills.ts: applyOne calls applyManagedHooks
when metadata.hooks is set; calls removeManagedHooks(skillName) when
a previously-installed skill no longer declares hooks; orphan
removal also drops the skill's hooks. New SyncResult field
`hooksApplied`. Earlier `meta` declaration deduped (it was redeclared
in the postInstall block).
## Verification
164 test files / 2182 tests green (up from 2171).
End-to-end on a real machine after this PR ships:
```yaml
# skill metadata.yaml
hooks:
PreToolUse:
- type: command
command: "audit-pretool.sh"
SessionStart:
- type: command
command: "claude-greeting.sh"
```
```
mcpctl skills sync
# → mcpctl: 1 installed, 1 hooks applied
jq '.hooks' ~/.claude/settings.json
# → entries tagged with _mcpctl_managed + _mcpctl_source
```
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
d60ad52018 |
feat(cli): postInstall executor for mcpctl skills sync
Closes the biggest deferred item from PR-5. metadata.postInstall scripts now actually run when their hash changes, with audit emission back to mcpd. Trust model unchanged from the corporate-appliance design: mcpd is the source of truth, content is reviewed at publish time, the client just runs. No sandbox, no signing, no consent prompts. The controls that matter are already on the publishing side (RBAC, audit, reviewer queue). What we DO provide is ops hygiene: - Hard timeout (default 60 s; per-skill override via metadata.postInstallTimeoutSec). SIGTERM at the deadline, SIGKILL after a 2 s grace. - Hash pinning. The script's sha256 is recorded in ~/.mcpctl/skills-state.json. Re-syncs of an unchanged script are a cheap no-op. A re-published "same version, fixed script" still triggers re-execution because its hash changed. - Curated env. PATH/HOME/USER/SHELL inherited; everything else dropped. MCPCTL_SKILL_NAME / _VERSION / _DIR / _PROJECT injected so scripts have stable context. - Per-skill install log under ~/.mcpctl/skills/<name>/install.log. Bounded at 5 × 256 KB; old entries truncated from the front. - Audit event back to mcpd (POST /api/v1/audit-events, eventKind=skill_postinstall) on every run, including hostname so admins can see fleet rollout state. Best-effort — failures are warned but never block the sync. - Path-escape rejection. metadata.postInstall must resolve inside the skill bundle; relative paths that try to climb out are refused. - Auto-chmod 0755 on the script before spawn. Some upstreams ship 0644 + a shebang and expect a shell to handle it; we always spawn the path directly so we need +x. Failure semantics: on timeout or non-zero exit, the recorded postInstallHash is NOT updated. Next sync retries. Other skills in the same sync run continue regardless — postInstall errors are scoped, not fatal. ## Files ### Added - src/cli/src/utils/postinstall.ts (~200 LOC) - src/cli/tests/utils/postinstall.test.ts (~190 LOC, 10 tests covering success, env vars, chmod, non-zero exit, timeout via exec sleep, path-escape, missing script, log file shape + append-across-runs) ### Edited - src/cli/src/commands/skills.ts: applyOne now invokes runPostInstall + emitPostInstallAudit when metadata.postInstall is set and the script hash differs from prior state. New SyncResult fields: postInstallsRan, postInstallsSkipped. Summary line surfaces "N postInstall ran". --skip-postinstall flag now actually does what it says. ## Verification 163 test files / 2171 tests green (up from 2161). End-to-end on a real machine (after this PR ships and a skill with metadata.postInstall is published): ``` mcpctl skills sync # → mcpctl: 1 installed, 1 postInstall ran cat ~/.mcpctl/skills/<name>/install.log # see stdout/stderr mcpctl skills sync # idempotent — skipped ``` If the same skill is republished with a fixed script: ``` mcpctl skills sync # → mcpctl: 1 updated, 1 postInstall ran (hash changed → rerun) ``` If the script fails (exit 7): ``` mcpctl skills sync # → mcpctl: 1 updated, 1 errors mcpctl skills sync # state.postInstallHash NOT updated → retries ``` Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
58e8e956ce |
feat(cli+mcpd): mcpctl skills sync + config claude extension
Phase 5 of the Skills + Revisions + Proposals work. Skills are now
materialised onto disk under ~/.claude/skills/<name>/, with
hash-pinned diff against mcpd, atomic per-skill install, and
preservation of locally-modified files. `mcpctl config claude --project X`
now wires the full pickup chain: writes .mcpctl-project marker, runs
the initial sync, installs the SessionStart hook so subsequent Claude
invocations stay in sync transparently.
## Sync algorithm
1. Resolve project: `--project` flag overrides; else walk up from cwd
looking for `.mcpctl-project`; else fall back to globals-only.
2. GET /api/v1/projects/:name/skills/visible (or
/api/v1/skills?scope=global without a project). Server returns
id + name + semver + scope + contentHash + metadata — no body, no
files. The contentHash is sha256 of the canonicalised body, computed
server-side; any reordering of keys produces the same hash, so it's
a stable diff key.
3. Load ~/.mcpctl/skills-state.json (lives outside ~/.claude/skills/
on purpose — Claude Code reads that tree and we don't want to
pollute it with our bookkeeping).
4. Diff:
- server skill not in state → INSTALL
- server skill, state contentHash matches → SKIP (cheap path)
- server skill, state contentHash differs → UPDATE (fetch full body)
- state skill not in server → orphan, REMOVE (preserve if locally
modified, unless --force)
5. Atomic per-skill install: write to <targetDir>.mcpctl-staging-<pid>/,
rename existing tree to .mcpctl-trash-<pid>, swap staging in,
rmtree the trash. A concurrent reader (Claude Code starting up)
never sees a partial tree.
6. State file updated with new versions, per-file SHA-256, install
path. saveState is atomic (temp + rename).
## Failure semantics
- `--quiet` mode (used by SessionStart hook): exit 0 on network /
timeout / mcpd error. Fail-open is non-negotiable here — we never
want a hung mcpd to block Claude Code starting up.
- Auth failure: exit 1, clear "run mcpctl login" message.
- Disk error during state save: exit 2.
- Per-skill errors are collected in the result and reported as a
count; one bad skill doesn't stop the others.
Network fetches run with concurrency 5. The server-side
`/visible` endpoint is metadata-only so the cheap path (everything
unchanged) needs exactly one HTTP roundtrip total.
## Files added
### CLI utilities (src/cli/src/utils/)
- skills-state.ts — load/save state, per-file sha256, edit detection.
- project-marker.ts — walk-up to find `.mcpctl-project`, bounded by
user home so we never search above $HOME.
- sessionhook.ts — install/remove a SessionStart hook entry tagged
with `_mcpctl_managed: true`. Idempotent. Defensive against
missing/empty/JSONC settings.json.
- skills-disk.ts — atomic install via staging-dir rename swap,
symmetric atomic delete via trash-dir rename. Path-escape attempts
in files{} are rejected.
### CLI command (src/cli/src/commands/)
- skills.ts — `mcpctl skills sync` Commander wrapper + the
`runSkillsSync(opts, deps)` library function (also called from
`mcpctl config claude --project`). Supports `--dry-run`, `--force`,
`--quiet`, `--keep-orphans`. `--skip-postinstall` is reserved
(postInstall execution lands in a follow-up PR, not this one).
### Wiring
- index.ts: registers `mcpctl skills` after `mcpctl review`.
- config.ts: `mcpctl config claude --project X` now writes the
`.mcpctl-project` marker, runs `runSkillsSync` in-process, and calls
`installManagedSessionHook('mcpctl skills sync --quiet')`. New flag
`--skip-skills` opts out (used by tests; useful for CI).
## Server-side change
- src/mcpd/src/services/skill.service.ts: getVisibleSkills now
computes contentHash on the fly from the canonical body shape the
client will reconstruct. Cheap (sha256 of ~few KB per skill); no
schema migration needed since hash is derived not stored.
## Tests
Four new utility test files (31 tests) under src/cli/tests/utils/:
- sessionhook.test.ts — creation, idempotency, command updates,
preservation of user hooks, removal, empty/JSONC tolerance.
- skills-disk.test.ts — atomic write, replacement without leftovers,
path-escape rejection, atomic delete, listing ignores
staging/trash artifacts.
- skills-state.test.ts — sha256 determinism, state round-trip,
schema-version drift handling, edit detection.
- project-marker.test.ts — cwd hit, walk-up, $HOME boundary, empty
marker, write+read round-trip.
The existing `mcpctl config claude` test (claude.test.ts) was updated
to pass `--skip-skills` so it stays focused on .mcp.json generation;
the new sync flow is covered by the utility tests.
Full suite: 162 test files / 2157 tests green (up from 158 / 2127).
## Deferred to a follow-up
- `metadata.hooks` materialisation into `~/.claude/settings.json` —
the data path exists, sync receives it; PR-7 or a focused follow-up
will write the `_mcpctl_managed: true` entries for declarative
hooks.
- `metadata.mcpServers` auto-attach via mcpd API — likewise.
- `metadata.postInstall` script execution — the most substantive
deferred piece. Current sync logs a TODO and skips. The corporate
trust model (publisher-side rigor, not client-side defence) means
this is straightforward to add once we wire the curated env +
timeout + audit emission. Orthogonal to file sync, easier to ship
separately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|