Skip to content

Commit 9469e25

Browse files
committed
Add Oxc tooling, tests, CI, and documentation
Configure oxlint for correctness/suspicious/pedantic rules and oxfmt for consistent formatting. Add a GitHub Actions workflow that runs the full check suite on push and PR. Add node:test coverage for the Claude adapter (subagent ID stability) and the query layer (filter application, read-only SQL gating). Add the project README with install, setup, and usage docs.
1 parent 972255a commit 9469e25

6 files changed

Lines changed: 387 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
jobs:
10+
package:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v4
15+
16+
- name: Setup Node
17+
uses: actions/setup-node@v4
18+
with:
19+
node-version: 20
20+
cache: npm
21+
22+
- name: Install dependencies
23+
run: npm ci
24+
25+
- name: Validate package
26+
run: npm run check
27+
28+
- name: Dry-run pack
29+
run: npm run pack:dry-run

.oxfmtrc.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"$schema": "./node_modules/oxfmt/configuration_schema.json",
3+
"printWidth": 100,
4+
"semi": true,
5+
"singleQuote": false,
6+
"trailingComma": "all",
7+
"sortImports": {
8+
"newlinesBetween": true,
9+
"sortSideEffects": false
10+
},
11+
"sortPackageJson": {
12+
"sortScripts": true
13+
},
14+
"ignorePatterns": ["node_modules/**"]
15+
}

.oxlintrc.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"$schema": "./node_modules/oxlint/configuration_schema.json",
3+
"categories": {
4+
"correctness": "error",
5+
"suspicious": "warn",
6+
"pedantic": "warn"
7+
},
8+
"plugins": ["eslint", "typescript", "unicorn", "oxc", "import"],
9+
"env": {
10+
"node": true,
11+
"es2024": true
12+
},
13+
"ignorePatterns": ["node_modules/**"],
14+
"rules": {
15+
"typescript/no-explicit-any": "error",
16+
"eslint/no-console": "off"
17+
}
18+
}

README.md

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# pi-amnesia
2+
3+
> Your agents forget everything. Amnesia remembers.
4+
5+
A [pi](https://github.com/badlogic/pi) extension that syncs, indexes, and analyzes session data from **Claude Code**, **Pi**, and **Codex** — with semantic search powered by Turso native vectors.
6+
7+
## Features
8+
9+
- **`/sync`** — Incrementally ingests new session files into Turso.
10+
- **`/analyze [focus]`** — Asks the active model to analyze patterns in your session history.
11+
- **`/summarize [filter]`** — Summarizes recent sessions grouped by project.
12+
- **`query_agent_data`** — A tool for structured queries, text search, semantic search, and read-only SQL.
13+
14+
## Install
15+
16+
```bash
17+
pi install git:github.com/manusajith/pi-amnesia
18+
```
19+
20+
## Setup
21+
22+
### 1. Create a Turso database
23+
24+
```bash
25+
turso db create amnesia
26+
turso db show amnesia --url
27+
turso db tokens create amnesia
28+
```
29+
30+
### 2. Set environment variables
31+
32+
```bash
33+
export TURSO_URL="libsql://amnesia-<your-org>.turso.io"
34+
export TURSO_AUTH_TOKEN="<your-token>"
35+
```
36+
37+
### 3. Restart pi and sync
38+
39+
```text
40+
/sync
41+
```
42+
43+
## Embeddings
44+
45+
Amnesia uses OpenAI embeddings for semantic search.
46+
47+
By default it uses `text-embedding-3-small`. You can override that with:
48+
49+
```bash
50+
export AMNESIA_EMBEDDING_MODEL="text-embedding-3-large"
51+
```
52+
53+
The API key is resolved from Pi's model registry automatically, so you do not need a separate `OPENAI_API_KEY` when Pi already has an OpenAI provider configured.
54+
55+
If no OpenAI key is available, Amnesia still works — semantic search is disabled and text search remains available.
56+
57+
## Usage
58+
59+
### Sync sessions
60+
61+
```text
62+
/sync
63+
```
64+
65+
Scans:
66+
67+
- `~/.claude/projects/`
68+
- `~/.pi/agent/sessions/`
69+
- `~/.codex/archived_sessions/`
70+
71+
### Analyze patterns
72+
73+
```text
74+
/analyze
75+
/analyze errors
76+
/analyze tools
77+
/analyze project my-app
78+
/analyze last-week
79+
/analyze agent claude last-month
80+
```
81+
82+
### Summarize sessions
83+
84+
```text
85+
/summarize
86+
/summarize today
87+
/summarize project my-app
88+
/summarize last-week
89+
```
90+
91+
### Natural-language exploration
92+
93+
Ask things like:
94+
95+
- "What tools do I use most across projects?"
96+
- "Find sessions where I dealt with authentication issues"
97+
- "Show me errors that keep recurring"
98+
- "What did I work on this week?"
99+
100+
### Semantic search
101+
102+
When embeddings are available:
103+
104+
- "Find conversations about database migrations"
105+
- "When did I last work on deployment pipelines?"
106+
107+
## Data sources
108+
109+
| Agent | Location | Format |
110+
| ----------- | ------------------------------------ | --------------------------------------- |
111+
| Claude Code | `~/.claude/projects/**/*.jsonl` | Messages with tool calls and sidechains |
112+
| Pi | `~/.pi/agent/sessions/**/*.jsonl` | Typed event streams |
113+
| Codex | `~/.codex/archived_sessions/*.jsonl` | Session metadata and response items |
114+
115+
## Schema
116+
117+
- **`projects`** — unique project paths and extracted repo names
118+
- **`sessions`** — per-conversation metadata
119+
- **`messages`** — user/assistant/system messages with optional embeddings
120+
- **`tool_calls`** — tool invocations and results
121+
- **`sync_state`** — per-source sync watermark
122+
- **`insights`** — derived analysis data
123+
124+
## Development
125+
126+
```bash
127+
npm install
128+
npm run check
129+
npm run format
130+
```
131+
132+
Tooling:
133+
134+
- **oxlint** for linting
135+
- **oxfmt** for formatting
136+
- **tsc** for type-checking
137+
- **node:test** for tests
138+
139+
## Adding adapters
140+
141+
To support another agent, implement `SessionAdapter` in `extensions/amnesia/lib/adapters/` and register it in `extensions/amnesia/lib/sync.ts`.
142+
143+
```ts
144+
import type { SessionAdapter } from "./types.ts";
145+
146+
export const myAdapter: SessionAdapter = {
147+
name: "my-agent",
148+
async discoverFiles(sinceMs?) {
149+
return [];
150+
},
151+
async parse(filePath) {
152+
return null;
153+
},
154+
};
155+
```
156+
157+
## License
158+
159+
MIT

test/claude-adapter.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import assert from "node:assert/strict";
2+
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import test from "node:test";
6+
7+
import { claudeAdapter } from "../extensions/amnesia/lib/adapters/claude.ts";
8+
9+
function buildClaudeLine(content: Record<string, unknown>): string {
10+
return `${JSON.stringify(content)}\n`;
11+
}
12+
13+
function createUserLine(): string {
14+
return buildClaudeLine({
15+
sessionId: "session-1",
16+
cwd: "/workspace/sample-app",
17+
version: "2.1.45",
18+
gitBranch: "main",
19+
type: "user",
20+
uuid: "msg-1",
21+
parentUuid: null,
22+
timestamp: "2026-03-20T10:00:00.000Z",
23+
message: {
24+
role: "user",
25+
content: "Review the auth changes",
26+
timestamp: 1770000000000,
27+
},
28+
});
29+
}
30+
31+
function createAssistantLine(): string {
32+
return buildClaudeLine({
33+
sessionId: "session-1",
34+
cwd: "/workspace/sample-app",
35+
version: "2.1.45",
36+
gitBranch: "main",
37+
type: "assistant",
38+
uuid: "msg-2",
39+
parentUuid: "msg-1",
40+
timestamp: "2026-03-20T10:01:00.000Z",
41+
message: {
42+
role: "assistant",
43+
content: [{ type: "text", text: "I found one issue." }],
44+
},
45+
});
46+
}
47+
48+
async function createClaudeSubagentFile(): Promise<string> {
49+
const root = await mkdtemp(join(tmpdir(), "amnesia-claude-"));
50+
const subagentDir = join(root, "subagents");
51+
const filePath = join(subagentDir, "agent-a123.jsonl");
52+
await mkdir(subagentDir, { recursive: true });
53+
await writeFile(filePath, `${createUserLine()}${createAssistantLine()}`);
54+
return filePath;
55+
}
56+
57+
test("claude adapter gives subagents stable unique session ids and uses cwd as project path", async () => {
58+
const filePath = await createClaudeSubagentFile();
59+
const parsed = await claudeAdapter.parse(filePath);
60+
61+
assert.ok(parsed);
62+
assert.equal(parsed.session.id, "session-1:subagent:agent-a123");
63+
assert.equal(parsed.session.project, "/workspace/sample-app");
64+
assert.equal(parsed.session.repoName, "sample-app");
65+
assert.equal(parsed.messages.length, 2);
66+
});

test/queries.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
4+
import { createClient } from "@libsql/client";
5+
6+
import { applyMigrations } from "../extensions/amnesia/lib/db.ts";
7+
import { rawQuery, searchMessages } from "../extensions/amnesia/lib/queries.ts";
8+
9+
async function createTestDb() {
10+
const db = createClient({ url: "file::memory:" });
11+
await applyMigrations(db);
12+
return db;
13+
}
14+
15+
async function seedSearchFixtures(db: ReturnType<typeof createClient>): Promise<void> {
16+
await db.execute(
17+
`INSERT INTO projects (id, path, repo_name) VALUES (1, '/workspace/app-one', 'app-one'), (2, '/workspace/app-two', 'app-two')`,
18+
);
19+
await db.execute({
20+
sql: `INSERT INTO sessions (id, project_id, agent, started_at) VALUES (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)`,
21+
args: [
22+
"claude-new",
23+
1,
24+
"claude",
25+
"2026-03-20T10:00:00.000Z",
26+
"pi-new",
27+
1,
28+
"pi",
29+
"2026-03-20T10:00:00.000Z",
30+
"claude-old",
31+
2,
32+
"claude",
33+
"2026-02-01T10:00:00.000Z",
34+
],
35+
});
36+
await db.execute({
37+
sql: `INSERT INTO messages (id, session_id, role, content, timestamp, seq) VALUES (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?)`,
38+
args: [
39+
"m1",
40+
"claude-new",
41+
"user",
42+
"auth bug in login flow",
43+
"2026-03-20T10:00:00.000Z",
44+
0,
45+
"m2",
46+
"pi-new",
47+
"user",
48+
"auth bug in login flow",
49+
"2026-03-20T10:00:00.000Z",
50+
0,
51+
"m3",
52+
"claude-old",
53+
"user",
54+
"auth bug in login flow",
55+
"2026-02-01T10:00:00.000Z",
56+
0,
57+
],
58+
});
59+
}
60+
61+
async function searchScopedMessages() {
62+
const db = await createTestDb();
63+
await seedSearchFixtures(db);
64+
65+
const rows = await searchMessages(db, {
66+
search: "auth bug",
67+
agent: "claude",
68+
project: "app-one",
69+
since: "2026-03-01T00:00:00.000Z",
70+
});
71+
72+
db.close();
73+
return rows;
74+
}
75+
76+
test("searchMessages applies agent, project, and since filters", async () => {
77+
const rows = await searchScopedMessages();
78+
79+
assert.equal(Array.isArray(rows), true);
80+
if (!Array.isArray(rows)) {
81+
throw new TypeError("Expected searchMessages to return rows");
82+
}
83+
84+
assert.equal(rows.length, 1);
85+
assert.equal(rows[0]?.session_id, "claude-new");
86+
});
87+
88+
test("rawQuery only allows read-only SQL", async () => {
89+
const db = await createTestDb();
90+
91+
const blocked = await rawQuery(db, "DELETE FROM sessions");
92+
assert.deepEqual(blocked, {
93+
error: "Only SELECT, WITH, and EXPLAIN queries are allowed.",
94+
});
95+
96+
const allowed = await rawQuery(db, "SELECT 1 as value");
97+
assert.equal("rows" in allowed, true);
98+
99+
db.close();
100+
});

0 commit comments

Comments
 (0)