Skip to content

feat(server): read file + list directory endpoints for external projects#120

Merged
dvcdsys merged 4 commits into
developfrom
feat/file-tree-endpoints
Jul 2, 2026
Merged

feat(server): read file + list directory endpoints for external projects#120
dvcdsys merged 4 commits into
developfrom
feat/file-tree-endpoints

Conversation

@dvcdsys

@dvcdsys dvcdsys commented Jul 1, 2026

Copy link
Copy Markdown
Owner

What

Two read-only, external-project-only endpoints (surfaced through server API → CLI → MCP → skills) that let an agent read the actual contents and file tree of a repo the harness can't see locally:

  • POST /api/v1/projects/{path}/file — read a file, whole or by line range (FileReadRequest{file,start?,end?}FileContent)
  • POST /api/v1/projects/{path}/tree — list a directory, one level, ls-like (TreeRequest{dir?}DirectoryListing)

Why

Workspace / external (GitHub-backed) projects are used for research, but their files live only on the cix server — the harness (Claude Code locally, and especially Cowork, which has no local filesystem) can't read them. Search returns only chunk fragments; there was no way to read a whole file, a line range, or browse a directory. The server-side git worktree is the source of truth (the index stores only chunks — full files aren't reconstructable from them), so these read straight from disk.

How

  • External-only gating via the existing access.IsProjectExternal (git_repos-peer) helper — no new schema column. Local projects → 409 with a detail hint to use native filesystem tools; external-but-not-yet-cloned → 409. Access level GroupRead (requireProjectAccess).
  • Path safety: safeJoin rejects .., absolute paths, and symlink escape (EvalSymlinks re-check within root).
  • Concurrency: new internal/repolocks package — a per-repo sync.RWMutex shared (same instance) between the file read handlers (read-lock) and the clone worker's worktree rewrite in repojobs (write-lock around CloneOrFetch). A read never observes a mid-reset worktree. Both run in one process, so an in-process lock is sufficient.
  • Size / line / entry caps with truncated flags.
  • Addressing follows the existing project-scoped family: {path} = project hash in the URL, POST + JSON body, file arg named file/dir (not path, to avoid colliding with the {path} hash).

Surfaced across interfaces: CLI cix file <f> [--lines N:M] / cix tree [dir]; MCP cix_file / cix_tree (external-only in their descriptions); the three cix SKILL.md files + cix-workspace notes + /cix:file /cix:tree plugin commands.

Type of change

  • New feature

Checklist

  • go vet ./... passes (server + CLI)
  • Server go test ./... green; make build + make openapi-check pass
  • Concurrency test (TestReadProjectFile_Concurrent, 5 readers vs non-atomic writer) green under -race
  • No secrets or API keys committed

Follow-up (separate PR)

Rename the misleading {path} URL placeholder (it's really the project ID hash) → {projectId}. Non-breaking on the wire; deferred to keep this PR focused.

🤖 Generated with Claude Code

dvcdsys and others added 4 commits July 1, 2026 16:08
Add two read-only, external-project-only capabilities so agents (Claude
Code, and especially Cowork which has no local filesystem) can inspect the
actual contents and file tree of workspace / external repos the harness
cannot see locally:

- POST /api/v1/projects/{path}/file — read a file whole or by line range
- POST /api/v1/projects/{path}/tree — list a directory (one level, ls-like)

Both read from the server-side git worktree (the source of truth; the index
stores only chunks). Gated to EXTERNAL (GitHub-backed) projects via the
existing access.IsProjectExternal helper — local projects return 409 with a
hint to use native filesystem tools. Access is GroupRead (requireProjectAccess).

Safety + concurrency:
- safeJoin rejects path traversal (.., absolute, symlink escape)
- new internal/repolocks package: per-repo RWMutex shared between the file
  read handlers (read-lock) and the clone worker's worktree rewrite
  (write-lock around CloneOrFetch), so a read never observes a mid-reset tree
- size/line/entry caps with truncated flags

Surfaced across all interfaces:
- CLI: cix file <f> [--lines N:M], cix tree [dir]
- MCP: cix_file, cix_tree (external-only, described)
- skills: file/dir section in the three cix SKILL.md + cix-workspace notes,
  new /cix:file and /cix:tree plugin commands

Tests: internal/httpapi/file_test.go covers gating (local 409, uncloned 409,
unauthorized 404, group-shared 200), path traversal, line ranges, tree
listing, and a required concurrency test (5 readers vs a non-atomic writer,
green under -race).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ud past byte cap

Addresses review findings on the file/tree endpoints:

- safeJoin now rejects the repo-root .git segment, so git metadata is
  unreadable via /file too (it was already hidden from /tree). First-segment
  check targets exactly the clone's own .git.
- CLI and MCP renderers decide "no lines" on end_line < start_line (the
  server's authoritative signal) instead of Content == "", which wrongly
  swallowed a legitimate single-blank-line file.
- ReadProjectFile returns 400 with an actionable detail when a line range
  begins past the 2 MiB read cap, instead of a confusing empty 200.

Tests: .git paths added to the traversal test; new TestReadProjectFile_RangePastByteCap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, honest total_lines

- file.go: take the per-repo read-lock BEFORE path validation so a concurrent
  clone can't swap in an escaping symlink between check and open (TOCTOU)
- file.go: reject inverted line ranges (end<start) with 422 instead of silently
  clamping — enforced server-side for MCP/raw callers, not just the CLI
- file.go: /tree now 400s on a malformed body instead of silently listing root
- file.go: total_lines counts the whole file on disk (streams past the 2 MiB
  content cap) instead of just the buffered slice
- repojobs: run the indexer's worktree walk under the per-repo read-lock so a
  concurrent clone reset can't tear the index across two revisions
- repolocks: add panic-safe WithWrite/WithRead helpers (defer-unlock); the clone
  worker used a plain unlock that leaked the lock forever on a recovered panic
- tests: symlink-swap TOCTOU guard, /tree access-gating, byte-cap total_lines,
  inverted-range 422, malformed-body 400, repolocks panic + mutual-exclusion,
  handleIndex-takes-read-lock, and a FileContent/DirectoryListing wire golden

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mode truncation warning

- cmd/file.go: emit the truncation warning to stderr in --raw mode too, so
  `cix file … --raw > copy` no longer silently drops capped content
- client/file_test: parse the server's FileContent/DirectoryListing wire shape
  (twin golden of the server-side contract test), assert 422/409/404/400 surface
  as errors, and that an unset range omits start/end from the request body
- cmd/mcp_tools_test: formatFileContent/formatTree rendering, incl. the
  empty-range (end<start) and truncated signals the MCP tools rely on
- cmd/file_test: raw-mode truncation-warning coverage (stdout vs stderr)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dvcdsys dvcdsys merged commit 9408378 into develop Jul 2, 2026
5 checks passed
@dvcdsys dvcdsys deleted the feat/file-tree-endpoints branch July 2, 2026 11:14
dvcdsys added a commit that referenced this pull request Jul 2, 2026
…ync in CI

#120 added the `cix tree`/`cix file` guidance block to the plugin copy of
the cix-workspace skill (and to skills/cix/SKILL.md and both cix-cowork
skills) but missed the canonical skills/cix-workspace/SKILL.md — so
sync-skills.sh --check drifted, and a plain `sync-skills.sh` run would have
overwritten the plugin copy and deleted #120's addition.

- Forward-port the block into canonical skills/cix-workspace/SKILL.md
  (plugin was ahead; copy plugin → canonical to restore byte-identity).
- Align the investigator agent `description` quoting (canonical → quoted,
  matching the plugin bundle).
- Wire sync-skills.sh --check into ci-plugin.yml and broaden its path
  triggers to plugins/** and skills/** so a canonical-only edit runs the
  drift check — previously only plugins/cix/** triggered it, the exact gap
  that let #120 drift through unnoticed.
- Validate the cix-cowork plugin manifest in the jq step too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dvcdsys added a commit that referenced this pull request Jul 2, 2026
… deps) (#107)

* ci(dependabot): target the develop branch for version updates

Our flow is feature -> develop (dev/manual tests) -> main (release), but
Dependabot defaulted to the repo default branch (main), so weekly version
bumps landed straight on main, bypassing develop. Set target-branch:
"develop" on all five update blocks (gomod x2, npm, docker, github-actions)
so scheduled version PRs go to develop.

Note: Dependabot reads this config from the default branch (main), so the
change only takes effect once it reaches main. Security (alert-triggered)
updates still target the default branch by platform design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(deps): bump the go group in /server with 6 updates (#102)

* docs: add reindex-after-upgrade disclaimer to README

* chore(deps): bump the go group in /server with 6 updates

Bumps the go group in /server with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [github.com/getkin/kin-openapi](https://github.com/getkin/kin-openapi) | `0.135.0` | `0.140.0` |
| [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) | `5.2.4` | `5.3.0` |
| [github.com/oapi-codegen/runtime](https://github.com/oapi-codegen/runtime) | `1.4.0` | `1.4.2` |
| [golang.org/x/crypto](https://github.com/golang/crypto) | `0.52.0` | `0.53.0` |
| [golang.org/x/sync](https://github.com/golang/sync) | `0.20.0` | `0.21.0` |
| [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) | `1.34.1` | `1.53.0` |


Updates `github.com/getkin/kin-openapi` from 0.135.0 to 0.140.0
- [Release notes](https://github.com/getkin/kin-openapi/releases)
- [Commits](getkin/kin-openapi@v0.135.0...v0.140.0)

Updates `github.com/go-chi/chi/v5` from 5.2.4 to 5.3.0
- [Release notes](https://github.com/go-chi/chi/releases)
- [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md)
- [Commits](go-chi/chi@v5.2.4...v5.3.0)

Updates `github.com/oapi-codegen/runtime` from 1.4.0 to 1.4.2
- [Release notes](https://github.com/oapi-codegen/runtime/releases)
- [Commits](oapi-codegen/runtime@v1.4.0...v1.4.2)

Updates `golang.org/x/crypto` from 0.52.0 to 0.53.0
- [Commits](golang/crypto@v0.52.0...v0.53.0)

Updates `golang.org/x/sync` from 0.20.0 to 0.21.0
- [Commits](golang/sync@v0.20.0...v0.21.0)

Updates `modernc.org/sqlite` from 1.34.1 to 1.53.0
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.34.1...v1.53.0)

---
updated-dependencies:
- dependency-name: github.com/getkin/kin-openapi
  dependency-version: 0.140.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go
- dependency-name: github.com/go-chi/chi/v5
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go
- dependency-name: github.com/oapi-codegen/runtime
  dependency-version: 1.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go
- dependency-name: golang.org/x/crypto
  dependency-version: 0.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go
- dependency-name: golang.org/x/sync
  dependency-version: 0.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go
- dependency-name: modernc.org/sqlite
  dependency-version: 1.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump github.com/spf13/cobra from 1.8.0 to 1.10.2 in /cli in the go group (#97)

* docs: add reindex-after-upgrade disclaimer to README

* chore(deps): bump github.com/spf13/cobra in /cli in the go group

Bumps the go group in /cli with 1 update: [github.com/spf13/cobra](https://github.com/spf13/cobra).


Updates `github.com/spf13/cobra` from 1.8.0 to 1.10.2
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](spf13/cobra@v1.8.0...v1.10.2)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-version: 1.10.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump @babel/core from 7.29.0 to 7.29.7 in /server/dashboard (#103)

* docs: add reindex-after-upgrade disclaimer to README

* chore(deps-dev): bump @babel/core in /server/dashboard

Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.29.0 to 7.29.7.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.29.7/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-version: 7.29.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump alpine from 3.20 to 3.24 in /server (#95)

* docs: add reindex-after-upgrade disclaimer to README

* chore(deps): bump alpine from 3.20 to 3.24 in /server

Bumps alpine from 3.20 to 3.24.

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: '3.24'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump actions/setup-go from 5 to 6 (#100)

* docs: add reindex-after-upgrade disclaimer to README

* chore(deps): bump actions/setup-go from 5 to 6

Bumps [actions/setup-go](https://github.com/actions/setup-go) from 5 to 6.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](actions/setup-go@v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump softprops/action-gh-release from 2 to 3 (#98)

* docs: add reindex-after-upgrade disclaimer to README

* chore(deps): bump softprops/action-gh-release from 2 to 3

Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](softprops/action-gh-release@v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump docker/login-action from 3 to 4 (#93)

* docs: add reindex-after-upgrade disclaimer to README

* chore(deps): bump docker/login-action from 3 to 4

Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](docker/login-action@v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(ci): add Docker Hub tag retention + auto-prune (#105)

The Docker Hub repo had grown past 150 GB — Docker Hub never prunes tags,
so every release (~1 GB CUDA) plus scout/test/dev scan tags accumulated
forever (53 tags, ~44 of them stale).

Add server/scripts/dockerhub-prune.sh: a deny-by-default retention policy
that keeps the floating tags (latest, cu128, develop-cu128) + the newest
KEEP_RELEASES (default 3) semver versions, and deletes everything else
(scout-*, *-cu130/-cu126, go-*, *-dev*, ci-test-*, unprefixed 0.x, old
releases). DRY_RUN=true by default; portable across GNU + BSD (macOS) sort.

Enforce it automatically:
- release-server.yml: prune job after each release push (keep newest 3).
- cleanup-dockerhub.yml: weekly sweep + manual dispatch (dry-run default).

Both reuse the existing DOCKER_USERNAME / DOCKER_PASSWORD secrets. Deleted
release tags remain rebuildable via the release workflow_dispatch path.

Also fix portainer-stack-cuda.yml: it still referenced the obsolete
`go-cu128` floating tag (renamed to `cu128` long ago) — a future redeploy
from that file would have failed to pull once go-cu128 is pruned.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* chore(deps): bump actions/upload-artifact from 4 to 7 (#94)

* docs: add reindex-after-upgrade disclaimer to README

* chore(deps): bump actions/upload-artifact from 4 to 7

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump actions/download-artifact from 4 to 8 (#99)

* docs: add reindex-after-upgrade disclaimer to README

* chore(deps): bump actions/download-artifact from 4 to 8

Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](actions/download-artifact@v4...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump golang from 1.25-alpine to 1.26-alpine in /server (#101)

* docs: add reindex-after-upgrade disclaimer to README

* chore(deps): bump golang from 1.25-alpine to 1.26-alpine in /server

Bumps golang from 1.25-alpine to 1.26-alpine.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.26-alpine
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Anton Tribulkevich <dvcdsys@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat(cowork): cix in Claude Cowork via MCP connector + skills plugin (#106)

* feat(cowork): cix in Claude Cowork via MCP connector + skills plugin

Claude Cowork doesn't load Claude Code plugins/hooks/slash-commands and
doesn't shell out to the cix CLI, so cix was unreachable there — exactly
where it's most useful (large cross-repo research). Expose it over MCP.

Two-step, server-centric, multi-server:

- `cix mcp`: stdio MCP server subcommand (cli/cmd/mcp.go, mcp_tools.go) as a
  thin front-end over the existing internal/client HTTP client. 11 tools —
  list servers/projects/workspaces, cross-project workspace_search, and
  per-project search/definitions/references/symbols/files/summary. No cwd
  inference and no "current project"; project (host_path) and workspace are
  explicit, server is an optional per-call selector resolved against a cached
  serverRegistry.
- `cix mcp connect claude`: one-command installer (cli/cmd/mcp_connect.go)
  that registers this binary in claude_desktop_config.json via the official
  mcpServers mechanism — absolute path, preserves other servers, writes .bak,
  idempotent, --print dry-run, disconnect to undo, no secrets.
- cli/mcpb/: optional .mcpb desktop-extension bundle (make mcpb) for
  redistributing the connector to machines without cix.
- plugins/cix-cowork/: skills-only marketplace plugin (2nd entry) — the cix
  and cix-workspace skills adapted from the CLI to the cix_* MCP tools
  (workspace uses sequential cix_search drill-down, no sub-agent; trust-rules
  + troubleshooting in references/). Install after connecting the server.
- doc/COWORK_MCP.md, marketplace 2nd entry, go.mod (MCP SDK), Makefile mcpb.
- fix(plugin): quote the cix-workspace-investigator description so strict
  `claude plugin validate` passes (unquoted "Do not use for:" colon).

The Claude Code `cix` plugin is unchanged (CLI + hooks); this is its Cowork
counterpart over the same cix server.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: slim README to a landing page; add credits, Voyage, team-deploy guide

Cut README from ~790 to ~320 lines by de-duplicating repeated notes
(bootstrap admin, reindex-after-upgrade, drift, GPU) and relocating
reference material into focused docs. No content lost — moved, not deleted.

New docs:
- doc/CLI_REFERENCE.md   — full CLI surface + per-project config
- doc/DASHBOARD.md       — pages, auth, authorization model, drift
- doc/TROUBLESHOOTING.md — common issues + search-quality tuning
- doc/TEAM_DEPLOYMENT.md — self-hosting cix for a team (DevOps guide)

README also gains an Embedding providers section (local / Voyage AI /
OpenAI-compatible — Voyage was already a first-class provider but nearly
invisible) and an Acknowledgements section crediting upstreams.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(cowork): MCP install/uninstall seam, drop .mcpb, fix min_score & partial-status

Revise the Cowork MCP integration and fix three review findings.

Connector UX & packaging:
- Rename `cix mcp connect|disconnect claude` → `install|uninstall claude-desktop`
  behind a host-registry seam (mcpHosts/lookupHost), so adding hosts
  (e.g. Codex/TOML) is a table entry, not a new command.
- Drop the `.mcpb` desktop-extension path (build.sh/manifest.json, Makefile
  target) — MCP connector + marketplace skills only.
- Rewrite doc/COWORK_MCP.md and plugin docs to match.

Review fixes (MCP must mirror the CLI — it is just another CLI surface):
- cix_search: apply the documented default min_score 0.4 when omitted instead
  of silently sending nothing (server default 0.2). Single source of truth via
  searchDefaultMinScore, shared by the `cix search` flag and the MCP tool.
  min_score is now *float64 — omit → 0.4, explicit (incl 0) → passthrough.
- cix_workspace_search: add the missing min_score parameter (the skill docs
  promise it and the agent errored without it). Thread it through
  client.WorkspaceSearch (server already supports ?min_score=); add a matching
  `cix ws search --min-score` flag for CLI parity.
- formatWorkspaceSearch: surface status=="partial_failure" with a banner, the
  way the CLI renderSearch does, so partial results aren't read as complete.

Add TestRegisterCixTools_SchemaReflection guarding the *float64 schema
reflection. go test ./... green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(cowork): tighten host-config perms & warn on empty partial_failure

- mcp install/uninstall: preserve the host config's file mode and create a
  fresh one private (0o600) instead of 0o644 — it sits next to other MCP
  servers' secret env values; chmod even on overwrite so an already
  world-readable config is tightened rather than left as-is.
- formatWorkspaceSearch: check partial_failure BEFORE the empty-result
  shortcut (like the CLI's renderSearch) so an empty result caused by repo
  failures warns about incomplete coverage instead of reading as "no matches".
- test: cover the empty partial_failure case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* docs: credit odvcencio/gotreesitter in acknowledgements

The Go tree-sitter binding cix's AST chunking first grew from. Credit where
the head start came from.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: credit CLI and dashboard dependencies in acknowledgements

The acknowledgements covered the server stack but omitted two whole
categories. Add them:

- CLI: Cobra, Charm (Bubble Tea/Bubbles/Lip Gloss), the MCP Go SDK (which
  the Cowork integration stands on), rjeczalik/notify, and koanf.
- Dashboard: React + Vite, Radix UI + Tailwind (shadcn/ui pattern), TanStack
  Query, openapi-typescript, lucide, and sonner.

Also point the closing note at server/dashboard/package.json for the full
frontend dependency list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(server): read file + list directory endpoints for external projects (#120)

* feat(server): read file + list directory endpoints for external projects

Add two read-only, external-project-only capabilities so agents (Claude
Code, and especially Cowork which has no local filesystem) can inspect the
actual contents and file tree of workspace / external repos the harness
cannot see locally:

- POST /api/v1/projects/{path}/file — read a file whole or by line range
- POST /api/v1/projects/{path}/tree — list a directory (one level, ls-like)

Both read from the server-side git worktree (the source of truth; the index
stores only chunks). Gated to EXTERNAL (GitHub-backed) projects via the
existing access.IsProjectExternal helper — local projects return 409 with a
hint to use native filesystem tools. Access is GroupRead (requireProjectAccess).

Safety + concurrency:
- safeJoin rejects path traversal (.., absolute, symlink escape)
- new internal/repolocks package: per-repo RWMutex shared between the file
  read handlers (read-lock) and the clone worker's worktree rewrite
  (write-lock around CloneOrFetch), so a read never observes a mid-reset tree
- size/line/entry caps with truncated flags

Surfaced across all interfaces:
- CLI: cix file <f> [--lines N:M], cix tree [dir]
- MCP: cix_file, cix_tree (external-only, described)
- skills: file/dir section in the three cix SKILL.md + cix-workspace notes,
  new /cix:file and /cix:tree plugin commands

Tests: internal/httpapi/file_test.go covers gating (local 409, uncloned 409,
unauthorized 404, group-shared 200), path traversal, line ranges, tree
listing, and a required concurrency test (5 readers vs a non-atomic writer,
green under -race).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(server): block .git via /file, honest empty-range signal, fail loud past byte cap

Addresses review findings on the file/tree endpoints:

- safeJoin now rejects the repo-root .git segment, so git metadata is
  unreadable via /file too (it was already hidden from /tree). First-segment
  check targets exactly the clone's own .git.
- CLI and MCP renderers decide "no lines" on end_line < start_line (the
  server's authoritative signal) instead of Content == "", which wrongly
  swallowed a legitimate single-blank-line file.
- ReadProjectFile returns 400 with an actionable detail when a line range
  begins past the 2 MiB read cap, instead of a confusing empty 200.

Tests: .git paths added to the traversal test; new TestReadProjectFile_RangePastByteCap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(server): address PR #120 review — lock ordering, panic-safe locks, honest total_lines

- file.go: take the per-repo read-lock BEFORE path validation so a concurrent
  clone can't swap in an escaping symlink between check and open (TOCTOU)
- file.go: reject inverted line ranges (end<start) with 422 instead of silently
  clamping — enforced server-side for MCP/raw callers, not just the CLI
- file.go: /tree now 400s on a malformed body instead of silently listing root
- file.go: total_lines counts the whole file on disk (streams past the 2 MiB
  content cap) instead of just the buffered slice
- repojobs: run the indexer's worktree walk under the per-repo read-lock so a
  concurrent clone reset can't tear the index across two revisions
- repolocks: add panic-safe WithWrite/WithRead helpers (defer-unlock); the clone
  worker used a plain unlock that leaked the lock forever on a recovered panic
- tests: symlink-swap TOCTOU guard, /tree access-gating, byte-cap total_lines,
  inverted-range 422, malformed-body 400, repolocks panic + mutual-exclusion,
  handleIndex-takes-read-lock, and a FileContent/DirectoryListing wire golden

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(cli): file/tree client + MCP compatibility tests; fix(cli): raw-mode truncation warning

- cmd/file.go: emit the truncation warning to stderr in --raw mode too, so
  `cix file … --raw > copy` no longer silently drops capped content
- client/file_test: parse the server's FileContent/DirectoryListing wire shape
  (twin golden of the server-side contract test), assert 422/409/404/400 surface
  as errors, and that an unset range omits start/end from the request body
- cmd/mcp_tools_test: formatFileContent/formatTree rendering, incl. the
  empty-range (end<start) and truncated signals the MCP tools rely on
- cmd/file_test: raw-mode truncation-warning coverage (stdout vs stderr)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(skills): forward-port #120 file/dir docs to canonical + enforce sync in CI

#120 added the `cix tree`/`cix file` guidance block to the plugin copy of
the cix-workspace skill (and to skills/cix/SKILL.md and both cix-cowork
skills) but missed the canonical skills/cix-workspace/SKILL.md — so
sync-skills.sh --check drifted, and a plain `sync-skills.sh` run would have
overwritten the plugin copy and deleted #120's addition.

- Forward-port the block into canonical skills/cix-workspace/SKILL.md
  (plugin was ahead; copy plugin → canonical to restore byte-identity).
- Align the investigator agent `description` quoting (canonical → quoted,
  matching the plugin bundle).
- Wire sync-skills.sh --check into ci-plugin.yml and broaden its path
  triggers to plugins/** and skills/** so a canonical-only edit runs the
  drift check — previously only plugins/cix/** triggered it, the exact gap
  that let #120 drift through unnoticed.
- Validate the cix-cowork plugin manifest in the jq step too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(server/cuda): bump pinned llama.cpp digest (#108)

Upstream ghcr.io/ggml-org/llama.cpp:server-cuda moved past the pinned
digest. Bump to the current digest reported by the weekly pin-freshness
workflow:

  2ea4be9… → d0cd0c4563e046f6ad0c8c27fe78280376d0d645cc3fef7a1ab87c75de08377b

The runtime stage copies all /app/*.so* so a shared-lib layout change
stays robust, but `make scout-cuda` must still rebuild + scan (and
confirm /app/llama-server resolves its libs) before promoting to prod —
release-server.yml runs that scan at tag time.

Closes #108

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(dashboard): make legacy clipboard copy read its own textarea, not the key input

On plain-HTTP deploys (e.g. http://<lan-ip>:21847) the API-key "created"
popup runs in a non-secure context, so navigator.clipboard is unavailable
and both Copy buttons fall through to the document.execCommand('copy')
fallback (legacyCopy).

That fallback was fragile on WebKit: the reveal screen auto-selects the
"Full key" input on mount, and the copy textarea was hidden with opacity:0.
WebKit refuses execCommand('copy') on effectively-invisible nodes, so the
copy silently read the still-selected key input instead — which made the
"Connect the cix CLI" button copy the bare key instead of the CLI command.

Harden legacyCopy:
- blur the previously focused/selected element so a stale selection can't win
- position the textarea off-screen instead of opacity:0 (a real rendered node)
- setSelectionRange after select() for iOS/older Safari

Verified in Chromium that the copy source is the intended textarea for both
buttons. Frontend-only; no CLI/server change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dvcdsys added a commit that referenced this pull request Jul 2, 2026
#120 added the `cix file` / `cix tree` slash commands and skill guidance to
the cix plugin but left the version at 0.2.1. `claude plugin update` keys on
the manifest version, so it sees 0.2.1 == 0.2.1 and silently no-ops — users
on an older 0.2.1 never receive the new content unless they remove+reinstall.
Bump to 0.3.0 so the new commands propagate via the normal update path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
dvcdsys added a commit that referenced this pull request Jul 2, 2026
chore(plugin/cix): bump version 0.2.1 → 0.3.0 so plugin update propagates #120
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.

1 participant