kavach-ai 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 +168 -0
- package/bin/kavach.js +70 -0
- package/package.json +37 -0
- package/scripts/platform.js +122 -0
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# Kavach
|
|
2
|
+
|
|
3
|
+
Your agents, your data, your machine.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g kavach-ai
|
|
7
|
+
kavach install model # free local model, ~1.5GB
|
|
8
|
+
kavach open # studio at http://localhost:5181
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The package is `kavach-ai`; the command it installs is `kavach`. (`kavach` on
|
|
12
|
+
npm belongs to an unrelated project.)
|
|
13
|
+
|
|
14
|
+
The binary ships inside a small per-platform package that npm selects
|
|
15
|
+
automatically, so there is no download step and nothing to fetch outside the
|
|
16
|
+
registry — it installs cleanly behind a corporate proxy.
|
|
17
|
+
|
|
18
|
+
No account needed for the free tier. The model runs on your CPU, works offline,
|
|
19
|
+
and costs nothing — forever.
|
|
20
|
+
|
|
21
|
+
## What runs where
|
|
22
|
+
|
|
23
|
+
Kavach is a **hosted control plane** with a **local data plane**. The split is
|
|
24
|
+
the point of the product:
|
|
25
|
+
|
|
26
|
+
| | Where it lives |
|
|
27
|
+
|---|---|
|
|
28
|
+
| Chat sessions and messages | your machine |
|
|
29
|
+
| Knowledge base — files, chunks, embeddings | your machine |
|
|
30
|
+
| Agents you write, and their prompts | your machine |
|
|
31
|
+
| Automations and workflow definitions | your machine |
|
|
32
|
+
| Run history, traces, outputs | your machine |
|
|
33
|
+
| Your model API keys | your machine, encrypted at rest |
|
|
34
|
+
| Login, entitlement, billing | Kavach |
|
|
35
|
+
| Licensed marketplace templates | Kavach |
|
|
36
|
+
|
|
37
|
+
Your documents are chunked, embedded, and searched locally. They are never
|
|
38
|
+
uploaded. Kavach could not produce them under subpoena, because Kavach never
|
|
39
|
+
had them. The client is never issued a credential that can read Kavach's
|
|
40
|
+
database — the control-plane API has no endpoint that returns tenant content,
|
|
41
|
+
so a stolen token grants access to nothing.
|
|
42
|
+
|
|
43
|
+
## Cost tiers
|
|
44
|
+
|
|
45
|
+
The composer's toggle picks how a turn is served:
|
|
46
|
+
|
|
47
|
+
- **free** — the bundled local model. Zero cost, works with the network
|
|
48
|
+
unplugged, needs no login.
|
|
49
|
+
- **auto** — scores the task, then routes to the cheapest connected model that
|
|
50
|
+
can handle it. The default.
|
|
51
|
+
- **paid** — always the strongest model you have connected.
|
|
52
|
+
|
|
53
|
+
`auto` explains itself: hover the model chip to see which model answered and
|
|
54
|
+
why.
|
|
55
|
+
|
|
56
|
+
## Commands
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
kavach open start the daemon and open the studio
|
|
60
|
+
kavach serve start the daemon without opening a browser
|
|
61
|
+
kavach install model download the local model that fits this machine
|
|
62
|
+
kavach install whisper add local speech-to-text (~148MB)
|
|
63
|
+
kavach install router add the routing model used by `auto` (~500MB)
|
|
64
|
+
kavach install engine the inference engine only, without a model
|
|
65
|
+
kavach model list show the models this machine can run
|
|
66
|
+
kavach model use <id> switch local models
|
|
67
|
+
kavach doctor check the install and report anything wrong
|
|
68
|
+
kavach status show entitlement and configuration
|
|
69
|
+
kavach login link this install to a Kavach account
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Local models
|
|
73
|
+
|
|
74
|
+
`kavach install model` probes the machine and picks:
|
|
75
|
+
|
|
76
|
+
| ID | Model | Download | Needs |
|
|
77
|
+
|---|---|---|---|
|
|
78
|
+
| `qwen3.5-2b` | Qwen3.5-2B, Opus-distilled | 1.5GB | 3GB free RAM, 2 cores |
|
|
79
|
+
| `qwen3.5-4b` | Qwen3.5-4B v2, Opus-distilled | 2.6GB | 6GB free RAM, 4 cores |
|
|
80
|
+
|
|
81
|
+
Selection is deliberately conservative and judged on *available* memory, not
|
|
82
|
+
installed: a machine with 16GB installed and 1GB free will OOM exactly like a
|
|
83
|
+
2GB machine. If the machine later has room for a better model, `kavach doctor`
|
|
84
|
+
says so.
|
|
85
|
+
|
|
86
|
+
These are small models. They are good at chat, summarisation, and extraction,
|
|
87
|
+
and they will visibly struggle with long tool-calling agent runs. That is what
|
|
88
|
+
`auto` is for.
|
|
89
|
+
|
|
90
|
+
## How `auto` decides
|
|
91
|
+
|
|
92
|
+
By default `auto` scores a request with a built-in heuristic: deterministic,
|
|
93
|
+
offline, no download, no added latency.
|
|
94
|
+
|
|
95
|
+
An optional routing model (Qwen3.5-0.8B plus the `acrouter` LoRA) scores more
|
|
96
|
+
accurately. It is **not** a download — upstream publishes the adapter as
|
|
97
|
+
safetensors against a safetensors base, and llama.cpp needs GGUF for both, so
|
|
98
|
+
building it is a one-time local conversion. `kavach install router` prints the
|
|
99
|
+
exact commands.
|
|
100
|
+
|
|
101
|
+
Routing never fails closed: if the model is absent, cold, or slow, `auto`
|
|
102
|
+
falls back to the heuristic. Failing to decide which model answers would mean
|
|
103
|
+
not answering at all.
|
|
104
|
+
|
|
105
|
+
## Speech to text
|
|
106
|
+
|
|
107
|
+
`kavach install whisper` adds whisper.cpp. Prebuilt engines are not published
|
|
108
|
+
for every platform; where none exists the command says so and prints the two
|
|
109
|
+
commands that build it, rather than reporting success and leaving you with a
|
|
110
|
+
microphone button that fails.
|
|
111
|
+
|
|
112
|
+
Dictated audio is transcribed on your machine and never leaves it — a voice
|
|
113
|
+
note is exactly the kind of content this architecture exists to keep local. The
|
|
114
|
+
transcriber starts on first use, so installs that never touch the microphone
|
|
115
|
+
pay nothing for it.
|
|
116
|
+
|
|
117
|
+
## Trial and billing
|
|
118
|
+
|
|
119
|
+
The trial is **7 days**, and the clock lives on Kavach's servers keyed to your
|
|
120
|
+
installation — reinstalling or deleting `~/.kavach` does not restart it.
|
|
121
|
+
|
|
122
|
+
When a trial or subscription lapses, Kavach becomes **read-only**:
|
|
123
|
+
|
|
124
|
+
- everything you created is still there and still readable
|
|
125
|
+
- writes are refused, and scheduled agents stop running
|
|
126
|
+
- nothing is deleted, and billing keeps working so you can resume
|
|
127
|
+
|
|
128
|
+
**Your local model and local data are never gated.** A personal install has no
|
|
129
|
+
login, no entitlement and no network requirement; `kavach chat` works forever,
|
|
130
|
+
lapsed subscription or not. Read-only applies only to a deployment running in
|
|
131
|
+
org mode, where Kavach is serving a team.
|
|
132
|
+
|
|
133
|
+
## Automations
|
|
134
|
+
|
|
135
|
+
Scheduled automations in personal mode **run only while this machine is
|
|
136
|
+
awake**. A missed schedule fires once on wake, marked as a catch-up, rather
|
|
137
|
+
than replaying every slot that was missed while the laptop was closed.
|
|
138
|
+
|
|
139
|
+
For schedules that must be reliable, deploy the same binary on always-on
|
|
140
|
+
infrastructure you own (a VM, your Docker host, your cluster) and point the
|
|
141
|
+
team's browsers at it. Same product, same data ownership, plus shared knowledge
|
|
142
|
+
bases, shared agents, and cron that actually fires.
|
|
143
|
+
|
|
144
|
+
## Environment
|
|
145
|
+
|
|
146
|
+
| Variable | Effect |
|
|
147
|
+
|---|---|
|
|
148
|
+
| `KAVACH_HOME` | Install root. Default `~/.kavach` |
|
|
149
|
+
| `KAVACH_LLAMA_SERVER` | Use your own llama.cpp build (e.g. compiled for your GPU) |
|
|
150
|
+
| `KAVACH_WHISPER_SERVER` | Use your own whisper.cpp build |
|
|
151
|
+
| `KAVACH_LLAMA_RELEASE_BASE` | Mirror for llama.cpp engine downloads |
|
|
152
|
+
| `KAVACH_WHISPER_RELEASE_BASE` | Mirror for whisper.cpp engine downloads |
|
|
153
|
+
| `KAVACH_BINARY` | Run a specific binary, e.g. one you built yourself |
|
|
154
|
+
| `KAVACH_LOG` | Log filter, e.g. `kavach_client=debug` |
|
|
155
|
+
|
|
156
|
+
## Uninstalling
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
npm uninstall -g kavach-ai
|
|
160
|
+
rm -rf ~/.kavach
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Everything Kavach stored is under that one directory, which is also all you
|
|
164
|
+
need to back up.
|
|
165
|
+
|
|
166
|
+
## Licence
|
|
167
|
+
|
|
168
|
+
MIT.
|
package/bin/kavach.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Launcher. Hands every argument to the native daemon.
|
|
6
|
+
*
|
|
7
|
+
* The wrapper stays as thin as possible: no argument parsing, no config
|
|
8
|
+
* reading, no business logic. Anything implemented here would have to be
|
|
9
|
+
* implemented a second time for people who run the binary directly, and the
|
|
10
|
+
* two copies would drift.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { spawn } = require('child_process');
|
|
14
|
+
|
|
15
|
+
const { binaryPath, developmentBinary, detect } = require('../scripts/platform');
|
|
16
|
+
|
|
17
|
+
function resolve() {
|
|
18
|
+
let info;
|
|
19
|
+
try {
|
|
20
|
+
info = detect();
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// Unsupported platform — detect() already explains what to do.
|
|
23
|
+
console.error(error.message);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const installed = binaryPath();
|
|
28
|
+
if (installed) return installed;
|
|
29
|
+
|
|
30
|
+
// Working on the daemon itself: a locally-built binary beats reinstalling.
|
|
31
|
+
const local = developmentBinary();
|
|
32
|
+
if (local) return local;
|
|
33
|
+
|
|
34
|
+
console.error(
|
|
35
|
+
`kavach: the binary for ${info.key} is not installed.\n\n` +
|
|
36
|
+
`It ships in ${info.package}, an optional dependency of this package.\n` +
|
|
37
|
+
`Installers skip optional dependencies when given --no-optional or\n` +
|
|
38
|
+
`--omit=optional, and some CI caches drop them.\n\n` +
|
|
39
|
+
` Fix: npm install -g kavach-ai --include=optional\n` +
|
|
40
|
+
` Build: cargo build --release -p kavach-client\n`
|
|
41
|
+
);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function main() {
|
|
46
|
+
const child = spawn(resolve(), process.argv.slice(2), { stdio: 'inherit' });
|
|
47
|
+
|
|
48
|
+
// Forward termination so Ctrl-C in the terminal stops the daemon rather
|
|
49
|
+
// than orphaning it holding the port and gigabytes of model memory.
|
|
50
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
51
|
+
process.on(signal, () => child.kill(signal));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
child.on('exit', (code, signal) => {
|
|
55
|
+
// Reproduce signal death as the conventional 128+n so shell scripts and
|
|
56
|
+
// supervisors see what actually happened.
|
|
57
|
+
process.exit(signal ? 128 + signalNumber(signal) : code ?? 0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
child.on('error', (error) => {
|
|
61
|
+
console.error(`kavach: could not start the daemon — ${error.message}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function signalNumber(signal) {
|
|
67
|
+
return { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGKILL: 9, SIGTERM: 15 }[signal] ?? 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kavach-ai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Kavach — your agents, your data, your machine. Runs a local AI model and studio on your own hardware.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"agents",
|
|
8
|
+
"local-first",
|
|
9
|
+
"llm",
|
|
10
|
+
"privacy",
|
|
11
|
+
"knowledge-base"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"homepage": "https://bluehawks.ai/kavach",
|
|
15
|
+
"bin": {
|
|
16
|
+
"kavach": "bin/kavach.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"bin/",
|
|
20
|
+
"scripts/platform.js",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "node --test \"scripts/*.test.js\"",
|
|
25
|
+
"build:platforms": "node scripts/build-platform-packages.js",
|
|
26
|
+
"prepublishOnly": "node scripts/no-publish.js"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"optionalDependencies": {
|
|
32
|
+
"@kavach-ai/cli-darwin-arm64": "0.1.0",
|
|
33
|
+
"@kavach-ai/cli-darwin-x64": "0.1.0",
|
|
34
|
+
"@kavach-ai/cli-linux-x64": "0.1.0",
|
|
35
|
+
"@kavach-ai/cli-linux-arm64": "0.1.0"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Locating the native binary.
|
|
5
|
+
*
|
|
6
|
+
* ## Why the binary ships inside npm packages rather than being downloaded
|
|
7
|
+
*
|
|
8
|
+
* The obvious design is a postinstall that fetches a release asset from
|
|
9
|
+
* GitHub. It cannot work here: the source repository is private, and a private
|
|
10
|
+
* repo's release assets are not anonymously downloadable, so every install
|
|
11
|
+
* would 404. Publishing the binaries to a second public repo would mean
|
|
12
|
+
* running release infrastructure whose only job is to be public.
|
|
13
|
+
*
|
|
14
|
+
* So the binaries are published as per-platform npm packages —
|
|
15
|
+
* `@kavach-ai/cli-linux-x64` and friends — and this package lists them as
|
|
16
|
+
* `optionalDependencies`. npm reads each one's `os`/`cpu` fields and installs
|
|
17
|
+
* only the one that matches, skipping the rest. This is what esbuild, swc and
|
|
18
|
+
* turbo do, and it buys three things beyond solving the private-repo problem:
|
|
19
|
+
* no network call outside the registry (so it works behind a corporate proxy
|
|
20
|
+
* that allows npm and nothing else), no checksum logic to get wrong, and an
|
|
21
|
+
* install that either fully succeeds or fully fails.
|
|
22
|
+
*
|
|
23
|
+
* Only compiled binaries are published. No source of any kind is in the
|
|
24
|
+
* tarballs — see `files` in package.json and the platform-package generator.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Platform package per `${process.platform}-${process.arch}`.
|
|
32
|
+
*
|
|
33
|
+
* Kept as a literal map rather than built by string concatenation so that
|
|
34
|
+
* `require.resolve` sees a static name and bundlers do not have to guess.
|
|
35
|
+
*/
|
|
36
|
+
const PACKAGES = {
|
|
37
|
+
'darwin-arm64': '@kavach-ai/cli-darwin-arm64',
|
|
38
|
+
'darwin-x64': '@kavach-ai/cli-darwin-x64',
|
|
39
|
+
'linux-x64': '@kavach-ai/cli-linux-x64',
|
|
40
|
+
'linux-arm64': '@kavach-ai/cli-linux-arm64',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Rust target triples, used by the release build and the generator. */
|
|
44
|
+
const TARGETS = {
|
|
45
|
+
'darwin-arm64': 'aarch64-apple-darwin',
|
|
46
|
+
'darwin-x64': 'x86_64-apple-darwin',
|
|
47
|
+
'linux-x64': 'x86_64-unknown-linux-gnu',
|
|
48
|
+
'linux-arm64': 'aarch64-unknown-linux-gnu',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Windows (`win32-x64` / `x86_64-pc-windows-msvc`) is deliberately absent as of
|
|
52
|
+
// 0.1.0. It is not unsupported in principle — the `.exe` handling below and in
|
|
53
|
+
// the platform-package generator is intact and the Rust target builds — there
|
|
54
|
+
// was simply no Windows machine available to build and smoke-test on when this
|
|
55
|
+
// released, and shipping a Windows binary nobody has ever run is worse than
|
|
56
|
+
// shipping none: `detect` below tells those users to build from source, whereas
|
|
57
|
+
// a listed-but-missing package installs cleanly and then cannot find its
|
|
58
|
+
// binary. Restore both entries above once a Windows build is verified.
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the platform package for the current machine.
|
|
62
|
+
*
|
|
63
|
+
* @throws {Error} naming the supported set, on an unsupported platform
|
|
64
|
+
*/
|
|
65
|
+
function detect(platform = process.platform, arch = process.arch) {
|
|
66
|
+
const key = `${platform}-${arch}`;
|
|
67
|
+
const pkg = PACKAGES[key];
|
|
68
|
+
|
|
69
|
+
if (!pkg) {
|
|
70
|
+
// "Unsupported platform" with no further detail is the message that
|
|
71
|
+
// generates support tickets. Name what is supported and the way out.
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Kavach has no prebuilt binary for ${key}.\n` +
|
|
74
|
+
`Supported: ${Object.keys(PACKAGES).join(', ')}.\n` +
|
|
75
|
+
`You can build from source: cargo build --release -p kavach-client`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
key,
|
|
81
|
+
package: pkg,
|
|
82
|
+
target: TARGETS[key],
|
|
83
|
+
binaryName: platform === 'win32' ? 'kavach.exe' : 'kavach',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Absolute path to the installed binary, or `null` when the platform package
|
|
89
|
+
* is not present.
|
|
90
|
+
*
|
|
91
|
+
* `null` rather than a throw: a missing optional dependency is a recoverable
|
|
92
|
+
* install problem with a known fix, and the caller prints that fix.
|
|
93
|
+
*/
|
|
94
|
+
function binaryPath(platform = process.platform, arch = process.arch) {
|
|
95
|
+
const { package: pkg, binaryName } = detect(platform, arch);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Resolve the package's own manifest rather than a path inside it — that
|
|
99
|
+
// works regardless of how the installer laid the tree out (hoisted,
|
|
100
|
+
// nested, pnpm's symlinked store).
|
|
101
|
+
const manifest = require.resolve(`${pkg}/package.json`, { paths: [__dirname] });
|
|
102
|
+
const candidate = path.join(path.dirname(manifest), 'bin', binaryName);
|
|
103
|
+
return fs.existsSync(candidate) ? candidate : null;
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Where a locally-built binary lives, for developers working on the daemon. */
|
|
110
|
+
function developmentBinary() {
|
|
111
|
+
const override = process.env.KAVACH_BINARY;
|
|
112
|
+
if (override) return fs.existsSync(override) ? override : null;
|
|
113
|
+
|
|
114
|
+
const { binaryName } = detect();
|
|
115
|
+
for (const profile of ['dist', 'release', 'debug']) {
|
|
116
|
+
const candidate = path.resolve(__dirname, '..', '..', '..', 'target', profile, binaryName);
|
|
117
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { PACKAGES, TARGETS, detect, binaryPath, developmentBinary };
|