Adds an optional `projectId` field to `.socraticode.json` so teams can
commit a stable project identifier to the repo. Without this field the
project ID is derived from the SHA-256 of the absolute checkout path,
which means the same project resolves to a different Qdrant collection
on every machine, OS user, filesystem layout, or worktree. With it,
every checkout addresses the same `codebase_*`, `codegraph_*`, and
`context_*` collections regardless of where the working tree lives on
disk.
This is the path-independent, multi-project complement to the existing
`SOCRATICODE_PROJECT_ID` env var. The env var is process-scoped and
global to all projects in a host, so it does not scale to a developer
who works on several projects on one laptop. The file is per-project
and shared across teammates via git.
Resolution precedence (highest first):
1. `SOCRATICODE_PROJECT_ID` env var (per-machine override)
2. `projectId` in .socraticode.json (committed, team-wide)
3. SHA-256 prefix of the absolute path (existing default)
Both override paths trim whitespace, validate against `[a-zA-Z0-9_-]+`,
and throw on invalid characters so a misconfigured value cannot
silently route a project to the wrong (or empty) collection. Malformed
JSON, missing fields, wrong types, and empty/whitespace-only values
fall through to the next precedence level so the MCP server stays
resilient against hand-edited config files. Branch-aware mode is
suppressed for either explicit override since explicit identifiers
are stable by intent.
Also fixes a pre-existing bug in `resolveLinkedCollections`: linked
projects were resolved via `coreProjectId(linkedPath)` (path hash
only), so a linked project that pinned its own `projectId` in
`.socraticode.json` would silently miss its actual data during
cross-project search. Linked-project resolution now goes through a
new `effectiveBaseProjectId` helper that honors the committed value,
preserving symmetry: a project addresses the same Qdrant collection
whether it is the current root or a linked dependency. Dedup is
tightened to use the same effective base ID, so two paths pinning the
same shared identifier collapse to a single result.
The env var deliberately does not leak into linked-project collection
names. It is process-scoped and applying it as a single value to every
linked path would collapse them onto the env-var collection, silently
losing per-project isolation.
Tests: 16 new cases in tests/unit/config.test.ts, written TDD-style
(RED to GREEN). Coverage:
- `projectIdFromPath` (13): file resolution, ignores path
differences when file projectId is set, whitespace trimming,
throws on invalid characters, falls back to hash on
empty/whitespace/wrong-type/null/missing-file/malformed-JSON,
env-var precedence over file, branch-suffix suppression, and
coexistence with `linkedProjects` in the same file.
- `resolveLinkedCollections` (3): linked project's committed
projectId honored, dedup on shared committed projectId, env var
does not leak into linked-project collection names.
The new branch-aware-suppression test explicitly disables git
`commit.gpgsign` and `tag.gpgsign` in its throwaway-repo fixture so
the test is robust against the developer's global git config.
Backwards compatible: zero behaviour change for users who do not adopt
the new field. The `SocratiCodeConfig` interface gains an optional
field; existing `linkedProjects` parsing is functionally identical
(routed through the new shared `loadSocratiCodeConfig` helper).
Composes cleanly with the recently-added `QDRANT_COLLECTION_PREFIX`:
prefix + projectId combine into `<prefix>codebase_<projectId>` as
expected.
README and DEVELOPER documentation updated: new "Team-Shared Index
(committed `projectId`)" section in README between Git Worktrees and
Cross-Project Search, and the env-var table notes the new precedence.
DEVELOPER's "Project ID & Collection Naming" section now documents
the three-level precedence and explains why both override paths
suppress the branch-aware suffix.
Co-authored-by: airmonitor <tomasz.szuster@gmail.com>
indexAllArtifacts and ensureArtifactsIndexed previously called saveContextMetadata only once, after the entire indexing pass completed. When the underlying loop took longer than the MCP client's tool-call timeout, completed artifacts appeared unindexed because their state was never persisted, and partial progress was lost.
This patch saves the metadata snapshot after every successfully indexed artifact, so each artifact's success is durable as soon as the indexing for it returns. It also seeds the in-flight stateMap from the previously-loaded existingStates so that interrupted runs can preserve completed work for artifacts already finished, and uses that same original snapshot to identify orphan artifacts that need cleanup when the config has changed.
Backwards compatible: a successful full run produces exactly the same final on-disk state as before. The only behavioural difference is in the interrupted-mid-run case, where the new code retains more state instead of losing everything since the last full pass.
Tests: 3 new cases in tests/unit/context-artifacts-checkpoint.test.ts covering the checkpointing path during full indexing, preservation of earlier successes when a later artifact fails, and preservation of up-to-date states while re-indexing stale ones. Existing unit tests continue to pass unchanged.
Co-authored-by: jackblackjack chugarev@gmail.com
Resolves#49. Reported by @awbait.
When sharing a single Qdrant server across multiple applications
(SocratiCode + Open-WebUI + custom RAG, etc.) or across multiple
SocratiCode instances (per-project, per-environment, per-user), the
fixed `codebase_<id>` / `codegraph_<id>` / `context_<id>` /
`<id>_symgraph_*` / `socraticode_metadata` collection names risk
colliding with other apps and prevent isolation between SocratiCode
instances.
This patch adds an optional QDRANT_COLLECTION_PREFIX env var that, when
set, is prepended verbatim to every Qdrant collection name SocratiCode
creates, queries, lists, or deletes. Default empty string preserves the
existing collection names exactly: fully backwards compatible.
Touchpoints (mechanical, no logic changes):
- src/constants.ts: new QDRANT_COLLECTION_PREFIX export with eager
validation. Qdrant accepts only [a-zA-Z0-9_-] in collection names; an
invalid prefix throws at module load with a message naming the
offending value, before any Qdrant call is attempted.
- src/config.ts: all six collection-name generators
(collectionName, graphCollectionName, contextCollectionName,
symgraphMetaCollectionName, symgraphFileCollectionName,
symgraphIndexCollectionName) prepend the prefix. Generator semantics
are otherwise unchanged.
- src/services/qdrant.ts: METADATA_COLLECTION (the global
socraticode_metadata collection used for cross-project state) also
honours the prefix, so two SocratiCode instances on one Qdrant keep
their metadata isolated as well as their per-project collections.
The two startsWith() filters in listCodebaseCollections — used by
codebase_list_projects to discover this instance's collections —
build the match prefix from QDRANT_COLLECTION_PREFIX so a prefixed
instance only sees its own collections, not those of co-tenants.
- src/tools/manage-tools.ts: codebase_list_projects similarly uses the
prefix in its filters. The projectId extraction (formerly
c.replace("codebase_", "")) now slices the full
${prefix}codebase_ token so the recovered id is correct under any
prefix; the codegraph cross-reference uses the same prefixed name.
Tests: 20 new test cases in tests/unit/qdrant-collection-prefix.test.ts
covering:
- Default empty prefix preserves the legacy collection-name forms for
all six generators (regression guard against backward-compat break).
- Empty-string env var is treated identically to unset.
- Non-empty prefix prepends correctly to all six generators, including
the suffix-style symgraph names.
- Two different prefixes produce disjoint collection-name sets for the
same projectId (the multi-instance isolation property).
- Validation rejects whitespace, slash, colon, and unicode characters.
- The error message includes the offending value for discoverability.
- Validation accepts the full set of legal characters.
Existing 752 unit tests continue to pass unchanged. Total: 772.
typecheck, biome, and CodeRabbit local review all clean. README
updated to document the new env var alongside the other QDRANT_*
settings, including the user-side responsibility to remove old
collections when changing prefix between runs.
Co-authored-by: awbait <awbait@users.noreply.github.com>
Address CodeRabbit review on PR #48. The early `isExternalModule` check
in resolveImport was filtering out any import starting with `golang.org/`
before the Go case had a chance to match it against the local module
path. This blocked legitimate local imports for any project whose own
module path starts with `golang.org/` (the Go team's own packages like
golang.org/x/sync, golang.org/x/net, etc., where each one's go.mod
declares `module golang.org/x/<name>`).
Skip the early external check for Go specifically. The Go case in
resolveImport already does its own module-path-aware classification
and returns null for everything outside the local module, including
stdlib and third-party deps. No regression in those cases.
New regression test asserts that
`module golang.org/x/custom` + `import "golang.org/x/custom/internal"`
resolves to the local internal/ package. Confirmed the test fails
without the fix and passes with it. Total: 752 unit tests pass.
Co-authored-by: mrsuit92 <mrsuit92@users.noreply.github.com>
Resolves#45. Reported by @mrsuit92.
Go projects produced 0 dependency edges in codebase_graph_query and
codebase_graph_stats even though import extraction worked correctly.
The Go case in resolveImport returned null unconditionally, with a
comment that resolution required go.mod analysis. This patch adds
that analysis and wires it into the resolver, mirroring the existing
buildJvmSuffixMap and buildCsNamespaceMap patterns.
Mechanism:
- buildGoModuleInfo reads <projectPath>/go.mod once at graph-build
time, parses the `module <path>` directive, and walks the file set
to build a directory-to-representative-file map for every Go
package. _test.go files are excluded from representative selection
because Go does not allow them to be imported from non-test code in
other packages. Files are sorted lexicographically for
deterministic representative selection across machines and runs.
Returns null when go.mod is missing or has no parseable module
directive; the resolver treats null as "no Go resolution available"
and behaves exactly as before this patch in those cases.
- The Go case in resolveImport now strips the module path prefix
from the import (handling the bare-module-path root case as well
as subpackage paths) and looks up the resulting directory in the
package map. Imports outside the module path return null and are
treated as external dependencies (or stdlib already filtered
upstream by isExternalModule).
- Map keys are forward-slash paths, not OS-native, so resolution
works on Windows: Go imports are always forward-slash regardless
of host OS, but path.dirname produces backslashes on Windows for
nested directories. Normalising the key to forward slashes at
build time keeps the lookup correct across platforms.
Limitations (deferred to follow-up issues if any user reports them):
- The parenthesised module ( path ) form in go.mod is not parsed.
Not used by any mainstream Go project (verified against cobra,
gin-gonic/gin, uber-go/zap real-world go.mod files).
- vendor/ directory shadowing of external imports is not honoured.
- replace directives in go.mod are not honoured.
- go.work multi-module workspaces are not handled (each workspace
module would need its own go.mod read and prefix matching).
These are real Go features but each one widens the patch and
narrowly affects specific user populations. They can be added as
separate small PRs if a real user hits them.
Tests: 16 new cases in tests/unit/graph-resolution.test.ts covering
the new buildGoModuleInfo function (parses simple go.mod, handles
leading whitespace and trailing content, returns null on missing or
malformed go.mod, excludes _test.go from representative selection,
omits test-only directories, uses forward-slash keys for nested
packages) and the Go resolveImport case (back-compat null without
goModuleInfo, subpackage import resolves to lex-smallest non-test
.go file, root-package import resolves to a project-root .go file,
external imports return null, missing or malformed go.mod returns
null, _test.go excluded from representative selection, lexically
smallest file picked deterministically, similar-prefix imports do
not falsely resolve, nested-package imports work cross-platform).
Existing 735 unit tests continue to pass unchanged. Total: 751.
typecheck, biome, and CodeRabbit local review all clean. CodeRabbit
caught a real Windows path-separator bug in the first iteration; the
fix and a regression test for it are included.
Co-authored-by: mrsuit92 <mrsuit92@users.noreply.github.com>
Resolves#46. Reported by @mrsuit92.
Python projects where each top-level directory is a runnable application
root (a common service-style monorepo layout) had `import config` from
`service-a/main.py` produce 0 dependency edges, even when
`service-a/config.py` sits next to the importer. At runtime Python
resolves this correctly because the importer's directory is sys.path[0]
when the file is run as `python main.py` from inside its own directory.
The static resolver did not check that path.
The Python case in graph-resolution.ts only tried:
<projectPath>/<module>.py
<projectPath>/src/<module>.py
<projectPath>/lib/<module>.py
It did not try `<sourceDir>/<module>.py`, so non-relative sibling
imports never resolved. Relative imports (`from .config import ...`)
already used sourceDir and worked.
Fix: add `<sourceDir>/<module>.py` as the LAST fallback, after the
existing project-root and src/lib checks. Tried last to preserve
project-root precedence, so any layout that resolved before this PR
continues to resolve to the same file. resolveRelativePath also handles
the `<sourceDir>/<module>/__init__.py` package case via its built-in
Python init fallback, so package-style sibling imports work too.
Tests: 5 new cases in tests/unit/graph-resolution.test.ts covering
sibling-flat resolution, dotted module paths, package via __init__.py,
project-root precedence preservation, and the negative case (no match
anywhere). Existing 730 tests continue to pass; total now 735.
typecheck, biome, and CodeRabbit local review all clean.
Co-authored-by: mrsuit92 <mrsuit92@users.noreply.github.com>
Resolves#43.
On Linux/Node combinations where one ast-grep grammar package's prebuilt
parser binary is missing for the host architecture, the v1.8.3 loader
silently failed to register every dynamic grammar in the batch, not just
the broken one. registerDynamicLanguage iterates and accesses each
module's lazy libraryPath getter; one throwing getter aborts the call
atomically and zero grammars end up registered.
Fix: pre-validate each grammar's libraryPath getter inside the per-
grammar try/catch so a missing prebuild is contained to that grammar.
Build the batch object with only the survivors and make ONE atomic
registerDynamicLanguage call. Standard environments are unaffected
because all grammars pass pre-validation. Affected environments lose
only the unloadable grammar, the rest register cleanly.
Also captures the actual error reason (the previous empty `catch {}`
discarded it), bumps symbol- and import-extraction failure logs from
debug to warn with one-shot dedupe per language, exposes loaded/failed
grammars via a new getDynamicLanguageStatus() API, and renders an "AST
grammars" block in codebase_graph_status output so users see loader
state without enabling debug logging.
Empirically verified against @ast-grep/napi@0.40.5 in a clean Node
environment. Two probes confirmed the napi semantics: sequential
register({A}); register({B}) calls are REPLACING (so per-language
registration is broken), and batch register with one bad getter is
ATOMIC (so pre-validation before the batch call is the only correct
pattern). All 721 existing unit tests continue to pass unchanged. Adds
9 new tests for the loader status API; total 730 pass. typecheck and
biome clean. CodeRabbit returned no findings on this diff.
Co-authored-by: X-Adam <X-Adam@users.noreply.github.com>
LM Studio's Local Server speaks the OpenAI-compatible /v1/embeddings
protocol, so users running it as their model host (chat plus embedding in
one desktop app, GGUF model management) had no clean integration path.
Changes:
- src/services/provider-lmstudio.ts: new LMStudioEmbeddingProvider wrapping
the OpenAI SDK with a custom baseURL (default http://localhost:1234/v1).
Sends a placeholder API key to satisfy the OpenAI SDK while LM Studio's
Local Server runs without auth by default. Skips the dimensions parameter
because LM Studio models have no Matryoshka projection. Forces
encoding_format=float to defeat the OpenAI SDK 6.x base64 default, which
would otherwise mangle LM Studio's plain-array responses into 1024 zeros.
- src/services/embedding-config.ts: extends the EmbeddingProvider union,
reads LMSTUDIO_URL and LMSTUDIO_API_KEY, fail-fast validation when
EMBEDDING_PROVIDER=lmstudio without EMBEDDING_MODEL or EMBEDDING_DIMENSIONS.
- src/services/embedding-provider.ts: factory case for lmstudio with a
dynamic import to avoid loading the OpenAI SDK at startup for ollama users.
- ensureReady distinguishes "LM Studio unreachable" from "reachable but
embedding model not loaded" so the operator knows whether to start the
Local Server or load the configured model.
- src/services/qdrant.ts: minor refactor to extract the hybrid-search query
payload to a local const for readability.
- README.md: dedicated LM Studio section, MCP host config example, env-var
table entries.
- tests/unit/embedding-config.test.ts: 8 new cases (required-env validation,
URL default and override, optional API key, context-length override).
- tests/unit/embedding-provider.test.ts: 3 new cases (factory wiring,
ensureReady error format against a closed port, healthCheck unreachable
output).
Backward compatible. The lmstudio provider is opt-in via
EMBEDDING_PROVIDER=lmstudio. Existing ollama, openai, and google paths are
untouched.
shields.io's `visual-studio-marketplace/*` endpoints currently return
"404: badge not found", so the version and installs badges on the
extension's marketplace listing render as broken images. Switch to
`vsmarketplacebadges.dev`, which is the de-facto third-party
replacement most VS Code extensions fall back to.
Also surface the marketplace listings in the root README's top badge
row, alongside the existing MCP-install deep-link badges. The two
serve different purposes (the deep links register the MCP config
without an extension, the marketplace badges link to the extension
itself), so both stay.
Two follow-up review fixes on the interactive-graph webview surface:
- `loadGraphHtml(projectId)` now resolves the projectId-derived path
and verifies the result stays inside `GRAPH_DIR`. The
`socraticode.openInteractiveGraph` command accepts an arbitrary
argument from any caller (palette, sidebar, other extensions), so a
value like `../../etc/passwd` would otherwise escape the cache
directory via `path.join`. Suspicious projectIds are now rejected
with a log entry and the function returns `undefined`.
- `handleWebviewMessage` now opens the document first, then clamps
the requested line number against the document's actual `lineCount`
before constructing a `Range`. The previous `m.line > 0` check
prevented negatives but allowed absurdly large values (e.g.
`Number.MAX_SAFE_INTEGER`) that would build a Range past the end of
the file. Selection is set on the resolved editor.
Lint, typecheck, manifest tests and build all clean.
A pass over the extension surface to address review feedback:
Safety / hardening:
- `graphPanel.ts`: validate `m.path` from the webview before opening
files. Reject absolute paths and any path that escapes the workspace
root (`..`, `/foo`, `C:/...`). Validate the line number is a positive
integer before constructing a `Range`. Surface failures via the output
channel rather than letting the rejection bubble up.
- `mcpProvider.ts`: defensively check that
`vscode.lm.registerMcpServerDefinitionProvider` exists before calling
it. The `engines.vscode: ^1.99.0` field already enforces this on
install, but some VS Code-derived editors mis-report their engine
version. The extension now degrades gracefully (sidebar, commands,
status bar still work) instead of failing activation.
- `commands.ts` and `graphPanel.ts`: wrap `workbench.action.chat.open`
in try/catch. Not every VS Code-compatible editor exposes that
command; falling back to the output channel avoids unhandled
rejections after the user clicked "Open chat".
- `extension.ts`: persist the first-run walkthrough flag only after the
walkthrough command resolves successfully, so a transient failure
doesn't silently skip the onboarding forever.
CI gates:
- `extension-ci.yml` and `extension-release.yml`: run `npm test` between
typecheck and build, so manifest-level smoke regressions can't slip
through to either the PR artefact or the marketplace publishes.
Settings copy:
- `socraticode.env` description: explicitly call out that the setting
is for non-secret config only. Recommend OS environment variables /
local `.env` files for API keys, since workspace settings can sync
via Settings Sync and end up in committed `.vscode/settings.json`.
Quality of life:
- `sidebar.ts` `formatRelative`: clamp the computed seconds to zero so
a file mtime slightly ahead of the local clock doesn't render
"-5s ago".
- `walkthroughs/first-index.md`: corrected the embedding model name
(`nomic-embed-text`, not `mxbai-embed-large`) to match the engine
default in `src/constants.ts`.
Lint / docs:
- `extension/README.md`: hyphenate "Eclipse Theia-based editors".
- `DEVELOPER.md`: add `text` language hint to the directory-tree code
fence (markdownlint MD040). Updated the inline comment for
`settings.ts` to reflect its current shape.
- `README.md`: reflow the "extension vs plugin" callout into a single
blockquote (markdownlint MD028).
Lint, typecheck, manifest tests and build all clean. Engine unit tests
unaffected (706/706 still pass).
Marketplace URL becomes giancarloerra.socraticode (VS Code Marketplace)
and giancarloerra/socraticode (Open VSX), matching the registered
publisher account. The Altaire sponsor link and license contact email
stay as they are; only the publisher slug and the corresponding
shields.io / marketplace URLs in the README change.
Two small additions to the marketplace README:
- Discord badge in the header row, next to GitHub stars. Static
shields.io badge linking to the community Discord. The auto-updating
member-count variant can replace it later if we publish the server
ID.
- New short "SocratiCode Cloud (private beta)" section between
Compatibility and Privacy. Mirrors the wording on socraticode.cloud:
managed infrastructure, webhook-driven indexing, shared team
indexes, SSO/SAML, audit logs, SOC 2 / ISO 27001-aligned controls,
private beta. Request-access link only; no in-extension Cloud
feature is implied.
Marketplace listing now points readers at both the community channel
and the hosted edition without overstating either.
The marketplace listing page is the single biggest conversion surface
for the extension. This pass turns the README into something that
reads as a polished product page, not an in-house engineering note.
Headline changes:
- Editor-neutral framing throughout. The same `.vsix` ships to VS
Code, Cursor, VSCodium, Gitpod, code-server, Theia, Antigravity,
and Particle Workbench. The previous wording was VS Code-first
(Copilot agent mode listed first, "VS Code's MCP host", title with
"for VS Code"). Replaced with editor-agnostic language: "any
MCP-compatible chat or agent in your editor", followed by named
surfaces (Copilot agent mode, Cursor's Agent / Composer, the
Gemini surface in Antigravity, Cline, Continue, Roo Code, others).
- Hero image referenced via raw GitHub URL (absolute, survives both
marketplace renderers).
- Badge row: VS Code Marketplace version + installs, Open VSX
version + downloads, GitHub stars, npm engine version, license.
Marketplace badges 404 until first publish; that's expected.
- Prominent "Full documentation, configuration reference, and
benchmarks on GitHub" link directly under the badges. The extension
README is intentionally short; the engine README is the deep doc.
- Headline benchmark callout up top: 61% less context, 84% fewer
tool calls, 37x faster on the 2.45M-line VS Code codebase, with a
direct anchor link to the full benchmark.
- New "Why bigger teams pick it" section covering refactor safety
on monorepos, multi-repo orgs, tool-independence, air-gapped
deployment, AGPL-3.0 transparency, and the 18+ language list.
- Compatibility section now lists every editor and every AI surface
explicitly, so a Cursor or Antigravity user can recognise their
setup without reading between the lines.
- Settings table simplified around the four real settings, with
`socraticode.env` documented as the single passthrough for every
engine knob (external Qdrant, embedding providers, project IDs,
branch-aware indexing, linked projects).
Manifest tweak:
- Add `galleryBanner` (`#3a3d8c` brand colour, dark theme) so the
Marketplace listing's hero strip matches the icon and the project
website rather than defaulting to grey.
Build, lint, typecheck and the manifest smoke tests are all clean.
Packaged size 216 KB.
README updates:
- Plugins table now includes a row for the VS Code / Open VSX extension,
with a list of every editor that pulls from one of the two registries
(Cursor, VSCodium, Gitpod, code-server, Theia, Antigravity, Particle
Workbench).
- Cursor Marketplace listing mentioned alongside the existing
`/add-plugin` URL install.
- A short "extension vs plugin" callout explaining when to install
which: the extension auto-registers MCP and adds native UI, the
plugin formats add skills + agent rules. Both can coexist.
DEVELOPER.md updates:
- New "VS Code / Open VSX Extension" section covering layout, local
development (F5 to launch an Extension Development Host), build /
lint / test / package commands, publishing flow, and versioning
policy (extension tracks the engine version via the bump script).
- Explicit list of what the extension is NOT (no re-implementation of
search, no shipped engine, no language-server features). Keeps scope
honest and prevents future feature creep.
- Added the new section to the table of contents.
`extension-ci.yml`: triggers on push and PR to `main` whenever anything
in `extension/` changes. Runs lint, typecheck, build, and `vsce
package`, then uploads the resulting `.vsix` as an artefact for manual
smoke testing (download from the Actions run, install via "Extensions:
Install from VSIX..." in any VS Code-compatible editor).
`extension-release.yml`: triggers on `v*` tags so engine release tags
fire the extension release in lockstep, with manual dispatch as a
fallback. Builds the `.vsix`, then publishes to:
- VS Code Marketplace via `vsce publish` (using the `VSCE_PAT` repo
secret).
- Open VSX Registry via `ovsx publish` (using the `OVSX_PAT` repo
secret).
When triggered by a tag, also attaches the `.vsix` to the GitHub
release so users on Open VSX-less editors can grab it directly.
Both workflows are scoped to the `extension/` working directory so
engine-only changes don't trigger them, and they rely on the
extension's own `package-lock.json` for npm caching. Permissions
follow least-privilege: workflow-level `contents: read`, with
`contents: write` granted only to the publish job for the GitHub
release upload step.
A new top-level `extension/` package containing the SocratiCode VS Code
extension. The same `.vsix` artefact ships to both VS Code Marketplace
and Open VSX, which means it installs cleanly in Cursor, VSCodium,
Gitpod, code-server, Eclipse Theia editors, Google Antigravity, and
Particle Workbench in addition to stock VS Code.
What it does:
- Auto-registers the SocratiCode MCP server in VS Code's MCP host via
`vscode.lm.registerMcpServerDefinitionProvider` (VS Code 1.99+).
Copilot agent mode, Cline, Continue, Roo Code and any other
MCP-aware client see SocratiCode's tools without the user editing
any `.vscode/mcp.json`.
- Forwards the user's `socraticode.env` setting to the engine
subprocess unchanged. Power users point at an external Qdrant
(`QDRANT_MODE=external` + `QDRANT_URL` + `QDRANT_API_KEY`), pick an
embedding provider, or set any other engine knob exactly as
documented in the engine README.
- Activity Bar sidebar with "Indexed projects" tree view and welcome
content. Discovers projects by listing graph artefacts the engine
writes to `os.tmpdir()/socraticode-graph/`.
- Webview panel for the interactive graph (`graphPanel.ts`). Reads
the engine's self-contained HTML, wraps it with a tight CSP, injects
a bridge script so node-clicks can `postMessage` back to the
extension and open files in the editor.
- Status-bar item showing "SocratiCode" with click-to-open-sidebar.
Honours the `socraticode.statusBar` setting.
- Two-step getting-started walkthrough (index your project, try
search and the interactive graph). Shown automatically on first
install; re-openable via the command palette.
- Command palette commands: index workspace, open graph, refresh
projects, open walkthrough, show output.
- Output channel for engine logs.
Build: esbuild bundles `src/extension.ts` to `dist/extension.js` (CJS,
node18, sourcemap), `vscode` external. Biome lint and `tsc` strict
mode both clean. Manifest smoke tests via Node's built-in test runner
catch package.json regressions before activation. Packaged size:
~215 KB `.vsix`.
Versioning: extension version tracks the engine version (currently
`1.7.2`). Patch drift is allowed for extension-only hotfixes. The
`scripts/bump-plugin-versions.mjs` hook from the previous commit
already includes `extension/package.json` in its bump list, so future
engine releases keep the extension in lockstep automatically.
The Cursor and Codex plugin manifests had drifted from the engine
version because the previous `release-it` `after:bump` hook only
updated `.claude-plugin/plugin.json`. Replace the inline-JS hook with
`scripts/bump-plugin-versions.mjs`, which iterates every known plugin
manifest (Claude / Cursor / Codex plus the upcoming
`extension/package.json`) and skips any that don't yet exist. This
keeps every distribution channel in lockstep on subsequent releases.
Bring `.cursor-plugin/plugin.json` and `.codex-plugin/plugin.json` up
to v1.7.2 so they reflect the current engine release. Tidy up the
plugin descriptions in the same pass (replace decorative dashes with
colons, no semantic change).
Address CodeRabbit findings on PR #36:
1. The previous patch attached `QDRANT_API_KEY` as an `api-key` header
regardless of URL scheme, which would leak the secret on the wire if
a user configured an authenticated Qdrant over plain HTTP. Add a
guard that rejects the combination and throws a specific error,
placed before the readiness probe so its message is not masked by
the generic "Cannot reach external Qdrant" handler. Loopback URLs
(`localhost`, `127.0.0.1`, `[::1]`) are accepted on `http://` so
local-dev workflows where users run authenticated Qdrant on plain
HTTP keep working. The URL is parsed (rather than checked with
startsWith) so hostnames like `http://localhost.evil.com` are not
mistaken for loopback.
2. The "omits api-key header when QDRANT_API_KEY is not set" test
relied on the spread of the real `constants.js` module, which means
it would flake to a header-attached state on any developer machine
with `QDRANT_API_KEY` exported in the shell. Pass
`QDRANT_API_KEY: undefined` explicitly so the override always wins.
3. Add two tests covering the new guard: one asserting the rejection
on plain HTTP for non-loopback hosts, and one asserting that the
localhost exception still attaches the api-key header.
4. Document the HTTPS requirement (and localhost exception) in the
`QDRANT_API_KEY` row of the README configuration table.
External Qdrant deployments that require authentication (notably Qdrant Cloud,
which returns 403 on every endpoint including /healthz without an api-key
header) cannot pass the existing readiness check, so codebase_index fails with
"Cannot reach external Qdrant" even though codebase_health and codebase_search
still succeed -- their qdrant-js client paths attach the key automatically.
Forward QDRANT_API_KEY as an `api-key` request header from
ensureExternalQdrantReady() by extending waitForService() with an optional
RequestInit. Docker-managed and unauthenticated external Qdrant deployments
remain unaffected (no init forwarded -> no headers).
Covered by two new unit tests in docker.test.ts.
The previous regex `^namespace\s+([\w.]+)` was anchored at column 0, so
nested namespace declarations (the `namespace Inner` block inside
`namespace Outer { ... }`) were silently missed. The capture group also
accepted invalid identifiers like `1Foo` and matched on stray
occurrences of the word `namespace` not followed by `;` or `{`.
The new regex `^\s*namespace\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*(?=[;{])`:
- `^\s*` allows leading whitespace, so indented (nested) declarations
are captured.
- The dotted-identifier capture requires each segment to start with a
letter or underscore, matching C# identifier rules.
- `(?=[;{])` requires a real terminator, filtering out spurious matches.
Adds three regression tests: nested-namespace capture, rejection of
digit-leading identifiers, and the terminator-lookahead behaviour.
Found by CodeRabbit on PR #34 (nitpick).
`buildCsNamespaceMap` previously iterated `fileSet` in fs.readdir()
traversal order, which POSIX does not guarantee. Combined with the
`candidates[0]` selection in `resolveImport`, this meant a `using`
directive could resolve to different files on different machines or
runs.
Sort the `.cs` paths lexically before scanning so candidate lists are
stable. Adds a regression test that builds the map twice from sets
populated in different orders and asserts both produce the same
candidate sequence and the same first candidate.
Found by CodeRabbit on PR #34.
C# imports previously always returned null because there is no direct
filename mapping for `using X.Y.Z;`. As a result `codebase_graph_query`
reported zero edges for C# projects, and the symbol-level tools
(impact, flow, symbol) silently lost cross-file resolution because they
walk the file-import graph for Tier 2/3 candidates.
Build a namespace-to-files map once per graph build by regex-scanning
every `.cs` file for `^namespace X.Y.Z` (covers both block-scoped and
file-scoped C# 10+ syntax) and consult it from the csharp branch of
resolveImport. When a namespace spans multiple files the first
candidate is returned; multi-file fan-out is tracked as a follow-up.
External namespaces (System.*, Microsoft.*) are still filtered upstream
by isExternalModule, so the map is only consulted for project-internal
names.
Cost is O(n) reads at graph-build time, gated by a `hasCs` precheck so
non-C# projects pay nothing.
Adds 12 unit tests covering block-scoped, file-scoped, multi-file,
multi-namespace-per-file, commented-out lines, non-.cs files, empty
projects, unknown namespaces, and external (System.*) filtering.
Symbol view (full rebuild)
──────────────────────────
Replaces the unusable "show all symbols at once" mode with a focus
graph (SourceTrail / IntelliJ pattern):
- Landing state shows the alphabetical list of all symbols in the
sidebar; canvas shows a "pick a symbol" overlay constrained to the
canvas area (right: 340px) so the list stays visible.
- Three entry paths to seed: list click, search bar, or symbol click
from a file's sidebar in Files view.
- Once seeded, canvas shows the symbol + its 2-hop callers/callees
neighbourhood (auto-falls back to depth 1 if > 60 nodes).
- Clicking any neighbour re-centres on it. Seed has a distinctive
orange ring + always-visible label so the anchor is obvious.
- "← Back to symbol list" link in the sidebar restores the empty
state — explicit way out instead of relying on accidental
empty-canvas clicks.
- Light file-grouping: each symbol's border colour is a stable hash
of its file path; symbols from the same file share a colour.
Layout dropdown clean-up
────────────────────────
Removed Force-directed (cose). Force-directed is the wrong algorithm
for code dependency graphs — hub clusters collapse, orphans fly off,
labels overlap, no tuning fixes the underlying shape mismatch. Tried
fcose as a substitute; same fundamental problem on dense code graphs.
Default is now Dagre TB; remaining options are Concentric, Breadth-
first, Grid, Circle — all deterministic.
UX polish
─────────
- autoungrabify: true — disables single-click drag, fixes "node jumps
on click" (trackpad clicks always have some motion which Cytoscape's
default interprets as a node grab).
- Tap-to-highlight neighbourhood — clicking any node highlights its
direct neighbours and fades the rest. Less aggressive than the
transitive blast-radius / call-flow buttons in the sidebar.
- Zoom-bound label visibility — labels hidden below zoom 0.55 so 100+
node graphs aren't a soup of overlapping text. Selected / highlighted
nodes always show their label.
- Layout-position persistence — switching Files ↔ Symbols and back
preserves Files-view positions instead of re-running the layout.
TDZ regression test
───────────────────
New tests/unit/viewer-app.test.ts runs the bundled viewer-app.js in a
sandboxed node:vm context with mocked DOM + Cytoscape, triggering
every function-typed style closure on a fake element. Catches TDZ
("Cannot access X before initialization") and other reference errors
at unit-test time. Would have caught both the prior `cy` and
`LABEL_ZOOM_THRESHOLD` ordering bugs before they reached the browser.
Stats-line consistency
──────────────────────
graph-visualize-html.ts — top bar now displays embedded counts
(sym.symbols.length / sym.symbolEdges.length) instead of meta counts.
Previously the top bar said "630 symbols" while the sidebar's "All
symbols" said "722" because meta excludes synthetic <module>
placeholders. Capped mode keeps meta counts with a "(capped)"
qualifier since the embedded set is empty there.
Quality gates
─────────────
- Biome lint: clean
- TypeScript (tsc): clean
- Unit tests: 687/687 (incl. the new viewer-app.js evaluation test)
- Add "Portable, AI- and host-agnostic" paragraph in the intro that
bundles two claims: the codebase context lives with the code (works
with any assistant / IDE / CLI, any combination), and pre-computed
reasoning (blast radius, call-flow, dependency traversal) lets
smaller / cheaper models hold their own on architectural questions.
- Add three rows to the "Built-in Code Search vs SocratiCode"
comparison table: symbol-level impact / blast radius, call-flow
tracing, interactive visual graph explorer.
- Plugins section: chain Claude Code's two install commands with &&
so the row is a single copy-paste — the install step was missing.
String.prototype.replace with a string replacement argument interprets
`$&`, `$'`, `` $` ``, and `$$` as special tokens (match, post-match,
pre-match, literal-$). The vendored bundles contain the classic
regex-escape idiom `"\\$&"` (`cytoscape.min.js` and `dagre.min.js` each
have one), which the asset-injection pipeline was corrupting into e.g.
`"\\{{CYTOSCAPE}}"` in the generated HTML. The visible symptom was a
broken Cytoscape search-box escape path whenever a user typed a regex
metacharacter into the live-search input.
Fix: convert the six asset/data replacements in buildInteractiveGraphHtml
to function-replacer form (`() => assets.cytoscape`), which uses the
returned string literally with no `$`-token interpretation. Same class
of bug would have affected embedded JSON had a file path or symbol name
contained `$&` — now neutralised for the whole chain.
Regression test added: asserts the `\\$&` idiom survives verbatim in the
rendered HTML and that neither `\\{{CYTOSCAPE}}` nor `\\{{DAGRE}}`
appears. A single `expect(html).toContain('"\\\\$&"')` catches the bug.
Quality gates (all green):
- Biome lint: clean
- TypeScript (tsc --noEmit): clean
- Unit tests: 686/686 (was 685; +1 regression test)
- CodeRabbit: No findings
- Snyk: 0 issues
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>