This guide explains how to build, test, and contribute a community package to the Locus monorepo.
All community packages live inside the Locus repository under packages/ and
are published to npm through the project's automated release pipeline.
To add a new package, you prepare it in the repo and submit a pull request.
When a user runs locus install <name>, the CLI fetches the package from npm
and places it in ~/.locus/packages/. The user then invokes it via
locus pkg <name> [args...], which resolves the binary and forwards arguments.
Packages use the @locusai/sdk to read project config, invoke Locus
sub-commands, and log with consistent formatting.
All official Locus packages are scoped under @locusai and prefixed with
locus-:
| Short name | npm package name |
|---|---|
telegram |
@locusai/locus-telegram |
slack |
@locusai/locus-slack |
jira |
@locusai/locus-jira |
Users install packages by short name:
locus install telegram # resolves to @locusai/locus-telegram
locus install slack # resolves to @locusai/locus-slackEvery package lives in packages/<name>/ inside the monorepo. Use the
@locusai/locus-telegram package as the canonical reference.
packages/<name>/
βββ bin/
β βββ locus-<name>.js # Compiled binary (build output)
βββ src/
β βββ cli.ts # Entry point (delegates to index.ts)
β βββ index.ts # Main logic
βββ package.json
βββ tsconfig.json
βββ README.md
Every package must include these fields:
| Field | Type | Description |
|---|---|---|
name |
string |
Must be @locusai/locus-<name>. |
bin |
object |
Binary entry point. Key must be locus-<name>. |
files |
string[] |
Must include "bin", "package.json", "README.md". Without this, npm may not ship compiled code. |
keywords |
string[] |
Must include "locusai-package". This is how the packages page discovers your package on npm. Without it, your package won't appear in the marketplace. |
locus.displayName |
string |
Human-readable name shown in locus packages list. |
locus.description |
string |
One-line description for the package listing. |
locus.commands |
string[] |
Sub-commands contributed. Used in locus pkg <name> dispatch. |
locus.version |
string |
Semver version for the Locus integration. |
engines |
object |
Must require Node.js 18+. |
Never use workspace:* for dependencies that get published to npm. Use real
semver versions (e.g., "^0.21.13") for inter-package deps like @locusai/sdk.
The workspace: protocol is not resolved by npm publish.
Your package must expose a binary via the "bin" field. After installation, Locus
discovers it at ~/.locus/packages/node_modules/.bin/locus-<name>.
The binary should be a compiled JavaScript file with a shebang:
#!/usr/bin/env node
import { main } from "./index.js";
main(process.argv.slice(2)).catch((error) => {
console.error(`Fatal error: ${error.message}`);
process.exit(1);
});When invoked via locus pkg <name> [args...], the remaining args are forwarded
to your binary verbatim.
Add the SDK as a dependency (use a real semver version, not workspace:*):
"dependencies": {
"@locusai/sdk": "^0.21.13"
}import { readLocusConfig } from "@locusai/sdk";
// Reads ~/.locus/config.json + ./.locus/config.json and merges them.
const config = readLocusConfig();
console.log(config.github.owner); // "myorg"
console.log(config.ai.model); // "claude-sonnet-4-6"import { invokeLocus, invokeLocusStream } from "@locusai/sdk";
// Captured output (blocking)
const result = await invokeLocus(["run", "42"]);
if (result.exitCode !== 0) {
console.error("locus run failed:", result.stderr);
}
// Streaming (non-blocking)
const child = invokeLocusStream(["run", "42"]);
child.stdout?.on("data", (chunk: Buffer) => process.stdout.write(chunk));
child.on("exit", (code) => console.log("exited with", code));import { createLogger } from "@locusai/sdk";
const logger = createLogger("mypackage");
logger.info("Started");
logger.warn("Rate limit approaching", { remaining: 5 });
logger.error("Connection failed", { code: 503 });
logger.debug("Debug info", { detail: "value" }); // only shown with LOCUS_DEBUG=1Output uses the same prefix symbols as the Locus CLI (β, β , β, β―).
The fastest way to scaffold a new package:
locus create <name>
# or with a custom description:
locus create <name> --description "My awesome integration"This generates the full directory structure, package.json, tsconfig.json,
entry points, and README β all with correct naming and configuration.
After scaffolding, skip to step 6 below.
mkdir -p packages/<name>/src packages/<name>/binUse the template above. Set the name to @locusai/locus-<name> and fill in
the locus manifest.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"isolatedModules": true,
"resolveJsonModule": true,
"noEmit": true,
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "bin"]
}Create src/cli.ts as the entry point:
#!/usr/bin/env node
import { main } from "./index.js";
main(process.argv.slice(2)).catch((error) => {
console.error(`Fatal error: ${error.message}`);
process.exit(1);
});Create src/index.ts with your main logic:
import { createLogger, readLocusConfig } from "@locusai/sdk";
const logger = createLogger("<name>");
export async function main(args: string[]): Promise<void> {
const config = readLocusConfig();
logger.info(`Hello from locus-<name>! Repo: ${config.github.owner}/${config.github.repo}`);
// Your logic here
}Add your package's build to the root build:packages script:
"build:<name>": "cd packages/<name> && bun run build"And include it in the build:packages chain.
# Install dependencies
bun install
# Build your package
cd packages/<name> && bun run build
# Test it locally via symlink
mkdir -p ~/.locus/packages/node_modules/.bin
ln -s $(pwd)/bin/locus-<name>.js ~/.locus/packages/node_modules/.bin/locus-<name>
mkdir -p ~/.locus/packages/node_modules/@locusai/locus-<name>
ln -s $(pwd)/package.json ~/.locus/packages/node_modules/@locusai/locus-<name>/package.json
ln -s $(pwd)/bin ~/.locus/packages/node_modules/@locusai/locus-<name>/bin
# Run your package
locus pkg <name>Every package must have a README.md that explains:
- What the package does
- Setup and configuration steps
- Available commands and usage
- Any required environment variables or API keys
See packages/telegram/README.md for a complete example.
Once your package builds, typechecks, and works locally:
- Fork the Locus repository
- Create a feature branch:
git checkout -b feat/locus-<name> - Add a changeset:
bun changeset(select your new package, chooseminor) - Commit your changes and push
- Open a pull request against
master
The maintainers will review your package. Once merged, it will be published to npm automatically through the Changesets release pipeline.
The Telegram package (packages/telegram/) is the canonical example of a
community package. Study its structure for:
package.json: Correct naming,locusmanifest,filesfield, dependency versionssrc/cli.ts: Entry point pattern (delegates toindex.ts)src/index.ts: Main logic using the SDK- Build script: Uses
bun buildto compile to a single binary - README.md: Complete documentation with setup, usage, and configuration
- Package manager: Bun (the monorepo uses
bun@1.2.4) - Bundler:
bun build(compiles TypeScript to a single Node.js binary) - Linter: Biome β run
bun run lintandbun run format - TypeScript: 5.8+ β run
bun run typecheck - Releases: Changesets β automated via GitHub Actions
- Package lives in
packages/<name>/in the monorepo -
package.jsonname is@locusai/locus-<name> -
"locus"field present with all required keys (displayName,description,commands,version) -
"files"field includes"bin","package.json","README.md" -
"keywords"includes"locusai-package"(required for marketplace discovery) -
"bin"field points to./bin/locus-<name>.js - Dependencies use real semver versions (no
workspace:*) - Binary builds cleanly with
bun run build -
bun run typecheckpasses with no errors -
bun run lintpasses with no errors - Tested locally with
locus pkg <name> -
README.mdexplains setup, configuration, and usage - Changeset added via
bun changeset
{ "name": "@locusai/locus-<name>", "version": "0.21.13", "description": "Short description of what the package does", "type": "module", "bin": { "locus-<name>": "./bin/locus-<name>.js" }, "files": [ "bin", "package.json", "README.md" ], // Required: Locus package manifest "locus": { "displayName": "Human Name", "description": "One-line description shown in locus packages list", "commands": ["<name>"], "version": "0.1.0" }, "scripts": { "build": "bun build src/cli.ts --outfile bin/locus-<name>.js --target node", "typecheck": "tsc --noEmit", "lint": "biome lint .", "format": "biome format --write ." }, "dependencies": { "@locusai/sdk": "^0.21.13" }, "devDependencies": { "typescript": "^5.8.3" }, "keywords": [ "locusai-package", "locus" ], "engines": { "node": ">=18" }, "license": "MIT" }