npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gitkraken/agent-tools

v0.2.0

Published

Host configuration and path helpers for AI coding agent integrations, with an in-memory filesystem for testing.

Readme

@gitkraken/agent-tools

The current scaffold exports AgentHostConfig, AgentHostPaths, AgentOrigin, joinPath, and a testing MemoryFileSystem. Client detection, MCP and skill configuration, and hook configuration are planned for later layers.

The package is ports & adapters: it never touches the real filesystem, the environment, or a child process directly. AgentHostConfig describes the host-provided platform, paths, filesystem port, and optional process facts. joinPath joins path segments using / without importing node:path.

import { joinPath } from '@gitkraken/agent-tools';

const settingsPath = joinPath('/Users/me', '.config', 'settings.json');

Install

pnpm add @gitkraken/agent-tools

Host configuration types

Import the configuration types when implementing a host adapter:

import type { AgentHostConfig } from '@gitkraken/agent-tools';

const config: AgentHostConfig = {
	platform: 'darwin', // 'darwin' | 'linux' | 'win32'
	paths: {
		home: '/Users/me',
		configHome: '/Users/me/.config', // resolved $XDG_CONFIG_HOME
		appSupport: '/Users/me/Library/Application Support',
		// claudeConfigDir, copilotHome, grokHome, piAgentDir: resolved env overrides, when set
	},
	fs: myFileSystemPort, // @gitkraken/shared-tools FileSystemPort
	resolveExecutable: (command) => myPathLookup(command), // optional: a client's binary on PATH
	openCodeMajor: 2, // optional: the installed OpenCode major version (0/undefined = unknown); does not select the MCP layout
};

MCP install/uninstall

import { getMcpCliInstallCommand, inspectMcpInstall, installMcp, uninstallMcp } from '@gitkraken/agent-tools';

// 1. Try the client's own CLI first, when it has one. `undefined` means go straight to step 2.
const request = { client: 'vscode', executablePath: '/usr/local/bin/gk', args: ['mcp', '--host=vscode'] } as const;
const argv = await getMcpCliInstallCommand(request, config);
if (argv) {
	const ok = await myHost.run(argv); // your own process spawn
	if (ok) return;
}

// 2. Fall back to a direct, comment-preserving config-file write.
const installed = await installMcp(request, config);
// { client, path, change: 'created' | 'updated' | 'unchanged', format, location, entry, removedLocations }

// Drift check, e.g. for a status command:
const status = await inspectMcpInstall(request, config);
// { state: 'missing' | 'drifted' | 'current', reason?, key?, entry?, path, location }

const removal = await uninstallMcp({ client: 'vscode' }, config);
// { client, removed, files: [{ path, change, removedLocations, reason? }] }

installMcp/uninstallMcp/inspectMcpInstall accept an optional configPath to target a specific file directly instead of the client's resolved config path.

getMcpCliInstallCommand returns undefined when a shortcut cannot do the job: the client has none, a configPath is set, the client is Cursor (its config file is always written directly), or Codex's config still holds the legacy ['mcp_servers.GitKraken'] table. installMcp and uninstallMcp throw unsupported client for an unknown id (installMcp also for the uninstall-only gemini), and throw without writing when a config file does not parse (inspectMcpInstall reports invalid-config instead). A leading UTF-8 BOM is accepted and kept.

Re-running installMcp when the entry is already in place writes nothing and reports change: 'unchanged'. inspectMcpInstall says why an entry is not current (reason, and the drifting key) and exposes the on-disk entry, so a host can carry user-added args forward before reinstalling.

OpenCode layouts

OpenCode reads MCP servers from /mcp/<name> (v1) or /mcp/servers/<name> (v2). Omitting openCode writes v2 and prunes a v1 entry, as in 0.1.1. A host that probed the installed OpenCode should pass a policy on every OpenCode call (inspect before and after repair, and install). The library never runs the probe or compares versions:

// Verified earlier-v1 OpenCode -> v1; verified >= 1.18.24, or a v2 client -> v2.
const policy = { mode: 'authoritative', layout: 'v1' } as const;
// Probe failed or unparseable: follow what the config already uses, never prune.
const unknown = { mode: 'infer-existing' } as const;

await installMcp({ ...request, openCode: policy }, config);
generateConfigEntry('opencode', '/usr/local/bin/gk', ['mcp'], { openCode: policy });
// { type: 'local', command: ['/usr/local/bin/gk', 'mcp'], enabled: true }   (v2 uses `disabled: false`)
clientJSONPath('opencode', { openCode: policy }); // '/mcp/GitKraken'
  • authoritative writes the selected layout and removes the other layout's GitKraken entry. infer-existing follows the layout the config already uses and repairs that entry in place; with both layouts present it selects v1, because OpenCode's compatibility reader retains it over the native entry. It leaves the duplicate alone. It never authorizes pruning, even if you copy the reported openCodeLayout elsewhere; pass infer-existing unchanged through both inspections and the install.
  • infer-existing throws OpenCodeMcpLayoutUnknownError when there is no entry to follow, and in the read-free helpers (generateConfigEntry, clientJSONPath), which cannot read the config. To generate an entry without a probe result, choose a documented fallback yourself, e.g. { mode: 'authoritative', layout: 'v1' }; generating performs no write or cleanup. A non-object GitKraken entry is treated as corruption and refused, not skipped. A malformed policy throws TypeError.
  • inspectMcpInstall compares the selected layout; an entry found only in the other layout is drifted / legacy-location with that entry exposed for argv repair, and a current entry with a prunable duplicate is drifted / legacy-location with the selected entry. With infer-existing the compared entry is the v1 one when both layouts are present. openCodeDuplicateLocation reports the pointer of the other layout's entry whenever both hold one, in either mode; it is never pruned by infer-existing. infer-existing with no entry in either layout (including a missing or blank file) is missing / layout-unknown, the case where installMcp would throw OpenCodeMcpLayoutUnknownError. Argv merging stays in the host: re-run inspectMcpInstall with the effective argv before installMcp.
  • A migration writes the selected entry first and the removal second. If the removal fails (or the file changed in between), installMcp throws OpenCodeMcpCleanupError; error.result describes what was committed, with no removedLocations. This is not success, since an older reader can still reject a leftover native servers object. Run the install again to finish. AbortError is rethrown unchanged.
  • Refused without writing: a non-object root, mcp or mcp.servers; a v2 write where mcp.servers is a flat server named servers (an object whose type or enabled is a scalar, array or null). inspectMcpInstall reports these same files as missing / unsafe-config, so a host can tell before installing. Custom fields inside a replaced GitKraken entry are not carried across layouts.
  • Safe migration needs your FileSystemPort.writeFile to leave the previous file intact when it rejects (write a temp file, then rename). Your readFile must also return exactly the bytes writeFile was given (no newline normalization, appended trailing newline, or BOM changes): the migration compares the re-read file against what it wrote before the destructive second write, and a port that does not round-trip turns the migration into OpenCodeMcpCleanupError (retrying completes it). The library cannot check this and holds no lock against concurrent writers.
  • uninstallMcp ignores the policy and removes both pointers, except that a GitKraken key on a flat server named servers is not a v2 entry and is left alone; configPath confines every read and write to that file.

Codex TOML edits preserve surrounding text and comments by splicing table blocks. When an entry needs to change but its valid TOML representation cannot be edited safely this way (for example, a drifted inline or dotted mcp_servers.GitKraken assignment), install or uninstall throws an error naming the config path and leaves the file untouched. A semantically current entry remains a no-op even when it uses an unusual representation.

JSON/JSONC config files are edited losslessly via jsonc-parser (comments and formatting outside the edited range survive); Codex's TOML config is edited by splicing raw text, the same way gkcli does, because a full parse/marshal round trip drops comments and reorders sibling tables.

Hook install/uninstall

import { HookConfigError, installHooks, uninstallHooks } from '@gitkraken/agent-tools';

try {
	const installed = await installHooks({ host: 'codex', executablePath: '/usr/local/bin/gk' }, config);
	// { host, path, events, blockingEvents, files: [{ path, change: 'created' | 'updated' | 'unchanged' | 'removed' }] }

	const removed = await uninstallHooks('codex', config, ['PreToolUse']);
	// { host, path, removedEvents, remainingEvents, files, reason?: 'file-missing' | 'not-installed' }
} catch (error) {
	if (error instanceof HookConfigError) {
		console.error(error.code, error.path); // e.g. 'invalid-json', 'wrong-shape', 'unmanaged-file'
	}
}

Only GitKraken-owned hooks are added or removed; other entries and keys in the hooks file are preserved, and a file that is not valid JSON (or, for the OpenCode and Pi relays, not written by GitKraken) is refused without being modified.

Client detection

import { allClients, clientDisplayName, detectClient, getClientInstallation } from '@gitkraken/agent-tools';

for (const client of allClients()) {
	// 15 installable clients; 'gemini' is retained separately as legacy, uninstall-only.
	const detection = await detectClient(client, config); // { detected, source?: 'executable' | 'config-file' | 'config-dir', reason?, checkedPaths, ... }
	const installation = await getClientInstallation(client, config); // { installed, paths }: the files that carry a GitKraken entry
	console.log(clientDisplayName(client), detection.detected, installation.installed);
}

Skills

import { installSkill, SkillConflictError, uninstallSkill } from '@gitkraken/agent-tools';

const request = { agent: 'cursor', name: 'commit-composer', files: { 'SKILL.md': skillMarkdown } } as const;

// action: 'create' | 'replace' | 'unchanged'. path is the skill directory; files lists each content file
// and the .gk-skill.json manifest as created, updated or unchanged (empty when nothing needed writing).
const { action, path, files, status } = await installSkill(request, config);

try {
	// action: 'remove' | 'absent'. files lists what was removed, including the directory once it was left empty.
	await uninstallSkill(request, config);
} catch (error) {
	// Modified or foreign content requires `force`. Errors carry `code`, `operation` and `path`.
	if (error instanceof SkillConflictError && error.code === 'modified') {
		await uninstallSkill(request, config, { force: true });
	}
}

status is the skill's state before the action: state is 'missing' | 'current' | 'older-release' | 'modified', reason is a kebab-case code (for example 'content-changed' or 'unowned') and is absent when the skill is current and clean, and path is the skill directory.

The host supplies bundled skill content. The library records ownership in .gk-skill.json through the injected filesystem port and protects modified files during install and uninstall; see ARCHITECTURE.md.

Testing

import { MemoryFileSystem } from '@gitkraken/agent-tools/testing';

const fs = new MemoryFileSystem({ '/Users/me/.cursor/mcp.json': '{}' });