the-local 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # the-local
2
+
3
+ Ship Claude Code **locals** (resident expert subagents) from npm packages into a
4
+ host TS/JS app — the TypeScript port of the [`the_local`](https://github.com/tylercschneider/the_local)
5
+ Ruby gem. A provider package commits its agent `.md` files; a host runs
6
+ `the-local install` to copy the aggregated set from its direct dependencies into
7
+ `.claude/agents/`, plus a delegation rule in `CLAUDE.md`.
8
+
9
+ > Status: early. Core install pipeline + CLI are in place and tested. See the
10
+ > open issues for remaining work (contract doc, npm publish, workspace support).
11
+
12
+ ## The model
13
+
14
+ - **Providers commit locals.** A package ships pre-rendered agent files at
15
+ `the-local/agents/<prefix>-<name>.md` and declares them in its `package.json`.
16
+ - **Hosts install verbatim.** `the-local install` reads the host's direct
17
+ `dependencies`, copies each provider's committed `.md` byte-for-byte into
18
+ `.claude/agents/`, and writes the delegation trigger into `CLAUDE.md`.
19
+ - **Direct-dependency scope.** Only the host's direct dependencies contribute
20
+ locals; transitive providers are filtered out.
21
+ - **Cross-language contract.** The agent `.md` format and the `CLAUDE.md`
22
+ `<!-- the_local:begin -->` / `<!-- the_local:end -->` markers are identical to
23
+ the Ruby gem's, so a Ruby gem and a TS package install into the same host
24
+ without clobbering each other. See [`docs/contract.md`](docs/contract.md).
25
+
26
+ ## Use (host app)
27
+
28
+ ```sh
29
+ npx the-local install # or: refresh
30
+ ```
31
+
32
+ ## Declare locals (provider package)
33
+
34
+ ```jsonc
35
+ // package.json
36
+ {
37
+ "the-local": {
38
+ "prefix": "keystone",
39
+ "scope": "UI — pages, forms, tables",
40
+ "agentsDir": "the-local/agents"
41
+ }
42
+ }
43
+ ```
44
+
45
+ Then commit `the-local/agents/keystone-*.md` alongside your code.
46
+
47
+ ## Develop
48
+
49
+ ```sh
50
+ pnpm install
51
+ pnpm test # vitest
52
+ pnpm typecheck
53
+ pnpm lint
54
+ pnpm build # emits dist/, including the the-local CLI
55
+ ```
@@ -0,0 +1,11 @@
1
+ export interface Agent {
2
+ prefix: string;
3
+ name: string;
4
+ description: string;
5
+ tools: string;
6
+ body: string;
7
+ knowledge?: string | string[];
8
+ }
9
+ export declare function qualifiedName(agent: Pick<Agent, "prefix" | "name">): string;
10
+ export declare function agentFilename(agent: Pick<Agent, "prefix" | "name">): string;
11
+ export declare function toMarkdown(agent: Agent): string;
package/dist/agent.js ADDED
@@ -0,0 +1,23 @@
1
+ export function qualifiedName(agent) {
2
+ return `${agent.prefix}-${agent.name}`;
3
+ }
4
+ export function agentFilename(agent) {
5
+ return `${qualifiedName(agent)}.md`;
6
+ }
7
+ export function toMarkdown(agent) {
8
+ const knowledge = Array.isArray(agent.knowledge)
9
+ ? agent.knowledge.join("\n\n")
10
+ : (agent.knowledge ?? "");
11
+ return [
12
+ "---",
13
+ `name: ${qualifiedName(agent)}`,
14
+ `description: ${agent.description}`,
15
+ `tools: ${agent.tools}`,
16
+ "---",
17
+ "",
18
+ agent.body,
19
+ "",
20
+ knowledge,
21
+ "",
22
+ ].join("\n");
23
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ export declare function run(argv: string[], hostDir: string): number;
3
+ export declare function main(argv: string[], cwd: string): Promise<number>;
4
+ export declare function isEntrypoint(moduleUrl: string, invokedPath: string | undefined): boolean;
package/dist/cli.js ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from "node:fs";
3
+ import { pathToFileURL } from "node:url";
4
+ import { installLocals } from "./installer.js";
5
+ import { buildProvider, scaffoldProvider } from "./provider.js";
6
+ const COMMANDS = new Set(["install", "refresh"]);
7
+ export function run(argv, hostDir) {
8
+ const command = argv[0] ?? "install";
9
+ if (!COMMANDS.has(command)) {
10
+ process.stderr.write(`the-local: unknown command "${command}" (expected install, refresh, provider, or build)\n`);
11
+ return 1;
12
+ }
13
+ const { providers, agents } = installLocals(hostDir);
14
+ process.stdout.write(`the-local: installed ${agents.length} agent(s) from ${providers.length} provider(s).\n`);
15
+ return 0;
16
+ }
17
+ // Dispatches every command. The provider-authoring commands run from a package
18
+ // directory; install/refresh fall through to `run`. Async because `build` loads
19
+ // the provider's config by dynamic import.
20
+ export async function main(argv, cwd) {
21
+ const [command, target] = argv;
22
+ if (command === "provider") {
23
+ const { config, created } = scaffoldProvider(target ?? cwd);
24
+ process.stdout.write(created
25
+ ? `the-local: scaffolded provider "${config.prefix}" — edit the-local.config.js, then run the-local build.\n`
26
+ : `the-local: the-local.config.js already exists; run the-local build to re-render.\n`);
27
+ return 0;
28
+ }
29
+ if (command === "build") {
30
+ const written = await buildProvider(target ?? cwd);
31
+ process.stdout.write(`the-local: rendered ${written.length} agent(s).\n`);
32
+ return 0;
33
+ }
34
+ return run(argv, cwd);
35
+ }
36
+ // Node sets `import.meta.url` to the module's real path, but leaves
37
+ // `process.argv[1]` as the symlink npm creates in `node_modules/.bin`. Resolve
38
+ // the symlink before comparing so the CLI still runs when invoked via `npx`.
39
+ export function isEntrypoint(moduleUrl, invokedPath) {
40
+ if (!invokedPath)
41
+ return false;
42
+ return moduleUrl === pathToFileURL(realpathSync(invokedPath)).href;
43
+ }
44
+ if (isEntrypoint(import.meta.url, process.argv[1])) {
45
+ void main(process.argv.slice(2), process.cwd()).then((code) => process.exit(code));
46
+ }
@@ -0,0 +1,2 @@
1
+ import type { Agent } from "./agent.js";
2
+ export declare const companionAgents: Agent[];
@@ -0,0 +1,49 @@
1
+ import { reference } from "./reference.js";
2
+ // the-local's own locals — the companion that makes the-local its own provider,
3
+ // dogfooding the same model every other provider uses. Each agent carries the
4
+ // reference verbatim as its knowledge; the rendered files live committed under
5
+ // the-local/agents/ and are what a host installs.
6
+ const PREFIX = "the-local";
7
+ export const companionAgents = [
8
+ {
9
+ prefix: PREFIX,
10
+ name: "info",
11
+ description: "Use to learn how the-local works — providers, the committed-.md install " +
12
+ "model, the delegation trigger, and the direct-dependency scope rule.",
13
+ tools: "Read",
14
+ body: "You explain how the-local works, answering only from the reference: " +
15
+ "providers ship committed locals, `the-local install` copies them verbatim " +
16
+ "into a host's .claude/agents/, the CLAUDE.md delegation trigger makes the " +
17
+ "host delegate, and only direct dependencies contribute. You make no changes.",
18
+ knowledge: reference,
19
+ },
20
+ {
21
+ prefix: PREFIX,
22
+ name: "install",
23
+ description: "Use to add the-local to a host app and set it up correctly.",
24
+ tools: "Bash, Read, Edit",
25
+ body: "You set the-local up in a host package or app, following the reference's " +
26
+ "install section exactly: add the-local to the host's dependencies, install, " +
27
+ "run `the-local install` to sync locals into .claude/agents/ and write the " +
28
+ "delegation trigger, and re-run it after dependency changes. You do not " +
29
+ "invent steps the reference does not list.",
30
+ knowledge: reference,
31
+ },
32
+ {
33
+ prefix: PREFIX,
34
+ name: "develop",
35
+ description: "Use PROACTIVELY to turn a package into a the-local provider — declaring " +
36
+ "the provider block, authoring the locals, and committing the rendered " +
37
+ "agents. MUST BE USED instead of wiring a provider by hand.",
38
+ tools: "Read, Write, Edit, Grep",
39
+ body: "You turn a package into a the-local provider following the reference's " +
40
+ "provider-author workflow: add the `the-local` block to package.json, author " +
41
+ "the standard locals (info, install, and a domain worker) with a guide as " +
42
+ "their knowledge, and render each to the-local/agents/. The deliverable is " +
43
+ "the committed, shipped the-local/agents/*.md — that is the whole contract a " +
44
+ "host reads from disk; a host never loads the package, so unless those files " +
45
+ "are committed and in package.json's files allowlist, the package contributes " +
46
+ "nothing. You keep them in sync with toMarkdown.",
47
+ knowledge: reference,
48
+ },
49
+ ];
@@ -0,0 +1,8 @@
1
+ export interface DiscoveredProvider {
2
+ packageName: string;
3
+ prefix: string;
4
+ scope: string | null;
5
+ agentFiles: string[];
6
+ }
7
+ export declare function directDependencies(hostDir: string): string[];
8
+ export declare function discoverProviders(hostDir: string): DiscoveredProvider[];
@@ -0,0 +1,35 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const DEFAULT_AGENTS_DIR = "the-local/agents";
4
+ function readManifest(packageJsonPath) {
5
+ if (!existsSync(packageJsonPath))
6
+ return null;
7
+ return JSON.parse(readFileSync(packageJsonPath, "utf8"));
8
+ }
9
+ export function directDependencies(hostDir) {
10
+ const manifest = readManifest(join(hostDir, "package.json"));
11
+ return Object.keys(manifest?.dependencies ?? {});
12
+ }
13
+ export function discoverProviders(hostDir) {
14
+ const nodeModulesDir = join(hostDir, "node_modules");
15
+ const providers = [];
16
+ for (const dependency of directDependencies(hostDir)) {
17
+ const packageDir = join(nodeModulesDir, dependency);
18
+ const manifest = readManifest(join(packageDir, "package.json"));
19
+ const declaration = manifest?.["the-local"];
20
+ if (!manifest || !declaration)
21
+ continue;
22
+ const prefix = declaration.prefix ?? dependency;
23
+ const agentsDir = join(packageDir, declaration.agentsDir ?? DEFAULT_AGENTS_DIR);
24
+ if (!existsSync(agentsDir)) {
25
+ throw new Error(`the-local: ${dependency} declares the-local locals but ships no committed agents at ` +
26
+ `${declaration.agentsDir ?? DEFAULT_AGENTS_DIR}. Build and commit them in ${dependency}.`);
27
+ }
28
+ const agentFiles = readdirSync(agentsDir)
29
+ .filter((entry) => entry.endsWith(".md"))
30
+ .sort()
31
+ .map((entry) => join(agentsDir, entry));
32
+ providers.push({ packageName: dependency, prefix, scope: declaration.scope ?? null, agentFiles });
33
+ }
34
+ return providers;
35
+ }
@@ -0,0 +1,8 @@
1
+ export { type Agent, agentFilename, qualifiedName, toMarkdown } from "./agent.js";
2
+ export { BEGIN_MARKER, END_MARKER, type ProviderTrigger, delegationRule, mergeTrigger } from "./trigger.js";
3
+ export { type ScopeInput, allowedProviders } from "./scope.js";
4
+ export { type DiscoveredProvider, directDependencies, discoverProviders } from "./discovery.js";
5
+ export { type InstallResult, installAgents, installLocals, writeTrigger } from "./installer.js";
6
+ export { reference } from "./reference.js";
7
+ export { companionAgents } from "./companion.js";
8
+ export { type ProviderAgentSpec, type ProviderConfig, buildProvider, prefixFromName, renderProvider, scaffoldProvider, starterConfig, } from "./provider.js";
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { agentFilename, qualifiedName, toMarkdown } from "./agent.js";
2
+ export { BEGIN_MARKER, END_MARKER, delegationRule, mergeTrigger } from "./trigger.js";
3
+ export { allowedProviders } from "./scope.js";
4
+ export { directDependencies, discoverProviders } from "./discovery.js";
5
+ export { installAgents, installLocals, writeTrigger } from "./installer.js";
6
+ export { reference } from "./reference.js";
7
+ export { companionAgents } from "./companion.js";
8
+ export { buildProvider, prefixFromName, renderProvider, scaffoldProvider, starterConfig, } from "./provider.js";
@@ -0,0 +1,9 @@
1
+ import { type DiscoveredProvider } from "./discovery.js";
2
+ import { type ProviderTrigger } from "./trigger.js";
3
+ export declare function installAgents(hostDir: string, providers: DiscoveredProvider[]): string[];
4
+ export declare function writeTrigger(hostDir: string, providers: ProviderTrigger[], filename?: string): void;
5
+ export interface InstallResult {
6
+ providers: DiscoveredProvider[];
7
+ agents: string[];
8
+ }
9
+ export declare function installLocals(hostDir: string): InstallResult;
@@ -0,0 +1,29 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { basename, join } from "node:path";
3
+ import { discoverProviders } from "./discovery.js";
4
+ import { delegationRule, mergeTrigger } from "./trigger.js";
5
+ const AGENTS_DIR = ".claude/agents";
6
+ export function installAgents(hostDir, providers) {
7
+ const destination = join(hostDir, AGENTS_DIR);
8
+ mkdirSync(destination, { recursive: true });
9
+ const written = [];
10
+ for (const provider of providers) {
11
+ for (const source of provider.agentFiles) {
12
+ const target = join(destination, basename(source));
13
+ copyFileSync(source, target);
14
+ written.push(target);
15
+ }
16
+ }
17
+ return written;
18
+ }
19
+ export function writeTrigger(hostDir, providers, filename = "CLAUDE.md") {
20
+ const path = join(hostDir, filename);
21
+ const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
22
+ writeFileSync(path, `${mergeTrigger(existing, delegationRule(providers))}\n`);
23
+ }
24
+ export function installLocals(hostDir) {
25
+ const providers = discoverProviders(hostDir);
26
+ const agents = installAgents(hostDir, providers);
27
+ writeTrigger(hostDir, providers);
28
+ return { providers, agents };
29
+ }
@@ -0,0 +1,16 @@
1
+ import { type Agent } from "./agent.js";
2
+ export declare function prefixFromName(packageName: string): string;
3
+ export interface ProviderConfig {
4
+ prefix: string;
5
+ scope?: string;
6
+ agentsDir?: string;
7
+ agents: ProviderAgentSpec[];
8
+ }
9
+ export type ProviderAgentSpec = Omit<Agent, "prefix">;
10
+ export declare function renderProvider(config: ProviderConfig, packageDir: string): string[];
11
+ export declare function starterConfig(packageName: string): ProviderConfig;
12
+ export declare function scaffoldProvider(packageDir: string): {
13
+ config: ProviderConfig;
14
+ created: boolean;
15
+ };
16
+ export declare function buildProvider(packageDir: string): Promise<string[]>;
@@ -0,0 +1,83 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { agentFilename, toMarkdown } from "./agent.js";
5
+ const DEFAULT_AGENTS_DIR = "the-local/agents";
6
+ // The agent filename namespace defaults to the package name with any npm scope
7
+ // dropped: `@event-engine/core` -> `core`.
8
+ export function prefixFromName(packageName) {
9
+ return packageName.replace(/^@[^/]+\//, "");
10
+ }
11
+ // Render each config agent to `<packageDir>/<agentsDir>/<prefix>-<name>.md` and
12
+ // return the written paths. Pure apart from the writes — the same render used
13
+ // for the-local's own companion, generalised to any provider config.
14
+ export function renderProvider(config, packageDir) {
15
+ const agentsDir = join(packageDir, config.agentsDir ?? DEFAULT_AGENTS_DIR);
16
+ mkdirSync(agentsDir, { recursive: true });
17
+ return config.agents.map((spec) => {
18
+ const agent = { prefix: config.prefix, ...spec };
19
+ const path = join(agentsDir, agentFilename(agent));
20
+ writeFileSync(path, toMarkdown(agent));
21
+ return path;
22
+ });
23
+ }
24
+ const CONFIG_FILE = "the-local.config.js";
25
+ // The starter config a freshly-scaffolded provider gets: the standard interface
26
+ // of a read-only `info` explainer and a `develop` domain worker, with TODO
27
+ // placeholders the author fills in. Mirrors the Ruby provider generator.
28
+ export function starterConfig(packageName) {
29
+ const prefix = prefixFromName(packageName);
30
+ const knowledge = `## ${prefix}\n\nTODO: document ${packageName} — what it does, how to use it, the conventions to enforce.`;
31
+ return {
32
+ prefix,
33
+ scope: `TODO: one-line phrase describing ${packageName}'s domain`,
34
+ agents: [
35
+ {
36
+ name: "info",
37
+ description: `Use to learn what ${packageName} offers and how to use it.`,
38
+ tools: "Read",
39
+ body: `You explain ${packageName}, answering only from the reference. You make no changes.`,
40
+ knowledge,
41
+ },
42
+ {
43
+ name: "develop",
44
+ description: `Use PROACTIVELY for work involving ${packageName}.`,
45
+ tools: "Read, Write, Edit, Grep",
46
+ body: `You do work involving ${packageName}, following the reference's conventions exactly.`,
47
+ knowledge,
48
+ },
49
+ ],
50
+ };
51
+ }
52
+ // Scaffold the provider side into a package: write a starter config (without
53
+ // clobbering an authored one). Later cycles wire package.json and render.
54
+ export function scaffoldProvider(packageDir) {
55
+ const manifestPath = join(packageDir, "package.json");
56
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
57
+ const config = starterConfig(manifest.name);
58
+ // Never clobber an authored config: if one already exists, the package is a
59
+ // provider already — leave its config, package.json, and rendered agents
60
+ // alone and let `the-local build` re-render from the author's config.
61
+ const configPath = join(packageDir, CONFIG_FILE);
62
+ const created = !existsSync(configPath);
63
+ if (!created)
64
+ return { config, created };
65
+ writeFileSync(configPath, `export default ${JSON.stringify(config, null, 2)};\n`);
66
+ const agentsDir = config.agentsDir ?? DEFAULT_AGENTS_DIR;
67
+ manifest["the-local"] = { prefix: config.prefix, scope: config.scope, agentsDir };
68
+ const files = Array.isArray(manifest.files) ? manifest.files : [];
69
+ if (!files.includes(agentsDir))
70
+ files.push(agentsDir);
71
+ manifest.files = files;
72
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
73
+ renderProvider(config, packageDir);
74
+ return { config, created };
75
+ }
76
+ // Re-render a provider's committed agents from its authored `the-local.config.js`
77
+ // (the analog of Ruby's `rake the_local:build`). Loaded by dynamic import so the
78
+ // config stays plain ESM data with no toolchain — run after editing the config.
79
+ export async function buildProvider(packageDir) {
80
+ const configUrl = pathToFileURL(join(packageDir, CONFIG_FILE)).href;
81
+ const loaded = (await import(configUrl));
82
+ return renderProvider(loaded.default, packageDir);
83
+ }
@@ -0,0 +1 @@
1
+ export declare const reference = "## the-local\n\n> **DO NOT** explore the the-local package source code. This reference is the\n> complete user-facing API, embedded verbatim into every the-local local so\n> their guidance never drifts. Keep it the single source of truth.\n\nthe-local is the engine that lets any npm package or app ship resident Claude\nCode expert subagents (\"locals\") that know its conventions. A provider package\ndeclares its locals and commits them as rendered `.md` files; the-local installs\nthe aggregated set from every directly-depended provider into a consuming app's\n`.claude/agents/`, plus a delegation rule so the host's agent actually uses them.\n\n### The model\n\n- **Providers ship committed locals.** A provider declares itself in its\n `package.json` with a `\"the-local\"` block and commits one rendered file per\n local at `the-local/agents/<prefix>-<name>.md`. **These committed files are the\n contract** \u2014 they are what a host reads. A provider can build them from a\n companion + `toMarkdown`, but the committed `.md` is all a host ever sees.\n- **Install discovers committed `.md` on disk.** In a host, `the-local install`\n reads each direct dependency's committed `the-local/agents/*.md` straight from\n its package path under `node_modules` and copies them into `.claude/agents/`\n byte-for-byte \u2014 no provider code is loaded. Output depends only on the provider\n package version, so it is a true carbon copy across every app, and a fragile\n provider can't crash the install.\n- **The delegation trigger.** Install also writes a generated block into the\n host's `CLAUDE.md`, between the `<!-- the_local:begin -->` and\n `<!-- the_local:end -->` markers, telling the host agent to delegate to these\n locals. This is what makes delegation actually happen. The markers are shared\n with the Ruby `the_local` gem, so a gem and an npm package never clobber each\n other's block.\n- **Direct-dependency scope.** Only the host's direct dependencies contribute\n locals; transitive providers are filtered out, so a host gets exactly the\n experts for the packages it chose.\n\n### Install (in any package or app)\n\n1. Add `the-local` to the host's `package.json` dependencies and install it.\n2. Run `the-local install` (or `npx the-local install`). This syncs every direct\n provider's committed locals into `.claude/agents/` and writes the delegation\n trigger into `CLAUDE.md`. It runs from the host's working directory.\n3. Re-run `the-local install` after any dependency change (a provider added,\n removed, or upgraded) to bring the host's locals back in sync. The package\n only exposes the command; a script or hook can automate re-running it.\n\n### Author a provider (turn a package into a provider)\n\n1. Add a `\"the-local\"` block to the package's `package.json` and create the\n `the-local/agents/` directory.\n2. Write each local in the standard interface \u2014 `info` (read-only explainer),\n `install` (sets the package up in a host), and a domain worker (`develop` for\n libraries, `operate` for CLIs) \u2014 tailoring its `description`, `tools`, and\n `body` to your package, with a guide as its embedded `knowledge`.\n3. Render each local to `the-local/agents/<prefix>-<name>.md` with `toMarkdown`,\n then **commit and ship** those files (they must be in `package.json`'s `files`\n allowlist). This is the whole contract: a host discovers your locals by\n reading these committed files from your package on disk \u2014 it never loads your\n code \u2014 so if they aren't committed and shipped, you contribute nothing, and if\n they are, you contribute everything. A drift test asserting each committed\n file equals its `toMarkdown` keeps the artifact honest.\n\n### The package.json declaration\n\n```json\n{\n \"the-local\": {\n \"prefix\": \"my-pkg\",\n \"scope\": \"one-line domain phrase\",\n \"agentsDir\": \"the-local/agents\"\n }\n}\n```\n\n- `prefix` is the agent filename namespace; defaults to the package name.\n- `scope` is a one-line domain phrase used to generate the delegation trigger;\n omit it for a bare `- <prefix>-* agents` bullet.\n- `agentsDir` is the path to the committed `.md` files, relative to the package\n root; it defaults to `the-local/agents`.\n\n### Conventions\n\n- The committed `.md` files are the contract; commit them, and never render in\n the host at install time.\n- A local's guide documents the providing package only and stays the single\n source of truth; never let a rendered `.md` drift from `toMarkdown`.\n- Only direct dependencies contribute, so depend on a provider directly to gain\n its locals.";
@@ -0,0 +1,92 @@
1
+ // the-local's own knowledge guide, embedded verbatim as the `knowledge` of
2
+ // every companion agent (info / install / develop). This is the single source
3
+ // of truth for how the-local works; the rendered companion `.md` files are
4
+ // built from it, so it must describe the port's actual behaviour. Keep it free
5
+ // of a trailing newline so `toMarkdown` ends each agent on a single newline.
6
+ export const reference = `## the-local
7
+
8
+ > **DO NOT** explore the the-local package source code. This reference is the
9
+ > complete user-facing API, embedded verbatim into every the-local local so
10
+ > their guidance never drifts. Keep it the single source of truth.
11
+
12
+ the-local is the engine that lets any npm package or app ship resident Claude
13
+ Code expert subagents ("locals") that know its conventions. A provider package
14
+ declares its locals and commits them as rendered \`.md\` files; the-local installs
15
+ the aggregated set from every directly-depended provider into a consuming app's
16
+ \`.claude/agents/\`, plus a delegation rule so the host's agent actually uses them.
17
+
18
+ ### The model
19
+
20
+ - **Providers ship committed locals.** A provider declares itself in its
21
+ \`package.json\` with a \`"the-local"\` block and commits one rendered file per
22
+ local at \`the-local/agents/<prefix>-<name>.md\`. **These committed files are the
23
+ contract** — they are what a host reads. A provider can build them from a
24
+ companion + \`toMarkdown\`, but the committed \`.md\` is all a host ever sees.
25
+ - **Install discovers committed \`.md\` on disk.** In a host, \`the-local install\`
26
+ reads each direct dependency's committed \`the-local/agents/*.md\` straight from
27
+ its package path under \`node_modules\` and copies them into \`.claude/agents/\`
28
+ byte-for-byte — no provider code is loaded. Output depends only on the provider
29
+ package version, so it is a true carbon copy across every app, and a fragile
30
+ provider can't crash the install.
31
+ - **The delegation trigger.** Install also writes a generated block into the
32
+ host's \`CLAUDE.md\`, between the \`<!-- the_local:begin -->\` and
33
+ \`<!-- the_local:end -->\` markers, telling the host agent to delegate to these
34
+ locals. This is what makes delegation actually happen. The markers are shared
35
+ with the Ruby \`the_local\` gem, so a gem and an npm package never clobber each
36
+ other's block.
37
+ - **Direct-dependency scope.** Only the host's direct dependencies contribute
38
+ locals; transitive providers are filtered out, so a host gets exactly the
39
+ experts for the packages it chose.
40
+
41
+ ### Install (in any package or app)
42
+
43
+ 1. Add \`the-local\` to the host's \`package.json\` dependencies and install it.
44
+ 2. Run \`the-local install\` (or \`npx the-local install\`). This syncs every direct
45
+ provider's committed locals into \`.claude/agents/\` and writes the delegation
46
+ trigger into \`CLAUDE.md\`. It runs from the host's working directory.
47
+ 3. Re-run \`the-local install\` after any dependency change (a provider added,
48
+ removed, or upgraded) to bring the host's locals back in sync. The package
49
+ only exposes the command; a script or hook can automate re-running it.
50
+
51
+ ### Author a provider (turn a package into a provider)
52
+
53
+ 1. Add a \`"the-local"\` block to the package's \`package.json\` and create the
54
+ \`the-local/agents/\` directory.
55
+ 2. Write each local in the standard interface — \`info\` (read-only explainer),
56
+ \`install\` (sets the package up in a host), and a domain worker (\`develop\` for
57
+ libraries, \`operate\` for CLIs) — tailoring its \`description\`, \`tools\`, and
58
+ \`body\` to your package, with a guide as its embedded \`knowledge\`.
59
+ 3. Render each local to \`the-local/agents/<prefix>-<name>.md\` with \`toMarkdown\`,
60
+ then **commit and ship** those files (they must be in \`package.json\`'s \`files\`
61
+ allowlist). This is the whole contract: a host discovers your locals by
62
+ reading these committed files from your package on disk — it never loads your
63
+ code — so if they aren't committed and shipped, you contribute nothing, and if
64
+ they are, you contribute everything. A drift test asserting each committed
65
+ file equals its \`toMarkdown\` keeps the artifact honest.
66
+
67
+ ### The package.json declaration
68
+
69
+ \`\`\`json
70
+ {
71
+ "the-local": {
72
+ "prefix": "my-pkg",
73
+ "scope": "one-line domain phrase",
74
+ "agentsDir": "the-local/agents"
75
+ }
76
+ }
77
+ \`\`\`
78
+
79
+ - \`prefix\` is the agent filename namespace; defaults to the package name.
80
+ - \`scope\` is a one-line domain phrase used to generate the delegation trigger;
81
+ omit it for a bare \`- <prefix>-* agents\` bullet.
82
+ - \`agentsDir\` is the path to the committed \`.md\` files, relative to the package
83
+ root; it defaults to \`the-local/agents\`.
84
+
85
+ ### Conventions
86
+
87
+ - The committed \`.md\` files are the contract; commit them, and never render in
88
+ the host at install time.
89
+ - A local's guide documents the providing package only and stays the single
90
+ source of truth; never let a rendered \`.md\` drift from \`toMarkdown\`.
91
+ - Only direct dependencies contribute, so depend on a provider directly to gain
92
+ its locals.`;
@@ -0,0 +1,6 @@
1
+ export interface ScopeInput {
2
+ providerNames: string[];
3
+ directDependencies: string[];
4
+ installedPackages: string[];
5
+ }
6
+ export declare function allowedProviders(input: ScopeInput): string[];
package/dist/scope.js ADDED
@@ -0,0 +1,4 @@
1
+ export function allowedProviders(input) {
2
+ const { providerNames, directDependencies, installedPackages } = input;
3
+ return [...new Set(providerNames)].filter((name) => directDependencies.includes(name) || !installedPackages.includes(name));
4
+ }
@@ -0,0 +1,8 @@
1
+ export declare const BEGIN_MARKER = "<!-- the_local:begin -->";
2
+ export declare const END_MARKER = "<!-- the_local:end -->";
3
+ export interface ProviderTrigger {
4
+ prefix: string;
5
+ scope?: string | null;
6
+ }
7
+ export declare function delegationRule(providers: ProviderTrigger[]): string;
8
+ export declare function mergeTrigger(existing: string, rule: string): string;
@@ -0,0 +1,32 @@
1
+ export const BEGIN_MARKER = "<!-- the_local:begin -->";
2
+ export const END_MARKER = "<!-- the_local:end -->";
3
+ function escapeRegExp(value) {
4
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5
+ }
6
+ function bullet(provider) {
7
+ const target = `${provider.prefix}-* agents`;
8
+ return `- ${provider.scope ? `${provider.scope} → ${target}` : target}`;
9
+ }
10
+ export function delegationRule(providers) {
11
+ return [
12
+ BEGIN_MARKER,
13
+ "## Delegate to your locals",
14
+ "",
15
+ "This project has installed expert subagents. Before doing work yourself,",
16
+ "check whether a local owns it and delegate — never work from memory on",
17
+ "something a local covers:",
18
+ "",
19
+ providers.map(bullet).join("\n"),
20
+ "",
21
+ "See each agent's description for specifics.",
22
+ END_MARKER,
23
+ ].join("\n");
24
+ }
25
+ export function mergeTrigger(existing, rule) {
26
+ const section = new RegExp(`${escapeRegExp(BEGIN_MARKER)}[\\s\\S]*?${escapeRegExp(END_MARKER)}`);
27
+ if (section.test(existing))
28
+ return existing.replace(section, rule);
29
+ if (existing.trim() === "")
30
+ return rule;
31
+ return `${existing.replace(/\r?\n$/, "")}\n\n${rule}`;
32
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "the-local",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Ship Claude Code locals from npm packages into a host TS/JS app — the TypeScript port of the the_local Ruby gem.",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/DYB-Development/the-local.git"
12
+ },
13
+ "homepage": "https://github.com/DYB-Development/the-local#readme",
14
+ "bugs": "https://github.com/DYB-Development/the-local/issues",
15
+ "files": ["dist", "README.md", "the-local/agents"],
16
+ "the-local": {
17
+ "prefix": "the-local",
18
+ "scope": "Claude Code locals — packages ship subagents that the-local installs into a host app",
19
+ "agentsDir": "the-local/agents"
20
+ },
21
+ "main": "dist/index.js",
22
+ "bin": {
23
+ "the-local": "dist/cli.js"
24
+ },
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
29
+ }
30
+ },
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.build.json && chmod +x dist/cli.js",
33
+ "build:agents": "pnpm build && node scripts/build-companion.mjs",
34
+ "prepare": "pnpm build",
35
+ "prepublishOnly": "pnpm build",
36
+ "typecheck": "tsc --noEmit",
37
+ "test": "vitest run",
38
+ "test:watch": "vitest",
39
+ "lint": "eslint ."
40
+ },
41
+ "devDependencies": {
42
+ "@eslint/js": "^9.17.0",
43
+ "@types/node": "^22.10.0",
44
+ "eslint": "^9.17.0",
45
+ "typescript": "^5.7.2",
46
+ "typescript-eslint": "^8.18.0",
47
+ "vitest": "^2.1.8"
48
+ }
49
+ }
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: the-local-develop
3
+ description: Use PROACTIVELY to turn a package into a the-local provider — declaring the provider block, authoring the locals, and committing the rendered agents. MUST BE USED instead of wiring a provider by hand.
4
+ tools: Read, Write, Edit, Grep
5
+ ---
6
+
7
+ You turn a package into a the-local provider following the reference's provider-author workflow: add the `the-local` block to package.json, author the standard locals (info, install, and a domain worker) with a guide as their knowledge, and render each to the-local/agents/. The deliverable is the committed, shipped the-local/agents/*.md — that is the whole contract a host reads from disk; a host never loads the package, so unless those files are committed and in package.json's files allowlist, the package contributes nothing. You keep them in sync with toMarkdown.
8
+
9
+ ## the-local
10
+
11
+ > **DO NOT** explore the the-local package source code. This reference is the
12
+ > complete user-facing API, embedded verbatim into every the-local local so
13
+ > their guidance never drifts. Keep it the single source of truth.
14
+
15
+ the-local is the engine that lets any npm package or app ship resident Claude
16
+ Code expert subagents ("locals") that know its conventions. A provider package
17
+ declares its locals and commits them as rendered `.md` files; the-local installs
18
+ the aggregated set from every directly-depended provider into a consuming app's
19
+ `.claude/agents/`, plus a delegation rule so the host's agent actually uses them.
20
+
21
+ ### The model
22
+
23
+ - **Providers ship committed locals.** A provider declares itself in its
24
+ `package.json` with a `"the-local"` block and commits one rendered file per
25
+ local at `the-local/agents/<prefix>-<name>.md`. **These committed files are the
26
+ contract** — they are what a host reads. A provider can build them from a
27
+ companion + `toMarkdown`, but the committed `.md` is all a host ever sees.
28
+ - **Install discovers committed `.md` on disk.** In a host, `the-local install`
29
+ reads each direct dependency's committed `the-local/agents/*.md` straight from
30
+ its package path under `node_modules` and copies them into `.claude/agents/`
31
+ byte-for-byte — no provider code is loaded. Output depends only on the provider
32
+ package version, so it is a true carbon copy across every app, and a fragile
33
+ provider can't crash the install.
34
+ - **The delegation trigger.** Install also writes a generated block into the
35
+ host's `CLAUDE.md`, between the `<!-- the_local:begin -->` and
36
+ `<!-- the_local:end -->` markers, telling the host agent to delegate to these
37
+ locals. This is what makes delegation actually happen. The markers are shared
38
+ with the Ruby `the_local` gem, so a gem and an npm package never clobber each
39
+ other's block.
40
+ - **Direct-dependency scope.** Only the host's direct dependencies contribute
41
+ locals; transitive providers are filtered out, so a host gets exactly the
42
+ experts for the packages it chose.
43
+
44
+ ### Install (in any package or app)
45
+
46
+ 1. Add `the-local` to the host's `package.json` dependencies and install it.
47
+ 2. Run `the-local install` (or `npx the-local install`). This syncs every direct
48
+ provider's committed locals into `.claude/agents/` and writes the delegation
49
+ trigger into `CLAUDE.md`. It runs from the host's working directory.
50
+ 3. Re-run `the-local install` after any dependency change (a provider added,
51
+ removed, or upgraded) to bring the host's locals back in sync. The package
52
+ only exposes the command; a script or hook can automate re-running it.
53
+
54
+ ### Author a provider (turn a package into a provider)
55
+
56
+ 1. Add a `"the-local"` block to the package's `package.json` and create the
57
+ `the-local/agents/` directory.
58
+ 2. Write each local in the standard interface — `info` (read-only explainer),
59
+ `install` (sets the package up in a host), and a domain worker (`develop` for
60
+ libraries, `operate` for CLIs) — tailoring its `description`, `tools`, and
61
+ `body` to your package, with a guide as its embedded `knowledge`.
62
+ 3. Render each local to `the-local/agents/<prefix>-<name>.md` with `toMarkdown`,
63
+ then **commit and ship** those files (they must be in `package.json`'s `files`
64
+ allowlist). This is the whole contract: a host discovers your locals by
65
+ reading these committed files from your package on disk — it never loads your
66
+ code — so if they aren't committed and shipped, you contribute nothing, and if
67
+ they are, you contribute everything. A drift test asserting each committed
68
+ file equals its `toMarkdown` keeps the artifact honest.
69
+
70
+ ### The package.json declaration
71
+
72
+ ```json
73
+ {
74
+ "the-local": {
75
+ "prefix": "my-pkg",
76
+ "scope": "one-line domain phrase",
77
+ "agentsDir": "the-local/agents"
78
+ }
79
+ }
80
+ ```
81
+
82
+ - `prefix` is the agent filename namespace; defaults to the package name.
83
+ - `scope` is a one-line domain phrase used to generate the delegation trigger;
84
+ omit it for a bare `- <prefix>-* agents` bullet.
85
+ - `agentsDir` is the path to the committed `.md` files, relative to the package
86
+ root; it defaults to `the-local/agents`.
87
+
88
+ ### Conventions
89
+
90
+ - The committed `.md` files are the contract; commit them, and never render in
91
+ the host at install time.
92
+ - A local's guide documents the providing package only and stays the single
93
+ source of truth; never let a rendered `.md` drift from `toMarkdown`.
94
+ - Only direct dependencies contribute, so depend on a provider directly to gain
95
+ its locals.
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: the-local-info
3
+ description: Use to learn how the-local works — providers, the committed-.md install model, the delegation trigger, and the direct-dependency scope rule.
4
+ tools: Read
5
+ ---
6
+
7
+ You explain how the-local works, answering only from the reference: providers ship committed locals, `the-local install` copies them verbatim into a host's .claude/agents/, the CLAUDE.md delegation trigger makes the host delegate, and only direct dependencies contribute. You make no changes.
8
+
9
+ ## the-local
10
+
11
+ > **DO NOT** explore the the-local package source code. This reference is the
12
+ > complete user-facing API, embedded verbatim into every the-local local so
13
+ > their guidance never drifts. Keep it the single source of truth.
14
+
15
+ the-local is the engine that lets any npm package or app ship resident Claude
16
+ Code expert subagents ("locals") that know its conventions. A provider package
17
+ declares its locals and commits them as rendered `.md` files; the-local installs
18
+ the aggregated set from every directly-depended provider into a consuming app's
19
+ `.claude/agents/`, plus a delegation rule so the host's agent actually uses them.
20
+
21
+ ### The model
22
+
23
+ - **Providers ship committed locals.** A provider declares itself in its
24
+ `package.json` with a `"the-local"` block and commits one rendered file per
25
+ local at `the-local/agents/<prefix>-<name>.md`. **These committed files are the
26
+ contract** — they are what a host reads. A provider can build them from a
27
+ companion + `toMarkdown`, but the committed `.md` is all a host ever sees.
28
+ - **Install discovers committed `.md` on disk.** In a host, `the-local install`
29
+ reads each direct dependency's committed `the-local/agents/*.md` straight from
30
+ its package path under `node_modules` and copies them into `.claude/agents/`
31
+ byte-for-byte — no provider code is loaded. Output depends only on the provider
32
+ package version, so it is a true carbon copy across every app, and a fragile
33
+ provider can't crash the install.
34
+ - **The delegation trigger.** Install also writes a generated block into the
35
+ host's `CLAUDE.md`, between the `<!-- the_local:begin -->` and
36
+ `<!-- the_local:end -->` markers, telling the host agent to delegate to these
37
+ locals. This is what makes delegation actually happen. The markers are shared
38
+ with the Ruby `the_local` gem, so a gem and an npm package never clobber each
39
+ other's block.
40
+ - **Direct-dependency scope.** Only the host's direct dependencies contribute
41
+ locals; transitive providers are filtered out, so a host gets exactly the
42
+ experts for the packages it chose.
43
+
44
+ ### Install (in any package or app)
45
+
46
+ 1. Add `the-local` to the host's `package.json` dependencies and install it.
47
+ 2. Run `the-local install` (or `npx the-local install`). This syncs every direct
48
+ provider's committed locals into `.claude/agents/` and writes the delegation
49
+ trigger into `CLAUDE.md`. It runs from the host's working directory.
50
+ 3. Re-run `the-local install` after any dependency change (a provider added,
51
+ removed, or upgraded) to bring the host's locals back in sync. The package
52
+ only exposes the command; a script or hook can automate re-running it.
53
+
54
+ ### Author a provider (turn a package into a provider)
55
+
56
+ 1. Add a `"the-local"` block to the package's `package.json` and create the
57
+ `the-local/agents/` directory.
58
+ 2. Write each local in the standard interface — `info` (read-only explainer),
59
+ `install` (sets the package up in a host), and a domain worker (`develop` for
60
+ libraries, `operate` for CLIs) — tailoring its `description`, `tools`, and
61
+ `body` to your package, with a guide as its embedded `knowledge`.
62
+ 3. Render each local to `the-local/agents/<prefix>-<name>.md` with `toMarkdown`,
63
+ then **commit and ship** those files (they must be in `package.json`'s `files`
64
+ allowlist). This is the whole contract: a host discovers your locals by
65
+ reading these committed files from your package on disk — it never loads your
66
+ code — so if they aren't committed and shipped, you contribute nothing, and if
67
+ they are, you contribute everything. A drift test asserting each committed
68
+ file equals its `toMarkdown` keeps the artifact honest.
69
+
70
+ ### The package.json declaration
71
+
72
+ ```json
73
+ {
74
+ "the-local": {
75
+ "prefix": "my-pkg",
76
+ "scope": "one-line domain phrase",
77
+ "agentsDir": "the-local/agents"
78
+ }
79
+ }
80
+ ```
81
+
82
+ - `prefix` is the agent filename namespace; defaults to the package name.
83
+ - `scope` is a one-line domain phrase used to generate the delegation trigger;
84
+ omit it for a bare `- <prefix>-* agents` bullet.
85
+ - `agentsDir` is the path to the committed `.md` files, relative to the package
86
+ root; it defaults to `the-local/agents`.
87
+
88
+ ### Conventions
89
+
90
+ - The committed `.md` files are the contract; commit them, and never render in
91
+ the host at install time.
92
+ - A local's guide documents the providing package only and stays the single
93
+ source of truth; never let a rendered `.md` drift from `toMarkdown`.
94
+ - Only direct dependencies contribute, so depend on a provider directly to gain
95
+ its locals.
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: the-local-install
3
+ description: Use to add the-local to a host app and set it up correctly.
4
+ tools: Bash, Read, Edit
5
+ ---
6
+
7
+ You set the-local up in a host package or app, following the reference's install section exactly: add the-local to the host's dependencies, install, run `the-local install` to sync locals into .claude/agents/ and write the delegation trigger, and re-run it after dependency changes. You do not invent steps the reference does not list.
8
+
9
+ ## the-local
10
+
11
+ > **DO NOT** explore the the-local package source code. This reference is the
12
+ > complete user-facing API, embedded verbatim into every the-local local so
13
+ > their guidance never drifts. Keep it the single source of truth.
14
+
15
+ the-local is the engine that lets any npm package or app ship resident Claude
16
+ Code expert subagents ("locals") that know its conventions. A provider package
17
+ declares its locals and commits them as rendered `.md` files; the-local installs
18
+ the aggregated set from every directly-depended provider into a consuming app's
19
+ `.claude/agents/`, plus a delegation rule so the host's agent actually uses them.
20
+
21
+ ### The model
22
+
23
+ - **Providers ship committed locals.** A provider declares itself in its
24
+ `package.json` with a `"the-local"` block and commits one rendered file per
25
+ local at `the-local/agents/<prefix>-<name>.md`. **These committed files are the
26
+ contract** — they are what a host reads. A provider can build them from a
27
+ companion + `toMarkdown`, but the committed `.md` is all a host ever sees.
28
+ - **Install discovers committed `.md` on disk.** In a host, `the-local install`
29
+ reads each direct dependency's committed `the-local/agents/*.md` straight from
30
+ its package path under `node_modules` and copies them into `.claude/agents/`
31
+ byte-for-byte — no provider code is loaded. Output depends only on the provider
32
+ package version, so it is a true carbon copy across every app, and a fragile
33
+ provider can't crash the install.
34
+ - **The delegation trigger.** Install also writes a generated block into the
35
+ host's `CLAUDE.md`, between the `<!-- the_local:begin -->` and
36
+ `<!-- the_local:end -->` markers, telling the host agent to delegate to these
37
+ locals. This is what makes delegation actually happen. The markers are shared
38
+ with the Ruby `the_local` gem, so a gem and an npm package never clobber each
39
+ other's block.
40
+ - **Direct-dependency scope.** Only the host's direct dependencies contribute
41
+ locals; transitive providers are filtered out, so a host gets exactly the
42
+ experts for the packages it chose.
43
+
44
+ ### Install (in any package or app)
45
+
46
+ 1. Add `the-local` to the host's `package.json` dependencies and install it.
47
+ 2. Run `the-local install` (or `npx the-local install`). This syncs every direct
48
+ provider's committed locals into `.claude/agents/` and writes the delegation
49
+ trigger into `CLAUDE.md`. It runs from the host's working directory.
50
+ 3. Re-run `the-local install` after any dependency change (a provider added,
51
+ removed, or upgraded) to bring the host's locals back in sync. The package
52
+ only exposes the command; a script or hook can automate re-running it.
53
+
54
+ ### Author a provider (turn a package into a provider)
55
+
56
+ 1. Add a `"the-local"` block to the package's `package.json` and create the
57
+ `the-local/agents/` directory.
58
+ 2. Write each local in the standard interface — `info` (read-only explainer),
59
+ `install` (sets the package up in a host), and a domain worker (`develop` for
60
+ libraries, `operate` for CLIs) — tailoring its `description`, `tools`, and
61
+ `body` to your package, with a guide as its embedded `knowledge`.
62
+ 3. Render each local to `the-local/agents/<prefix>-<name>.md` with `toMarkdown`,
63
+ then **commit and ship** those files (they must be in `package.json`'s `files`
64
+ allowlist). This is the whole contract: a host discovers your locals by
65
+ reading these committed files from your package on disk — it never loads your
66
+ code — so if they aren't committed and shipped, you contribute nothing, and if
67
+ they are, you contribute everything. A drift test asserting each committed
68
+ file equals its `toMarkdown` keeps the artifact honest.
69
+
70
+ ### The package.json declaration
71
+
72
+ ```json
73
+ {
74
+ "the-local": {
75
+ "prefix": "my-pkg",
76
+ "scope": "one-line domain phrase",
77
+ "agentsDir": "the-local/agents"
78
+ }
79
+ }
80
+ ```
81
+
82
+ - `prefix` is the agent filename namespace; defaults to the package name.
83
+ - `scope` is a one-line domain phrase used to generate the delegation trigger;
84
+ omit it for a bare `- <prefix>-* agents` bullet.
85
+ - `agentsDir` is the path to the committed `.md` files, relative to the package
86
+ root; it defaults to `the-local/agents`.
87
+
88
+ ### Conventions
89
+
90
+ - The committed `.md` files are the contract; commit them, and never render in
91
+ the host at install time.
92
+ - A local's guide documents the providing package only and stays the single
93
+ source of truth; never let a rendered `.md` drift from `toMarkdown`.
94
+ - Only direct dependencies contribute, so depend on a provider directly to gain
95
+ its locals.