Skip to content

Add Kindgi to a TypeScript or Node app

View as Markdown

This page adds Kindgi to a TypeScript or Node app you already have. The pack lives beside your code: its tools import your app's modules.

Before you start, set up what the Install page describes: Node 22.12 or later, Docker, and access to the runtime image. In the app's folder, npx --yes @kindgi/cli@0.1 doctor checks all three, and says how to fix what's missing.

In the app's root (where its package.json is), with no pack name:

Terminal window
npx --yes @kindgi/cli@0.1 init
pnpm install
  • npx --yes @kindgi/cli@0.1 runs the newest 0.1 release of the Kindgi CLI without installing it (Check this machine says how).
  • init with no pack name adds Kindgi to the app in this folder. The pack's id comes from the name in the app's package.json. If there's no name, or init says it doesn't make a pack id, add --pack-id=<id>: lowercase letters, digits and dashes, starting with a letter, such as --pack-id=acme-support. The app's name stays as it is.
  • Run your app's own package manager's install in place of pnpm install.
  • If your app's pnpm sets minimumReleaseAge, pnpm install refuses a Kindgi release younger than that. Wait until the release is old enough, or install the release before it (@kindgi/cli@<version> and @kindgi/sdk@<version>). The setting protects your app's dependencies, so leave it as it is.

init prints what it made, as JSON and then as a summary. pnpm install ends with Done:

…
Kindgi added to …/acme-support (augment mode).
Pack id: acme-support Version: 0.1.0
Wrote 19 files; skipped 0.
…
Done in 4.9s using pnpm v10.28.0
Excerpt

init adds:

  • the pack's config: its id (from the app's name), version, and where its primitives live. It's kindgi.config.ts in an app whose package.json says "type": "module", and kindgi.config.mts in any other;
  • a kindgi/ folder for the primitives: kindgi/tools/, kindgi/agents/, kindgi/guardrails/, kindgi/flows/. In an app that isn't "type": "module", it also gets a one-line package.json that makes them ES modules;
  • the SDK and the CLI, at the CLI's own version: @kindgi/sdk (a dependency) and @kindgi/cli (a devDependency);
  • zod, at the version Kindgi is tested with. That's 4.6.5 exactly if your app pins exact versions or its package manager saves them, otherwise ^4.6.5. If your app already has zod, init keeps it;
  • the tests kindgi test runs: vitest and vite in devDependencies, pinned the same way, and a vitest config (Test your tools);
  • in a pnpm app, allowBuilds: { esbuild: false } in pnpm-workspace.yaml. pnpm 11 and later won't install until every dependency's install script has a decision, and esbuild (the CLI's bundler) doesn't need its script. A decision the app already has, true or false, is kept;
  • in an npm app, the same decision in package.json's allowScripts ("esbuild": false). Without it, npm 11 warns on the first install that esbuild's script is "not yet covered by allowScripts". npm 10 ignores the field. A decision the app already has, for esbuild or a pinned esbuild@<version>, is kept;
  • the skills for your coding agent under .claude/skills/ and .agents/skills/, and .gitignore entries (.kindgi/, .kindgirc.json, .env.local).

If your app is on zod 3, init keeps it, and a Kindgi schema written with zod 3 doesn't compile: Kindgi takes zod 4 schemas. Write your tools' and agents' inputs and outputs as JSON Schema objects instead. Moving the app to zod 4 is a change to the app's own code (zod 4 changes some of zod 3's API, such as invalid_type_error), so it's the app owner's decision. A tool's input as JSON Schema:

input: {
type: 'object',
additionalProperties: false,
required: ['orderId'],
properties: { orderId: { type: 'string' } },
},
Excerpt

It creates no env files: kindgi dev reads your app's own .env and .env.local, and keeps the secrets you store in its own .kindgi/secrets.env, which your app doesn't load. If your app lints with ESLint, leave out what Kindgi builds: add ".kindgi/**" to the globalIgnores in eslint.config.mjs. To keep the pack separate from the app instead, pass --new-repo.

A tool imports your app's modules like any other file in it, path aliases (@/…) included. Ask your coding agent: "Make a Kindgi tool from our support request lookup." For reference, the tool looks like this:

kindgi/tools/get-request/index.ts
import { defineTool } from '@kindgi/sdk/define';
import type { ToolId } from '@kindgi/sdk/types';
import { z } from 'zod';
import { findRequest } from '@/lib/requests'; // your app's own code
const defined = defineTool({
id: 'acme-support.get-request' as ToolId,
description: 'Looks up a support request by its id (REQ-1234).',
version: '0.1.0',
input: z.object({ id: z.string() }),
output: z.object({
found: z.boolean(),
subject: z.string().optional(),
body: z.string().optional(),
}),
effects: [],
mutating: false,
handler: async ({ id }) => {
const request = findRequest(id);
return request
? { found: true, subject: request.subject, body: request.body }
: { found: false };
},
});
if (defined.kind === 'err') throw new Error(defined.error.message);
export default defined.value;

Ids start with the pack's id (acme-support, from the app's name). mutating: false says the tool only reads, so a dry run may call it.

A package a tool imports (an ORM client such as @prisma/client, an API SDK) must be in your app's dependencies, not devDependencies: the deployed pack installs production dependencies only, so a dev-only import works under kindgi dev and fails once deployed. kindgi dev warns when a tool imports one, and kindgi build refuses the pack until it moves. Build-time tools (the prisma CLI, typescript) stay in devDependencies.

Ask your coding agent: "Add an agent that reads a support request and sets its priority." For reference, the agent looks like this:

kindgi/agents/triage/index.ts
import { defineAgent } from '@kindgi/sdk/define';
import type { AgentId, Semver } from '@kindgi/sdk/types';
import { z } from 'zod';
const defined = defineAgent({
id: 'acme-support.triage' as AgentId,
version: '0.1.0' as Semver,
name: 'Triage',
description: 'Reads a support request and sets its priority.',
instructions:
'The user names a support request id. Look it up with the get-request tool, ' +
'then answer with its priority and a one-sentence summary.',
capabilities: [{ needs: [{ feature: 'tool-use' as const }] }],
tools: [{ id: 'acme-support.get-request', version: '^0.1.0' }],
retrieval: [],
guardrails: [],
output: {
schema: z.object({
priority: z.enum(['low', 'normal', 'urgent']),
summary: z.string(),
}),
},
budget: { maxSteps: 4, maxCostUsd: 0.05, maxWallMs: 60_000 },
});
if (defined.kind === 'err') throw new Error(defined.error.message);
export default defined.value;

output makes the answer typed: the model has to answer with JSON that fits the schema, and your app gets it as an object.

Terminal window
pnpm exec kindgi dev

kindgi dev builds the pack from kindgi/, runs its tools in a process started from your app's root, and reloads on every save.

When it's up, its banner names the project and its database:

…
✓ Kindgi is up
…
Project acme-support · database kindgi_acme_support
…
Excerpt

Until you register a model, agents answer with dev-echo, which only checks the wiring: it calls the agent's first tool with {"message": …} and can't produce a typed answer, so this agent fails with output-schema-violation. Register a model first, with your AI model provider's API key:

Terminal window
pnpm exec kindgi secrets set <KEY_NAME> --env=local --scope=tenant # prompts for the key; it isn't echoed
pnpm exec kindgi providers register --preset=<preset>
pnpm exec kindgi runs start --agent=acme-support.triage --input='{"userMessage":"Triage REQ-1002"}'

With Anthropic's key, for example, the three commands print:

…
Set ANTHROPIC_API_KEY at tenant in local.
…
✓ Registered anthropic: claude-opus-5-5, claude-sonnet-5-5 (default), claude-haiku-5-5, claude-haiku-4-5 — key ANTHROPIC_API_KEY (env local)
…
"status": "completed",
…
"output": {
"summary": "User unable to log in due to expired password with non-functional password reset email delivery.",
"priority": "urgent"
},
Excerpt

<KEY_NAME> and <preset> depend on the provider:

Provider <KEY_NAME> <preset>
Anthropic ANTHROPIC_API_KEY anthropic
OpenAI OPENAI_API_KEY openai
Gemini (a Google AI Studio key) GEMINI_API_KEY gemini-api
Groq GROQ_API_KEY groq
OpenRouter (a hosted gateway) OPENROUTER_API_KEY openrouter

To have kindgi dev register the model on every boot, in each worktree and after --reset, declare it in the pack's config: Declare them in your pack's config.

A tool's tests go beside it (kindgi/tools/get-request/index.test.ts), and kindgi test runs them:

Terminal window
pnpm exec kindgi test

With two tests beside get-request, it prints vitest's summary:

…
Test Files 1 passed (1)
Tests 2 passed (2)
…
Excerpt

init sets this up. It writes vitest.config.mts at the app's root (vitest.config.ts in a "type": "module" app), which runs kindgi/**/*.test.ts and none of your app's own tests. vitest doesn't read tsconfig.json, so the config copies its paths (Next.js's @/*) into resolve.alias, and a tool that imports @/lib/requests works under test as it does in your app. When you change those paths, change the config to match: init's summary names the ones it copied.

An app that runs vitest already keeps its own setup, and init says what kindgi test needs from it: a vitest.config.* at the root whose test.include covers kindgi/**/*.test.ts, with the aliases your tools import by.

kindgi build turns the pack into an image. It installs your app's production dependencies with install scripts off, so your app's own postinstall and prepare scripts don't run there, and the build says so:

✓ The app's own install scripts don't run in the image: postinstall (`prisma generate`)

When the image needs what such a script does, say so under image in kindgi.config.ts.

Prisma. A tool that uses Prisma's client needs prisma generate in the image. Without it the build fails when it loads the pack:

kindgi-index: file-import-failed: Failed to import kindgi/tools/find-order/index.ts: Error: @prisma/client did not initialize yet. Please run "prisma generate" and try to import it again.

prisma() copies the schema into the image and runs prisma generate after the install. Ask your coding agent: "Run prisma generate in the pack's image." For reference, it adds this to kindgi.config.ts:

// in kindgi.config.ts
import { prisma } from '@kindgi/sdk/build';
const config = {
// pack, discovery and environments, as kindgi init wrote them
image: {
extensions: [prisma({ schema: 'prisma/schema.prisma' })],
},
};
Excerpt

With a prisma.config.ts, name it too: prisma({ schema: 'prisma/schema.prisma', config: 'prisma.config.ts' }).

Also under image:

  • systemPackages: Debian packages the image needs, such as ['tesseract-ocr'].
  • buildEnv: placeholder variables for the build steps and for loading your modules while building, such as { DATABASE_URL: 'postgresql://build-placeholder' }. Never secrets: they're set while building only, and the final image doesn't have them.
  • patch-package: when a skipped script runs it, kindgi build prints the build step to add (defineBuildExtension, also from @kindgi/sdk/build). With pnpm, pnpm patch applies your patches in the install itself.

The build ends with the image and a check of what's in it:

✓ Built kindgi-pack/acme-orders:20261008.193804 (sha256:9e102b60f643…)
✓ /app/index.json in the image matches the local index byte for byte
Excerpt

Self-host with Docker pushes, signs and deploys it.