🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

jamgate

Package Overview
Dependencies
Maintainers
1
Versions
37
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

jamgate - npm Package Compare versions

Comparing version
0.8.0
to
0.9.0
+12
-3
dist/index.js

@@ -220,5 +220,14 @@ #!/usr/bin/env node

`Not saved — "${result.memory.text}" looks like a semantic duplicate of an ` +
`existing memory: ${near}. If it is genuinely the same fact, nothing to do. If it ` +
`is a distinct fact or an update, re-save with a \`subject\` so the gate treats it ` +
`as its own memory (or a time-aware update of that subject).`;
`existing memory: ${near}. If it is genuinely the same fact, nothing to do. ` +
// The advice has to match what the caller actually sent. Since 0.8.0 this branch
// is reachable WITH a subject (D-044), and telling an agent that just supplied
// one to "re-save with a subject" reads as the gate ignoring its input. Point it
// at the existing memory's subject, which is the thing that would actually work.
(result.memory.subject
? `If it is an UPDATE to that memory, re-save it with the EXISTING memory's ` +
`subject (shown above) rather than "${result.memory.subject}" — supersession ` +
`matches on the subject string, so a different spelling reads as a different ` +
`topic. If it is a genuinely distinct fact, say so and re-save it.`
: `If it is a distinct fact or an update, re-save with a \`subject\` so the gate ` +
`treats it as its own memory (or a time-aware update of that subject).`);
}

@@ -225,0 +234,0 @@ else {

+12
-1

@@ -10,3 +10,14 @@ import { execFile } from "node:child_process";

const execFileAsync = promisify(execFile);
const VALID_CLIENTS = ["claude-code", "claude-desktop", "cursor", "windsurf"];
const VALID_CLIENTS = [
"claude-code",
"claude-desktop",
"cursor",
"windsurf",
"gemini-cli",
"vscode",
"cline",
"roo",
"opencode",
"zed",
];
/** Parse `setup` flags: `--dry-run`, `--remote <url>`, `--token <token>`, `--client <id>...`. */

@@ -13,0 +24,0 @@ export function parseSetupArgs(argv, env) {

@@ -6,2 +6,8 @@ import { join } from "node:path";

}
/** The XDG config base (`$XDG_CONFIG_HOME` or `~/.config`), used by the CLIs/editors that
* follow the spec on macOS + Linux (Gemini, OpenCode, Zed). */
function xdgConfig(env) {
const h = home(env);
return env.XDG_CONFIG_HOME || (h ? join(h, ".config") : null);
}
/** Resolve the base config directory used by Claude Desktop, per platform. */

@@ -17,5 +23,28 @@ function claudeDesktopDir(platform, env) {

// Linux and everything else follow the XDG base-dir spec.
const base = env.XDG_CONFIG_HOME || (h ? join(h, ".config") : null);
const base = xdgConfig(env);
return base ? join(base, "Claude") : null;
}
/** VS Code's per-user data directory (`.../Code/User`), where the user-level `mcp.json` lives
* and under which VS Code extensions (Cline, Roo) keep their `globalStorage`. Stable "Code"
* only — Insiders/VSCodium use a different folder and are left to manual config. */
function vscodeUserDir(platform, env) {
const h = home(env);
if (platform === "darwin") {
return h ? join(h, "Library", "Application Support", "Code", "User") : null;
}
if (platform === "win32") {
return env.APPDATA ? join(env.APPDATA, "Code", "User") : null;
}
const base = xdgConfig(env);
return base ? join(base, "Code", "User") : null;
}
/** Zed's config base: `~/.config/zed` on macOS + Linux (Zed uses XDG even on macOS),
* `%APPDATA%\Zed` on Windows. */
function zedDir(platform, env) {
if (platform === "win32") {
return env.APPDATA ? join(env.APPDATA, "Zed") : null;
}
const base = xdgConfig(env);
return base ? join(base, "zed") : null;
}
export const CLIENTS = [

@@ -26,2 +55,4 @@ {

serverKey: "jamgate",
containerKey: "mcpServers",
shape: "claude-code",
supportsRemote: true,

@@ -43,2 +74,4 @@ configPath(_platform, env) {

serverKey: "jamgate",
containerKey: "mcpServers",
shape: "plain",
// Claude Desktop installs local (stdio) servers natively; remote servers go through the

@@ -60,2 +93,4 @@ // connectors UI / mcp-remote, not a plain HTTP entry, so we don't claim remote support.

serverKey: "jamgate",
containerKey: "mcpServers",
shape: "plain",
supportsRemote: true,

@@ -75,5 +110,7 @@ configPath(_platform, env) {

serverKey: "jamgate",
// Windsurf's remote support is SSE-based (`serverUrl`) rather than Streamable HTTP; rather
// than write an entry we haven't verified round-trips, we wire stdio only.
supportsRemote: false,
containerKey: "mcpServers",
// Windsurf (Cascade) now officially documents Streamable HTTP for remote servers, using its
// own `serverUrl` field + a `headers` object (D-046). We wire that in remote mode.
shape: "windsurf",
supportsRemote: true,
configPath(_platform, env) {

@@ -88,2 +125,114 @@ const h = home(env);

},
{
id: "gemini-cli",
label: "Gemini CLI",
serverKey: "jamgate",
containerKey: "mcpServers",
// Remote is Streamable HTTP via `httpUrl` (Gemini reserves plain `url` for SSE).
shape: "gemini",
supportsRemote: true,
// settings.json is Gemini's whole CLI config, not an MCP-only file.
sharedConfig: true,
configPath(_platform, env) {
const h = home(env);
return h ? join(h, ".gemini", "settings.json") : null;
},
detectPaths(_platform, env) {
const h = home(env);
return h ? [join(h, ".gemini")] : [];
},
},
{
id: "vscode",
label: "VS Code (Copilot)",
serverKey: "jamgate",
// VS Code's container key is `servers`, not `mcpServers`, and every entry carries a `type`.
containerKey: "servers",
shape: "vscode",
supportsRemote: true,
configPath(platform, env) {
const dir = vscodeUserDir(platform, env);
return dir ? join(dir, "mcp.json") : null;
},
detectPaths(platform, env) {
const dir = vscodeUserDir(platform, env);
return dir ? [dir] : [];
},
},
{
id: "cline",
label: "Cline",
serverKey: "jamgate",
containerKey: "mcpServers",
// Remote transport tag is the camelCase `streamableHttp` (NOT Roo's hyphenated form).
shape: "cline",
supportsRemote: true,
configPath(platform, env) {
const dir = vscodeUserDir(platform, env);
return dir
? join(dir, "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
: null;
},
detectPaths(platform, env) {
const dir = vscodeUserDir(platform, env);
return dir ? [join(dir, "globalStorage", "saoudrizwan.claude-dev")] : [];
},
},
{
id: "roo",
label: "Roo Code",
serverKey: "jamgate",
containerKey: "mcpServers",
// Remote transport tag is the hyphenated `streamable-http` (NOT Cline's camelCase form).
shape: "roo",
supportsRemote: true,
configPath(platform, env) {
const dir = vscodeUserDir(platform, env);
return dir
? join(dir, "globalStorage", "rooveterinaryinc.roo-cline", "settings", "mcp_settings.json")
: null;
},
detectPaths(platform, env) {
const dir = vscodeUserDir(platform, env);
return dir ? [join(dir, "globalStorage", "rooveterinaryinc.roo-cline")] : [];
},
},
{
id: "opencode",
label: "OpenCode",
serverKey: "jamgate",
// OpenCode nests servers under `mcp`, with a single `command` array and an `enabled` flag.
containerKey: "mcp",
shape: "opencode",
supportsRemote: true,
// opencode.json holds providers/models/etc., not just MCP servers.
sharedConfig: true,
configPath(_platform, env) {
const base = xdgConfig(env);
return base ? join(base, "opencode", "opencode.json") : null;
},
detectPaths(_platform, env) {
const base = xdgConfig(env);
return base ? [join(base, "opencode")] : [];
},
},
{
id: "zed",
label: "Zed",
serverKey: "jamgate",
// Zed's container key is `context_servers`; its custom-server entry is a bare {command,args}.
containerKey: "context_servers",
shape: "plain",
supportsRemote: true,
// settings.json is Zed's entire editor config (and commonly `//`-commented).
sharedConfig: true,
configPath(platform, env) {
const dir = zedDir(platform, env);
return dir ? join(dir, "settings.json") : null;
},
detectPaths(platform, env) {
const dir = zedDir(platform, env);
return dir ? [dir] : [];
},
},
];

@@ -96,30 +245,62 @@ export function clientById(id) {

}
/** The bearer `Authorization` header for the remote endpoint, or undefined when no token. */
function authHeaders(token) {
return token ? { Authorization: `Bearer ${token}` } : undefined;
}
/**
* Build the server entry to write for a given client and mode. The stdio entry runs the
* published package with `npx jamgate` so it always tracks the installed version; the remote
* entry points at the user's self-hosted HTTP endpoint with a bearer header. Claude Code gets
* an explicit `type` to match how its own `claude mcp add` records entries.
* entry points at the user's self-hosted HTTP endpoint with a bearer header. Each client's
* exact field names come straight from its official docs — see {@link EntryShape}.
*/
export function buildEntry(client, params) {
if (params.mode === "remote") {
if (!params.url)
throw new Error("remote mode requires a url");
const headers = {};
if (params.token)
headers.Authorization = `Bearer ${params.token}`;
const entry = { url: params.url };
if (Object.keys(headers).length > 0)
entry.headers = headers;
if (client.id === "claude-code")
entry.type = "http";
return entry;
const remote = params.mode === "remote";
if (remote && !params.url)
throw new Error("remote mode requires a url");
const headers = authHeaders(params.token);
switch (client.shape) {
case "claude-code":
if (remote)
return withHeaders({ type: "http", url: params.url }, headers);
// Match `claude mcp add`'s recorded shape exactly (type + empty env) so an entry it wrote
// is recognised as already-configured on re-run rather than read as a spurious diff.
return { command: "npx", args: ["jamgate"], type: "stdio", env: {} };
case "gemini":
// Streamable HTTP is `httpUrl` in Gemini CLI; plain `url` would select SSE instead.
if (remote)
return withHeaders({ httpUrl: params.url }, headers);
return { command: "npx", args: ["jamgate"] };
case "vscode":
if (remote)
return withHeaders({ type: "http", url: params.url }, headers);
return { type: "stdio", command: "npx", args: ["jamgate"] };
case "cline":
if (remote)
return withHeaders({ type: "streamableHttp", url: params.url }, headers);
return { command: "npx", args: ["jamgate"] };
case "roo":
if (remote)
return withHeaders({ type: "streamable-http", url: params.url }, headers);
return { command: "npx", args: ["jamgate"] };
case "opencode":
if (remote)
return withHeaders({ type: "remote", url: params.url, enabled: true }, headers);
return { type: "local", command: ["npx", "jamgate"], enabled: true };
case "windsurf":
// Windsurf's remote URL field is `serverUrl`, not `url`.
if (remote)
return withHeaders({ serverUrl: params.url }, headers);
return { command: "npx", args: ["jamgate"] };
case "plain":
default:
if (remote)
return withHeaders({ url: params.url }, headers);
return { command: "npx", args: ["jamgate"] };
}
const entry = { command: "npx", args: ["jamgate"] };
// Claude Code records stdio servers as {type, command, args, env}. Match that shape exactly
// so an entry written by `claude mcp add` is recognised as already-configured on re-run
// (otherwise the empty `env` would read as a spurious diff and re-"update" every run).
if (client.id === "claude-code") {
entry.type = "stdio";
entry.env = {};
}
}
/** Attach a `headers` object to an entry only when there is one (keeps token-less remote entries
* free of an empty `headers` key so they stay byte-identical across re-runs). */
function withHeaders(entry, headers) {
if (headers)
entry.headers = headers;
return entry;

@@ -126,0 +307,0 @@ }

@@ -32,9 +32,11 @@ function isPlainObject(value) {

* @param entry The desired server entry.
* @param container The top-level key the servers live under ("mcpServers" for most clients;
* "servers"/"context_servers"/"mcp" for VS Code/Zed/OpenCode).
*/
export function planMerge(existing, key, entry) {
export function planMerge(existing, key, entry, container = "mcpServers") {
const hadValidRoot = isPlainObject(existing);
const root = hadValidRoot ? { ...existing } : {};
const hadServers = isPlainObject(root.mcpServers);
const hadServers = isPlainObject(root[container]);
const servers = hadServers
? { ...root.mcpServers }
? { ...root[container] }
: {};

@@ -51,4 +53,4 @@ const current = servers[key];

servers[key] = entry;
root.mcpServers = servers;
root[container] = servers;
return { config: root, status, changed: true };
}

@@ -19,2 +19,8 @@ import { promises as fs } from "node:fs";

export const BACKUP_SUFFIX = ".jamgate-backup";
/** Does a wired entry describe a remote (HTTP) transport? Remote entries carry a URL field —
* `url`, or a client-specific alias (`httpUrl` for Gemini, `serverUrl` for Windsurf) — whereas
* stdio entries carry `command`. Used only for status reporting. */
function isRemoteEntry(entry) {
return "url" in entry || "httpUrl" in entry || "serverUrl" in entry;
}
async function pathExists(p) {

@@ -99,3 +105,15 @@ try {

const { existing, malformed, fileExists } = await readConfig(path);
const plan = planMerge(existing, client.serverKey, entry);
// Safety guard for SHARED config files (Gemini/Zed/OpenCode): if the file exists but doesn't
// parse as strict JSON (almost always `//` comments), we must NOT rewrite it — doing so would
// clobber the user's entire settings down to just our entry. Skip with a clear pointer instead.
// Dedicated MCP-only files (Cursor, Cline, …) keep the tolerant "rewrite malformed" behaviour,
// since there the backup already covers the only thing at risk.
if (client.sharedConfig && malformed && fileExists) {
return {
...base,
outcome: "skipped",
detail: `existing config isn't plain JSON (comments?) — add jamgate to ${client.containerKey} manually to keep it intact`,
};
}
const plan = planMerge(existing, client.serverKey, entry, client.containerKey);
if (plan.status === "already-configured") {

@@ -178,3 +196,3 @@ return { ...base, outcome: "already-configured" };

const servers = existing && typeof existing === "object" && existing !== null
? existing.mcpServers
? existing[client.containerKey]
: undefined;

@@ -186,3 +204,3 @@ const entry = servers && typeof servers === "object" && servers !== null

wired = true;
transport = "url" in entry ? "http" : "stdio";
transport = isRemoteEntry(entry) ? "http" : "stdio";
}

@@ -189,0 +207,0 @@ }

{
"name": "jamgate",
"version": "0.8.0",
"version": "0.9.0",
"mcpName": "io.github.amirj4m/jamgate",

@@ -5,0 +5,0 @@ "description": "A neutral, cross-agent memory quality gate for AI agents, delivered as an MCP server — a gate, not a store.",

@@ -95,3 +95,4 @@ # Jamgate

One command detects the MCP clients installed on your machine (Claude Code, Claude Desktop,
Cursor, Windsurf) and wires Jamgate into each:
Cursor, Windsurf, Gemini CLI, VS Code / Copilot, Cline, Roo Code, OpenCode, Zed) and wires
Jamgate into each:

@@ -157,2 +158,74 @@ ```bash

**Gemini CLI** — add the same `mcpServers` block to `~/.gemini/settings.json`.
**Cline / Roo Code** — add the same `mcpServers` block to the extension's MCP settings file
(Cline: `.../globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`; Roo:
`.../globalStorage/rooveterinaryinc.roo-cline/settings/mcp_settings.json`), or use each
extension's "Configure/Edit MCP Servers" button.
**VS Code (Copilot)** — add to the user `mcp.json` (Command Palette → **MCP: Open User
Configuration**). VS Code uses a `servers` key and an explicit `type`:
```json
{
"servers": {
"jamgate": { "type": "stdio", "command": "npx", "args": ["jamgate"] }
}
}
```
**OpenCode** — add to `~/.config/opencode/opencode.json` under the `mcp` key (note the single
`command` array and `enabled` flag):
```json
{
"mcp": {
"jamgate": { "type": "local", "command": ["npx", "jamgate"], "enabled": true }
}
}
```
**Zed** — add to `settings.json` under `context_servers`:
```json
{
"context_servers": {
"jamgate": { "command": "npx", "args": ["jamgate"] }
}
}
```
#### Supported agents
`jamgate setup` auto-wires every agent below whose MCP config it can merge **losslessly** —
each entry shape is verified against the vendor's official docs. Agents whose config lives in a
non-JSON format we can't safely round-trip (TOML / YAML) are listed as **manual** with the
one-liner to add yourself.
| Agent | Config file | `setup` | Remote (`--remote`) |
| --- | --- | --- | --- |
| Claude Code | `~/.claude.json` | ✅ auto | ✅ |
| Claude Desktop | `claude_desktop_config.json` | ✅ auto | connectors UI |
| Cursor | `~/.cursor/mcp.json` | ✅ auto | ✅ |
| Windsurf | `~/.codeium/windsurf/mcp_config.json` | ✅ auto | ✅ |
| Gemini CLI | `~/.gemini/settings.json` | ✅ auto | ✅ |
| VS Code (Copilot) | `<Code>/User/mcp.json` | ✅ auto | ✅ |
| Cline | `.../saoudrizwan.claude-dev/settings/cline_mcp_settings.json` | ✅ auto | ✅ |
| Roo Code | `.../rooveterinaryinc.roo-cline/settings/mcp_settings.json` | ✅ auto | ✅ |
| OpenCode | `~/.config/opencode/opencode.json` | ✅ auto | ✅ |
| Zed | `~/.config/zed/settings.json` | ✅ auto | ✅ |
| Codex CLI | `~/.codex/config.toml` (TOML) | manual¹ | — |
| Goose | `~/.config/goose/config.yaml` (YAML) | manual¹ | — |
| Continue | `~/.continue/config.yaml` (YAML) | manual¹ | — |
¹ **Manual** — these use TOML/YAML; rather than risk mangling comments/formatting we don't
auto-edit them. Add Jamgate by hand: **Codex CLI** →
`[mcp_servers.jamgate]` with `command = "npx"` and `args = ["jamgate"]` in `~/.codex/config.toml`;
**Goose** → a `stdio` extension under `extensions:` with `cmd: npx` / `args: ["jamgate"]`;
**Continue** → an `mcpServers:` list entry with `command: npx` / `args: [jamgate]`.
> For agents that live in a shared, comment-friendly settings file (Gemini, OpenCode, Zed),
> `setup` will **skip** rather than overwrite a file it can't parse as strict JSON — so a
> `//`-commented `settings.json` is never clobbered; add the block by hand in that case.
Restart the agent. It now has three tools:

@@ -172,2 +245,21 @@

## Agent skill: `memory-discipline`
Wiring in the three tools gives an agent the *ability* to remember. The
**`memory-discipline`** skill teaches it the *habits* — recall before answering,
save one granular durable fact at a time with a specific reused `subject`, never send
secrets, and treat gate verdicts as answers rather than errors to retry. Its rules are
distilled straight from Jamgate's own [decision log](./DECISIONS.md) (D-040…D-045).
It ships in this repo at [`skills/memory-discipline/SKILL.md`](./skills/memory-discipline/SKILL.md)
as a portable [agentskills.io](https://agentskills.io) instruction pack, installable into
70+ coding agents with one command:
```bash
npx skills add amirj4m/jamgate
```
The skill is prompt text, not code — it is **not** part of the npm package (the
`files` whitelist ships only `dist`), so it never bloats the runtime install.
## Optional: local semantic search

@@ -361,3 +453,4 @@

- **Desktops (Claude Code, Cursor, Windsurf) — one command:**
- **Desktops (Claude Code, Cursor, Windsurf, Gemini CLI, VS Code, Cline, Roo Code, OpenCode,
Zed) — one command:**

@@ -369,3 +462,3 @@ ```bash

This wires every detected client on that machine to your instance (Streamable HTTP clients
only; others are skipped with a reason).
only; others — e.g. Claude Desktop — are skipped with a reason).

@@ -608,4 +701,5 @@ - **Phone (Claude app) and claude.ai in a browser:** Settings → **Connectors** → *Add custom

- **One-click install** — `npx jamgate setup` wires every detected client (Claude Code,
Claude Desktop, Cursor, Windsurf) in one idempotent, backup-first command, plus a Cursor
deeplink and a Claude Desktop `.mcpb` bundle.
Claude Desktop, Cursor, Windsurf, Gemini CLI, VS Code / Copilot, Cline, Roo Code, OpenCode,
Zed) in one idempotent, backup-first command, plus a Cursor deeplink and a Claude Desktop
`.mcpb` bundle.
- **Deploy your own** *(no terminal)* — a `Dockerfile`, a Render blueprint, and a Railway

@@ -612,0 +706,0 @@ config so a non-technical user can click a button, log into a platform, and get their own