aizo-node 0.3.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.
Files changed (4) hide show
  1. package/README.md +350 -0
  2. package/install.js +138 -0
  3. package/package.json +26 -0
  4. package/run.js +19 -0
package/README.md ADDED
@@ -0,0 +1,350 @@
1
+ # aizo 爱憎
2
+
3
+ [中文文档](README.zh.md)
4
+
5
+ **aizo** (爱憎, *ài zēng*, "love and hate") is a lightweight, high-performance preference memory system for AI agents, built entirely in Rust.
6
+
7
+ It mimics human cognitive memory: rather than storing full conversation transcripts, it continuously **extracts, quantifies, decays, and recalls** a user's stable preferences, aversions, habits, communication styles, and hard limits from interaction history. The result is a compact, numerically-weighted personality profile that any agent can query in milliseconds.
8
+
9
+ ---
10
+
11
+ ## How it fits together
12
+
13
+ aizo is designed for two complementary usage loops:
14
+
15
+ ```
16
+ ╔══════════════════════════════════════════════════════════════════════╗
17
+ ║ 1. In-session (reactive — detects specific emotions in real time) ║
18
+ ╚══════════════════════════════════════════════════════════════════════╝
19
+
20
+ user ──► Claude Code ─────── aizo add ──────────────────┐
21
+
22
+ CLAUDE.md ◄── contributes ── local SQLite
23
+ (user preference)
24
+
25
+
26
+ ╔══════════════════════════════════════════════════════════════════════╗
27
+ ║ 2. Background (cron task — batch-analyzes accumulated sessions) ║
28
+ ╚══════════════════════════════════════════════════════════════════════╝
29
+
30
+ user ──► openclaw ──► sessions ─── aizo analyze ─────────┐
31
+
32
+ USER.md, SOUL.md, IDENTITY.md … ◄── contributes ── local SQLite
33
+ (user preference)
34
+ ```
35
+
36
+ **Loop 1 — In-session:** the agent detects a strong preference signal mid-conversation
37
+ (praise, complaint, explicit rule) and calls `aizo add` immediately. The updated SQLite
38
+ profile is then injected into `CLAUDE.md` (or equivalent context file) so the next
39
+ session starts with the latest understanding of the user.
40
+
41
+ **Loop 2 — Background:** other agents (openclaw, etc.) accumulate session transcripts
42
+ over time. A scheduled cron job runs `aizo analyze` to extract implicit preferences the
43
+ reactive loop may have missed. The enriched profile is then written into richer identity
44
+ files — `USER.md`, `SOUL.md`, `IDENTITY.md` — that build a persistent, evolving
45
+ picture of the user across all agents and tools.
46
+
47
+ The two loops reinforce each other: reactive writes give immediate recall accuracy;
48
+ batch analysis fills in the gaps and stabilises scores over time.
49
+
50
+ ---
51
+
52
+ ## Core design
53
+
54
+ ```
55
+ session transcript
56
+
57
+
58
+ flash LLM (claude-haiku-4-5)
59
+ │ semantic extraction
60
+
61
+ structured entries { category, item, base_score 0–10 }
62
+ │ smooth merge
63
+
64
+ SQLite (~/.aizo/preferences.db)
65
+
66
+
67
+ effective_weight = s · d(t)^α (score-modulated decay)
68
+ │ keyword or top-N recall
69
+
70
+ agent reads profile → personalizes response
71
+ ```
72
+
73
+ ### Scoring formula
74
+
75
+ All scoring logic lives in `src/scoring/mod.rs`. Every preference entry carries three computed fields, derived at read time from its `base_score` and `last_seen` timestamp.
76
+
77
+ **Step 1 — Decay coefficient** $d(t)$
78
+
79
+ $$d(t) = \phi + (1 - \phi) \cdot e^{-\lambda t}, \quad \lambda = \frac{\ln 2}{t_{1/2}}$$
80
+
81
+ where $t$ is days since `last_seen`, $t_{1/2}$ is the configured half-life, and $\phi$ is the floor.
82
+
83
+ **Step 2 — Score-dependent exponent** $\alpha$
84
+
85
+ $$\alpha = \frac{10 - s}{10}$$
86
+
87
+ Higher score → smaller $\alpha$ → decay has less effect. A score-10 preference ($\alpha = 0$) is fully decay-resistant; a score-0 entry ($\alpha = 1$) decays at full speed.
88
+
89
+ **Step 3 — Effective weight** $w$
90
+
91
+ $$w = s \cdot d(t)^{\alpha}$$
92
+
93
+ Expanding into a single expression:
94
+
95
+ $$\boxed{w = s \cdot \left[\phi + (1-\phi) \cdot e^{-\lambda t}\right]^{\frac{10-s}{10}}}$$
96
+
97
+ **Boundary behaviour**
98
+
99
+ | Score $s$ | $\alpha$ | Decay effect | Interpretation |
100
+ |---|---|---|---|
101
+ | 10 | 0.0 | None — $d^0 = 1$ | Core value, never fades |
102
+ | 7 | 0.3 | Slight | Strong preference, slow fade |
103
+ | 5 | 0.5 | Moderate | Neutral habit, fades at half speed |
104
+ | 1 | 0.9 | Near-full | Weak aversion, fades quickly |
105
+ | 0 | 1.0 | Full | $w = 0$ always — absolute zero |
106
+
107
+ Entries are **never hard-deleted by decay** — they sink toward the floor and persist as weak long-term memory. Use `--type taboo` to surface them explicitly regardless of effective weight.
108
+
109
+ ### Scoring scale (0–10)
110
+
111
+ | Score | Meaning |
112
+ |---|---|
113
+ | 0 | Absolute taboo / hard rejection |
114
+ | 1–3 | Clear dislike / aversion |
115
+ | 4–6 | Neutral tendency / weak pattern |
116
+ | 7–9 | Clear preference |
117
+ | 10 | Strong, consistent, high-priority love |
118
+
119
+ ### Score smoothing
120
+
121
+ When the same entry is seen again across sessions:
122
+ ```
123
+ new_base_score = old_base_score × 0.4 + incoming_score × 0.6
124
+ ```
125
+ `last_seen` is always refreshed, which resets the decay clock.
126
+
127
+ ---
128
+
129
+ ## Installation
130
+
131
+ ### From source (Rust ≥ 1.70)
132
+
133
+ ```bash
134
+ git clone https://github.com/mmmarcinho/aizo
135
+ cd aizo
136
+ cargo build --release
137
+ cp target/release/aizo /usr/local/bin/aizo
138
+ ```
139
+
140
+ ```bash
141
+ export ANTHROPIC_API_KEY=sk-ant-... # required for 'analyze'
142
+ ```
143
+
144
+ ---
145
+
146
+ ## CLI reference
147
+
148
+ ```
149
+ aizo [--db <path>] <COMMAND>
150
+ ```
151
+
152
+ | Command | Description |
153
+ |---|---|
154
+ | `analyze [file]` | Analyze session file or JSON export with flash LLM |
155
+ | `recall [query] [--type …] [--limit N] [--scenario …]` | Keyword + score-range recall — **primary agent call** |
156
+ | `top [N] [--type …]` | Top-N entries by effective weight (default 10) |
157
+ | `show` | Full profile sorted by effective weight |
158
+ | `add <item> <reason> [--score N]` | Manually add or update a preference |
159
+ | `tag <item> <keywords…>` | Add or replace keywords on an existing entry |
160
+ | `touch <item…>` | Reset decay clock without changing score |
161
+ | `remove <item…>` | Hard-remove an entry |
162
+ | `keywords` | List all stored keywords with entry counts |
163
+ | `clear` | Wipe entire profile and session history |
164
+ | `info` | DB path, score distribution, decay settings |
165
+ | `config show` | Print decay configuration |
166
+ | `config set-half-life <days>` | Set decay half-life |
167
+ | `config set-floor <0.0–1.0>` | Set minimum decay floor |
168
+
169
+ ### Score guide
170
+
171
+ There is no `category` field. The `base_score` is the only dimension that matters:
172
+
173
+ | Score | Meaning | `--type` alias |
174
+ |---|---|---|
175
+ | 0–1.5 | Hard limit / must never do | `taboo` |
176
+ | 1.6–4 | Clear dislike | `aversion` |
177
+ | 4–6.5 | Neutral habit or weak pattern | `habit` |
178
+ | 6.5–10 | Style / communication preference | `style` |
179
+ | 7–10 | Clear preference | `preference` |
180
+
181
+ Use `--type` on `recall` and `top` to filter by score range. Comma-separate for multi-type:
182
+
183
+ ```bash
184
+ aizo recall code --type preference,habit,style,taboo
185
+ aizo recall --type taboo # all hard limits, no keyword needed
186
+ aizo top 5 --type preference
187
+ ```
188
+
189
+ Use keywords (`--keywords` on add, or `aizo tag`) to add any taxonomy you want.
190
+
191
+ ### Examples
192
+
193
+ ```bash
194
+ # Analyze a session log
195
+ aizo analyze ./chat.txt
196
+ cat conversation.md | aizo analyze
197
+
198
+ # Agent recalls preferences before generating
199
+ aizo top 5
200
+ aizo recall "code style"
201
+
202
+ # Scenario-aware recall for coding tasks (expands to ~10 coding keywords)
203
+ aizo recall --scenario coding --type preference,style,habit,taboo --limit 20
204
+
205
+ # Type-only recall (no keyword — returns all entries in that score range)
206
+ aizo recall --type taboo # all hard limits
207
+ aizo recall code --type preference --limit 10 # top coding preferences
208
+ aizo recall code --type preference,habit --limit 20 # multiple types
209
+
210
+ # Inspect full profile
211
+ aizo show
212
+
213
+ # Manual entries — score encodes sentiment
214
+ aizo add "concise code" "Always asks for shorter implementations" --score 9.0
215
+ aizo add "verbose comments" "Complained about over-documented code" --score 1.5
216
+ aizo add "emojis in output" "Explicitly said never use emojis" --score 0.5
217
+ aizo add "uses dark mode" "Mentioned dark theme in every UI session" --score 5.0
218
+ aizo add "terse naming" "Consistently chose short variable names" --score 8.0
219
+
220
+ # Add or manage keywords for richer recall
221
+ aizo tag "concise code" brevity minimal short lean
222
+ aizo tag "verbose comments" verbosity docs comments over-engineering
223
+
224
+ # Tune decay (default: half-life 30d, floor 0.1)
225
+ aizo config set-half-life 14
226
+ aizo config set-floor 0.05
227
+
228
+ # Stats
229
+ aizo info
230
+ ```
231
+
232
+ ---
233
+
234
+ ## Entry format
235
+
236
+ ```json
237
+ {
238
+ "id": 1,
239
+ "item": "concise code",
240
+ "reason": "Always asks for shorter implementations with no fluff.",
241
+ "keywords": ["brevity", "minimal", "short", "lean"],
242
+ "base_score": 9.0,
243
+ "source": "analysis",
244
+ "added_at": "2026-05-07T14:00:00+00:00",
245
+ "last_seen": "2026-05-07T15:30:00+00:00",
246
+ "score_exponent": 0.1,
247
+ "decay_coefficient": 0.87,
248
+ "effective_weight": 7.83
249
+ }
250
+ ```
251
+
252
+ ---
253
+
254
+ ## Database schema
255
+
256
+ ```sql
257
+ CREATE TABLE preferences (
258
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
259
+ item TEXT NOT NULL,
260
+ reason TEXT NOT NULL,
261
+ keywords TEXT NOT NULL DEFAULT '', -- comma-separated synonym tags
262
+ base_score REAL NOT NULL DEFAULT 5.0, -- 0-10
263
+ source TEXT NOT NULL DEFAULT 'manual',
264
+ added_at TEXT NOT NULL,
265
+ last_seen TEXT NOT NULL -- resets decay clock on each reinforcement
266
+ );
267
+ -- UNIQUE on LOWER(item)
268
+
269
+ CREATE TABLE decay_config (
270
+ id INTEGER PRIMARY KEY CHECK(id = 1),
271
+ half_life_days REAL NOT NULL DEFAULT 30.0,
272
+ floor REAL NOT NULL DEFAULT 0.1
273
+ );
274
+
275
+ CREATE TABLE sessions (
276
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
277
+ analyzed_at TEXT NOT NULL,
278
+ extracted INTEGER NOT NULL DEFAULT 0
279
+ );
280
+ ```
281
+
282
+ ---
283
+
284
+ ## Agent integration
285
+
286
+ Any agent can call aizo as a subprocess — no embedding, no vector index, no runtime:
287
+
288
+ ```python
289
+ import subprocess, json
290
+
291
+ def top_preferences(n: int = 10) -> list[dict]:
292
+ return json.loads(subprocess.check_output(["aizo", "top", str(n), "--json"]))
293
+
294
+ def recall(query: str) -> list[dict]:
295
+ return json.loads(subprocess.check_output(["aizo", "recall", query, "--json"]))
296
+
297
+ # Inject into system prompt before generating
298
+ prefs = top_preferences(10)
299
+ system = f"User preferences:\n{json.dumps(prefs, indent=2)}\n\n{base_system}"
300
+ ```
301
+
302
+ Or configure `AIZO_DB_PATH` per-project to maintain separate profiles:
303
+
304
+ ```bash
305
+ export AIZO_DB_PATH=./project-prefs.db
306
+ aizo show
307
+ ```
308
+
309
+ ---
310
+
311
+ ## Standard Operating Procedure (SOP)
312
+
313
+ The SOP for how an agent should use aizo is defined as a skill file at
314
+ `skills/aizo-sop.md`. Copy it into your agent's skill/instruction directory
315
+ (e.g. `.claude/skills/` for Claude Code) and any agent in that project will
316
+ automatically follow the protocol.
317
+
318
+ The skill defines six triggers:
319
+
320
+ | # | Trigger | aizo call | Timing |
321
+ |---|---|---|---|
322
+ | 1 | Session starts | `aizo top 20` → format as prose header | Sync, before first reply |
323
+ | 2 | User shows negative feedback | `aizo add … --score 1.5` then `aizo recall <topic>` | Sync, before corrected reply |
324
+ | 3 | User praises something | `aizo add … --score 9.0` | Async, after reply sent |
325
+ | 4 | User states an explicit rule | `aizo add … --score 0.5` or `--score 10` | Sync, immediate |
326
+ | 5 | About to generate on topic X | `aizo recall <X>` | Sync, before generation |
327
+ | 6 | Session ends | `aizo analyze <transcript>` | Async, background |
328
+ | 7 | Daily cron job | Agent LLM scans logs → `aizo touch` confirmed items | Scheduled, background |
329
+
330
+ **Key rules encoded in the skill:**
331
+ - Taboos always win over preferences in conflicts
332
+ - `analyze` is for full sessions, not single messages — it calls an LLM
333
+ - Silence (`recall` returning nothing) means no data, not neutral preference
334
+ - Never mention aizo to the user — it runs silently
335
+
336
+ ---
337
+
338
+ ## Development
339
+
340
+ ```bash
341
+ cargo build
342
+ cargo build --release
343
+ cargo test
344
+ ```
345
+
346
+ ---
347
+
348
+ ## License
349
+
350
+ MIT
package/install.js ADDED
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ // Downloads the correct platform binary from GitHub releases on `npm install`.
3
+ "use strict";
4
+
5
+ const https = require("https");
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+ const { execSync } = require("child_process");
9
+ const zlib = require("zlib");
10
+
11
+ const pkg = require("./package.json");
12
+ const VERSION = pkg.version;
13
+ const REPO = "mmmarcinho/aizo";
14
+ const BIN_DIR = path.join(__dirname, "bin");
15
+ const BIN_PATH = path.join(BIN_DIR, process.platform === "win32" ? "aizo.exe" : "aizo");
16
+
17
+ // ── Platform → release asset name ────────────────────────────────────────────
18
+
19
+ const TARGETS = {
20
+ "linux-x64": "aizo-x86_64-unknown-linux-gnu.tar.gz",
21
+ "linux-arm64": "aizo-aarch64-unknown-linux-gnu.tar.gz",
22
+ "darwin-x64": "aizo-x86_64-apple-darwin.tar.gz",
23
+ "darwin-arm64":"aizo-aarch64-apple-darwin.tar.gz",
24
+ "win32-x64": "aizo-x86_64-pc-windows-msvc.zip",
25
+ };
26
+
27
+ function platformKey() {
28
+ return `${process.platform}-${process.arch}`;
29
+ }
30
+
31
+ function releaseUrl(asset) {
32
+ return `https://github.com/${REPO}/releases/download/v${VERSION}/${asset}`;
33
+ }
34
+
35
+ // ── HTTP helpers ──────────────────────────────────────────────────────────────
36
+
37
+ function get(url, cb) {
38
+ https.get(url, (res) => {
39
+ if (res.statusCode === 301 || res.statusCode === 302) {
40
+ return get(res.headers.location, cb);
41
+ }
42
+ if (res.statusCode !== 200) {
43
+ cb(new Error(`HTTP ${res.statusCode} for ${url}`));
44
+ return;
45
+ }
46
+ const chunks = [];
47
+ res.on("data", (c) => chunks.push(c));
48
+ res.on("end", () => cb(null, Buffer.concat(chunks)));
49
+ res.on("error", cb);
50
+ }).on("error", cb);
51
+ }
52
+
53
+ // ── tar.gz extraction (pure Node, no child_process) ──────────────────────────
54
+
55
+ function extractTarGz(buf, destDir) {
56
+ // Decompress gzip
57
+ const tar = zlib.gunzipSync(buf);
58
+
59
+ // Minimal tar parser — finds the first entry whose name ends with /aizo
60
+ let offset = 0;
61
+ while (offset + 512 <= tar.length) {
62
+ const header = tar.slice(offset, offset + 512);
63
+ const name = header.slice(0, 100).toString("utf8").replace(/\0/g, "").trim();
64
+ const sizeOctal = header.slice(124, 136).toString("utf8").replace(/\0/g, "").trim();
65
+ const size = parseInt(sizeOctal, 8) || 0;
66
+ offset += 512;
67
+
68
+ if (name === "" || size === 0) { offset += Math.ceil(size / 512) * 512; continue; }
69
+
70
+ const isAizoBin = /(?:^|[/\\])aizo(?:\.exe)?$/.test(name);
71
+ if (isAizoBin) {
72
+ const data = tar.slice(offset, offset + size);
73
+ fs.mkdirSync(destDir, { recursive: true });
74
+ const dest = path.join(destDir, process.platform === "win32" ? "aizo.exe" : "aizo");
75
+ fs.writeFileSync(dest, data, { mode: 0o755 });
76
+ return dest;
77
+ }
78
+ offset += Math.ceil(size / 512) * 512;
79
+ }
80
+ throw new Error("aizo binary not found in archive");
81
+ }
82
+
83
+ // ── zip extraction (Windows) — delegates to PowerShell ───────────────────────
84
+
85
+ function extractZip(buf, destDir) {
86
+ const tmp = path.join(destDir, "_aizo.zip");
87
+ fs.mkdirSync(destDir, { recursive: true });
88
+ fs.writeFileSync(tmp, buf);
89
+ execSync(
90
+ `powershell -NoProfile -Command "Expand-Archive -Force -Path '${tmp}' -DestinationPath '${destDir}'"`,
91
+ { stdio: "inherit" }
92
+ );
93
+ fs.unlinkSync(tmp);
94
+ // Find the .exe
95
+ const exe = fs.readdirSync(destDir).find((f) => f.endsWith(".exe"));
96
+ if (!exe) throw new Error("aizo.exe not found after zip extraction");
97
+ const dest = path.join(destDir, "aizo.exe");
98
+ fs.renameSync(path.join(destDir, exe), dest);
99
+ return dest;
100
+ }
101
+
102
+ // ── Main ──────────────────────────────────────────────────────────────────────
103
+
104
+ if (fs.existsSync(BIN_PATH)) {
105
+ process.exit(0); // already installed
106
+ }
107
+
108
+ const key = platformKey();
109
+ const asset = TARGETS[key];
110
+
111
+ if (!asset) {
112
+ console.error(
113
+ `[aizo] Unsupported platform: ${key}.\n` +
114
+ `Install via cargo instead: cargo install aizo`
115
+ );
116
+ process.exit(0); // non-fatal — maybe user builds from source
117
+ }
118
+
119
+ const url = releaseUrl(asset);
120
+ console.log(`[aizo] Downloading ${asset} …`);
121
+
122
+ get(url, (err, buf) => {
123
+ if (err) {
124
+ console.error(`[aizo] Download failed: ${err.message}`);
125
+ console.error(`[aizo] You can also install via: cargo install aizo`);
126
+ process.exit(0); // non-fatal
127
+ }
128
+
129
+ try {
130
+ const dest = asset.endsWith(".zip")
131
+ ? extractZip(buf, BIN_DIR)
132
+ : extractTarGz(buf, BIN_DIR);
133
+ console.log(`[aizo] Installed to ${dest}`);
134
+ } catch (e) {
135
+ console.error(`[aizo] Extraction failed: ${e.message}`);
136
+ process.exit(1);
137
+ }
138
+ });
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "aizo-node",
3
+ "version": "0.3.0",
4
+ "description": "爱憎 — preference memory for AI agents: extract, decay, and recall user likes and dislikes",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/mmmarcinho/aizo"
9
+ },
10
+ "homepage": "https://github.com/mmmarcinho/aizo",
11
+ "keywords": ["ai", "agent", "memory", "preferences", "llm", "cli"],
12
+ "bin": {
13
+ "aizo": "./run.js"
14
+ },
15
+ "scripts": {
16
+ "postinstall": "node install.js"
17
+ },
18
+ "engines": {
19
+ "node": ">=16"
20
+ },
21
+ "files": [
22
+ "install.js",
23
+ "run.js",
24
+ "README.md"
25
+ ]
26
+ }
package/run.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { spawnSync } = require("child_process");
5
+ const path = require("path");
6
+ const fs = require("fs");
7
+
8
+ const bin = path.join(__dirname, "bin", process.platform === "win32" ? "aizo.exe" : "aizo");
9
+
10
+ if (!fs.existsSync(bin)) {
11
+ console.error(
12
+ "[aizo] Binary not found. Try reinstalling: npm install aizo-node\n" +
13
+ "Or install via Cargo: cargo install aizo"
14
+ );
15
+ process.exit(1);
16
+ }
17
+
18
+ const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit" });
19
+ process.exit(result.status ?? 1);