anairyspirit 0.0.2 → 0.1.391
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 +44 -7
- package/bin/anairyspirit.js +30 -0
- package/lib/core.js +243 -0
- package/package.json +30 -4
- package/sums.json +8 -0
package/README.md
CHANGED
|
@@ -1,14 +1,51 @@
|
|
|
1
1
|
# anairyspirit
|
|
2
2
|
|
|
3
3
|
A personal AI memory graph that runs on your own machine. Your AI agents keep
|
|
4
|
-
long-term memory in it over MCP and REST.
|
|
4
|
+
long-term memory in it over MCP and REST; nothing leaves the machine.
|
|
5
5
|
|
|
6
|
-
This
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
This package installs it and runs it — macOS and Linux (x86_64, arm64), Node 18
|
|
7
|
+
or newer. It carries no program itself: it downloads the release it was built
|
|
8
|
+
for from GitHub and checks it against the checksums of that release's signed
|
|
9
|
+
manifest before anything runs.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx anairyspirit setup
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`setup` asks about encryption and the rest, installs the program as
|
|
18
|
+
`anairyspirit` (in `/usr/local/bin`, or `~/.local/bin` — then open a new
|
|
19
|
+
terminal) and starts its service.
|
|
20
|
+
|
|
21
|
+
## Use it from an AI client
|
|
22
|
+
|
|
23
|
+
Once installed, every command goes to the installed program, so an MCP client
|
|
24
|
+
can start it like this:
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{ "command": "npx", "args": ["-y", "anairyspirit", "mcp"] }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Update
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx anairyspirit@latest update
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
installs the newest release over yours — your memory, settings and keys stay.
|
|
37
|
+
An install made by Homebrew or a package is updated the way it came; the
|
|
38
|
+
program says how.
|
|
39
|
+
|
|
40
|
+
## Installed with `npm i -g`
|
|
41
|
+
|
|
42
|
+
The command is then `anairyspirit-npm` (the program itself is `anairyspirit`,
|
|
43
|
+
and the two must not share a name): `anairyspirit-npm setup`,
|
|
44
|
+
`anairyspirit-npm update`.
|
|
45
|
+
|
|
46
|
+
Other ways to install — a macOS disk image, Homebrew, Debian/Ubuntu and
|
|
47
|
+
Fedora/openSUSE packages, Docker — and the manual:
|
|
48
|
+
https://github.com/Seventhwalllab/anairyspirit
|
|
12
49
|
|
|
13
50
|
The program is distributed under its own licence agreement (EULA), shipped with
|
|
14
|
-
every release
|
|
51
|
+
every release and shown by `setup`.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// `anairyspirit` from npm: see lib/core.js for what it does and does not do.
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { main } = require('../lib/core.js');
|
|
8
|
+
|
|
9
|
+
const sumsFile = path.join(__dirname, '..', 'sums.json');
|
|
10
|
+
if (!fs.existsSync(sumsFile)) {
|
|
11
|
+
process.stderr.write('anairyspirit (npm): this copy carries no release (sums.json is made when the package is built).\n');
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
const sums = JSON.parse(fs.readFileSync(sumsFile, 'utf8'));
|
|
15
|
+
|
|
16
|
+
main(process.argv.slice(2), {
|
|
17
|
+
sums,
|
|
18
|
+
env: process.env,
|
|
19
|
+
home: os.homedir(),
|
|
20
|
+
platform: process.platform,
|
|
21
|
+
arch: process.arch,
|
|
22
|
+
self: __filename,
|
|
23
|
+
baseUrl: process.env.ANAIRYSPIRIT_NPM_BASE_URL,
|
|
24
|
+
}).then(
|
|
25
|
+
(code) => process.exit(code),
|
|
26
|
+
(err) => {
|
|
27
|
+
process.stderr.write(`anairyspirit (npm): ${err.message}\n`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
},
|
|
30
|
+
);
|
package/lib/core.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// The npm package of anairyspirit is an installer and a launcher, not the
|
|
3
|
+
// program: it carries no binary. It knows one release — the version in
|
|
4
|
+
// sums.json, with the sha256 of that release's archives, taken from the
|
|
5
|
+
// release's GPG-signed SHA256SUMS when the package was built — and does three
|
|
6
|
+
// things:
|
|
7
|
+
//
|
|
8
|
+
// * nothing installed yet: `anairyspirit` / `anairyspirit setup` downloads
|
|
9
|
+
// the archive for this machine from the GitHub release, checks its sha256,
|
|
10
|
+
// unpacks it and runs `./anairyspirit setup` from it — the same install as
|
|
11
|
+
// from the archive by hand;
|
|
12
|
+
// * installed (any way — disk image, Homebrew, .deb/.rpm, archive): every
|
|
13
|
+
// command goes to the installed program, so `npx -y anairyspirit mcp`
|
|
14
|
+
// works in an MCP client's configuration;
|
|
15
|
+
// * `anairyspirit update`: when the installed program is older than this
|
|
16
|
+
// package's release, downloads and checks that release and hands it to
|
|
17
|
+
// `anairyspirit update --from`, the program's own update.
|
|
18
|
+
//
|
|
19
|
+
// Its one command is `anairyspirit-npm`, never `anairyspirit`: `npm i -g` puts
|
|
20
|
+
// its commands into npm's bin folder — /usr/local/bin, /opt/homebrew/bin or
|
|
21
|
+
// /usr/bin — and `setup` installs the program as /usr/local/bin/anairyspirit
|
|
22
|
+
// (or ~/.local/bin). Under one name the copy would write through npm's link
|
|
23
|
+
// into this package, and in /usr/bin the program would take itself for a
|
|
24
|
+
// system package. `npx anairyspirit …` still runs it: npx runs a package's
|
|
25
|
+
// only command whatever its name.
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const os = require('os');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
const crypto = require('crypto');
|
|
31
|
+
const http = require('http');
|
|
32
|
+
const https = require('https');
|
|
33
|
+
const { spawn, spawnSync } = require('child_process');
|
|
34
|
+
|
|
35
|
+
const RELEASES = 'https://github.com/Seventhwalllab/anairyspirit/releases/download';
|
|
36
|
+
const TAG = 'anairyspirit (npm)';
|
|
37
|
+
|
|
38
|
+
/** The release archive for this platform, or null where there is none. */
|
|
39
|
+
function archiveFor(platform, arch, version) {
|
|
40
|
+
if (platform === 'darwin') {
|
|
41
|
+
// One universal archive for both Mac arches, flat: `anairyspirit` at its root.
|
|
42
|
+
return { file: `anairyspirit-${version}-macos-universal.tar.gz`, dir: '' };
|
|
43
|
+
}
|
|
44
|
+
if (platform === 'linux') {
|
|
45
|
+
const a = { x64: 'x86_64', arm64: 'arm64' }[arch];
|
|
46
|
+
if (!a) return null;
|
|
47
|
+
return { file: `anairyspirit-${version}-${a}.tar.gz`, dir: `anairyspirit-${version}-${a}` };
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The places installs put the program, beside PATH. */
|
|
53
|
+
function usualDirs(home) {
|
|
54
|
+
return ['/usr/local/bin', path.join(home, '.local', 'bin'), '/usr/bin',
|
|
55
|
+
'/opt/homebrew/bin', '/home/linuxbrew/.linuxbrew/bin'];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Where an installed program may be: PATH first, then `extra` (default: usualDirs). */
|
|
59
|
+
function candidates(env, home, extra) {
|
|
60
|
+
const dirs = (env.PATH || '').split(path.delimiter).filter(Boolean);
|
|
61
|
+
dirs.push(...(extra || usualDirs(home)));
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const d of dirs) {
|
|
65
|
+
const f = path.join(d, 'anairyspirit');
|
|
66
|
+
if (!seen.has(f)) { seen.add(f); out.push(f); }
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function realOrNull(f) {
|
|
72
|
+
try { return fs.realpathSync(f); } catch { return null; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A Node launcher — this one, or a copy of it in another npm prefix or npx cache. */
|
|
76
|
+
function isLauncher(file, selfReal) {
|
|
77
|
+
const real = realOrNull(file);
|
|
78
|
+
if (!real || real === selfReal) return true;
|
|
79
|
+
try {
|
|
80
|
+
const fd = fs.openSync(real, 'r');
|
|
81
|
+
const head = Buffer.alloc(64);
|
|
82
|
+
const n = fs.readSync(fd, head, 0, 64, 0);
|
|
83
|
+
fs.closeSync(fd);
|
|
84
|
+
const s = head.subarray(0, n).toString('latin1');
|
|
85
|
+
return s.startsWith('#!') && s.includes('node');
|
|
86
|
+
} catch {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The installed program, if there is one. */
|
|
92
|
+
function findInstalled(env, home, selfFile, extra) {
|
|
93
|
+
const selfReal = realOrNull(selfFile);
|
|
94
|
+
for (const f of candidates(env, home, extra)) {
|
|
95
|
+
try {
|
|
96
|
+
fs.accessSync(f, fs.constants.X_OK);
|
|
97
|
+
if (!fs.statSync(f).isFile()) continue;
|
|
98
|
+
} catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (isLauncher(f, selfReal)) continue;
|
|
102
|
+
return f;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** `Anairyspirit v0.1.391` → [0, 1, 391]; null when it says no version. */
|
|
108
|
+
function parseVersion(text) {
|
|
109
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(text || '');
|
|
110
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function compareVersions(a, b) {
|
|
114
|
+
for (let i = 0; i < 3; i++) if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Download `url` to `dest` (following redirects); resolves to its sha256. */
|
|
119
|
+
function download(url, dest, redirects = 5) {
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
const mod = url.startsWith('http://') ? http : https;
|
|
122
|
+
const req = mod.get(url, { headers: { 'User-Agent': 'anairyspirit-npm' } }, (res) => {
|
|
123
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
|
|
124
|
+
res.resume();
|
|
125
|
+
if (redirects <= 0) return reject(new Error(`too many redirects for ${url}`));
|
|
126
|
+
return resolve(download(new URL(res.headers.location, url).toString(), dest, redirects - 1));
|
|
127
|
+
}
|
|
128
|
+
if (res.statusCode !== 200) {
|
|
129
|
+
res.resume();
|
|
130
|
+
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
|
|
131
|
+
}
|
|
132
|
+
const hash = crypto.createHash('sha256');
|
|
133
|
+
const out = fs.createWriteStream(dest);
|
|
134
|
+
res.on('data', (chunk) => hash.update(chunk));
|
|
135
|
+
res.on('error', reject);
|
|
136
|
+
out.on('error', reject);
|
|
137
|
+
out.on('finish', () => resolve(hash.digest('hex')));
|
|
138
|
+
res.pipe(out);
|
|
139
|
+
});
|
|
140
|
+
req.on('error', reject);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Download this package's release for this machine into a fresh temporary
|
|
146
|
+
* folder, check it against sums.json and unpack it. Returns the folder, the
|
|
147
|
+
* unpacked directory and the program in it. Nothing is unpacked unless the
|
|
148
|
+
* sha256 matches.
|
|
149
|
+
*/
|
|
150
|
+
async function fetchRelease(sums, opts) {
|
|
151
|
+
const a = archiveFor(opts.platform, opts.arch, sums.version);
|
|
152
|
+
if (!a) throw new Error(`no release archive for ${opts.platform}/${opts.arch}`);
|
|
153
|
+
const expected = sums.files[a.file];
|
|
154
|
+
if (!expected) throw new Error(`this package lists no checksum for ${a.file}`);
|
|
155
|
+
const base = opts.baseUrl || `${RELEASES}/v${sums.version}`;
|
|
156
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'anairyspirit-npm-'));
|
|
157
|
+
const archive = path.join(tmp, a.file);
|
|
158
|
+
opts.log(`Downloading ${a.file} …`);
|
|
159
|
+
let got;
|
|
160
|
+
try {
|
|
161
|
+
got = await download(`${base}/${a.file}`, archive);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
164
|
+
throw e;
|
|
165
|
+
}
|
|
166
|
+
if (got !== expected) {
|
|
167
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
168
|
+
throw new Error(`${a.file}: sha256 ${got} is not the release's ${expected} — nothing was installed`);
|
|
169
|
+
}
|
|
170
|
+
opts.log('Checksum matches the release.');
|
|
171
|
+
const x = spawnSync('tar', ['-xzf', archive, '-C', tmp], { stdio: 'inherit' });
|
|
172
|
+
if (x.status !== 0) {
|
|
173
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
174
|
+
throw new Error(`could not unpack ${a.file}`);
|
|
175
|
+
}
|
|
176
|
+
const dir = path.join(tmp, a.dir);
|
|
177
|
+
return { tmp, dir, program: path.join(dir, 'anairyspirit') };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Run a program with this process's terminal; resolves to its exit code. */
|
|
181
|
+
function run(program, args, cwd) {
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
const child = spawn(program, args, { stdio: 'inherit', cwd });
|
|
184
|
+
const forward = (sig) => () => { try { child.kill(sig); } catch { /* gone */ } };
|
|
185
|
+
const handlers = { SIGINT: forward('SIGINT'), SIGTERM: forward('SIGTERM'), SIGHUP: forward('SIGHUP') };
|
|
186
|
+
for (const [sig, h] of Object.entries(handlers)) process.on(sig, h);
|
|
187
|
+
const done = () => { for (const [sig, h] of Object.entries(handlers)) process.removeListener(sig, h); };
|
|
188
|
+
child.on('error', (e) => { done(); reject(e); });
|
|
189
|
+
child.on('exit', (code, signal) => {
|
|
190
|
+
done();
|
|
191
|
+
resolve(code !== null ? code : 128 + (os.constants.signals[signal] || 0));
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function main(args, opts) {
|
|
197
|
+
const log = opts.log || ((m) => process.stderr.write(`${m}\n`));
|
|
198
|
+
const o = { ...opts, log };
|
|
199
|
+
if (!archiveFor(o.platform, o.arch, o.sums.version)) {
|
|
200
|
+
log(`${TAG}: anairyspirit runs on macOS and on Linux (x86_64, arm64); this is ${o.platform}/${o.arch}.`);
|
|
201
|
+
log('On Windows, run it in Docker: https://github.com/Seventhwalllab/anairyspirit#install');
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
const installed = findInstalled(o.env, o.home, o.self, o.extraDirs);
|
|
205
|
+
|
|
206
|
+
if (!installed) {
|
|
207
|
+
if (args.length === 0 || args[0] === 'setup') {
|
|
208
|
+
const r = await fetchRelease(o.sums, o);
|
|
209
|
+
try {
|
|
210
|
+
return await run(r.program, ['setup', ...args.slice(1)], r.dir);
|
|
211
|
+
} finally {
|
|
212
|
+
fs.rmSync(r.tmp, { recursive: true, force: true });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
log(`${TAG}: anairyspirit is not installed on this machine yet.`);
|
|
216
|
+
log('Install it first: npx anairyspirit setup');
|
|
217
|
+
return 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (args[0] === 'update' && !args.includes('--from')) {
|
|
221
|
+
const v = spawnSync(installed, ['--version'], { encoding: 'utf8' });
|
|
222
|
+
const have = parseVersion(v.stdout);
|
|
223
|
+
const want = parseVersion(o.sums.version);
|
|
224
|
+
if (have && compareVersions(have, want) >= 0) {
|
|
225
|
+
log(`${TAG}: the installed anairyspirit is v${have.join('.')}; this package carries v${o.sums.version} — nothing newer here.`);
|
|
226
|
+
log('For the newest release: npx anairyspirit@latest update');
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
229
|
+
const r = await fetchRelease(o.sums, o);
|
|
230
|
+
try {
|
|
231
|
+
return await run(installed, ['update', '--from', r.program, ...args.slice(1)], r.dir);
|
|
232
|
+
} finally {
|
|
233
|
+
fs.rmSync(r.tmp, { recursive: true, force: true });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return run(installed, args);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = {
|
|
241
|
+
archiveFor, usualDirs, candidates, isLauncher, findInstalled, parseVersion, compareVersions,
|
|
242
|
+
download, fetchRelease, run, main,
|
|
243
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anairyspirit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.391",
|
|
4
4
|
"description": "Personal AI memory graph that runs locally: long-term memory for your AI agents over MCP and REST",
|
|
5
5
|
"homepage": "https://github.com/Seventhwalllab/anairyspirit",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/Seventhwalllab/anairyspirit.git"
|
|
8
|
+
"url": "git+https://github.com/Seventhwalllab/anairyspirit.git",
|
|
9
|
+
"directory": "npm"
|
|
9
10
|
},
|
|
10
11
|
"license": "SEE LICENSE IN README.md",
|
|
11
|
-
"keywords": [
|
|
12
|
-
|
|
12
|
+
"keywords": [
|
|
13
|
+
"ai",
|
|
14
|
+
"memory",
|
|
15
|
+
"mcp",
|
|
16
|
+
"agents",
|
|
17
|
+
"knowledge-graph",
|
|
18
|
+
"local-first"
|
|
19
|
+
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"anairyspirit-npm": "bin/anairyspirit.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin",
|
|
25
|
+
"lib",
|
|
26
|
+
"sums.json",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"os": [
|
|
30
|
+
"darwin",
|
|
31
|
+
"linux"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"test": "node --test"
|
|
38
|
+
}
|
|
13
39
|
}
|
package/sums.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.1.391",
|
|
3
|
+
"files": {
|
|
4
|
+
"anairyspirit-0.1.391-macos-universal.tar.gz": "2f6e552bdf498641daf50cb745cd87f924318c730ef39a853b56c48a0fc08137",
|
|
5
|
+
"anairyspirit-0.1.391-arm64.tar.gz": "53001c120d0d95f01deac6f3cdd7103e619c86a7a09027cb2e739d2e03822695",
|
|
6
|
+
"anairyspirit-0.1.391-x86_64.tar.gz": "ffcacdef050ecdb2fa6a7568e6d09ed5951da093c71480e1b7a2f32af1974f69"
|
|
7
|
+
}
|
|
8
|
+
}
|