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 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 npm package is not the installer yet — it will install the signed program
7
- for your platform. Until then, install it from
8
- https://github.com/Seventhwalllab/anairyspirit — a macOS disk image, Homebrew,
9
- Debian/Ubuntu and Fedora/openSUSE packages, an archive for any Linux, or Docker:
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
- docker pull ghcr.io/seventhwalllab/anairyspirit:latest
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.0.2",
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": ["ai", "memory", "mcp", "agents", "knowledge-graph", "local-first"],
12
- "files": ["README.md"]
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
+ }