@hir4ta/memoria 0.13.2 → 0.14.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "memoria",
3
3
  "description": "A plugin that provides long-term memory for Claude Code. It automatically saves context lost during auto-compact, offering features for session restoration, recording technical decisions, and learning developer patterns.",
4
- "version": "0.13.2",
4
+ "version": "0.14.1",
5
5
  "author": {
6
6
  "name": "hir4ta"
7
7
  },
package/README.ja.md CHANGED
@@ -74,6 +74,16 @@ Claude Code内で以下を実行
74
74
  /plugin install memoria@memoria-marketplace
75
75
  ```
76
76
 
77
+ プロジェクトでmemoriaを初期化:
78
+
79
+ ```bash
80
+ # Claude Code内で
81
+ /memoria:init
82
+
83
+ # またはターミナルから
84
+ npx @hir4ta/memoria --init
85
+ ```
86
+
77
87
  Claude Codeを再起動して完了
78
88
 
79
89
  ## アップデート
@@ -129,6 +139,7 @@ Continue from a previous session? Use `/memoria:resume <id>`
129
139
 
130
140
  | コマンド | 説明 |
131
141
  | --------- | ------ |
142
+ | `/memoria:init` | プロジェクトでmemoriaを初期化 |
132
143
  | `/memoria:save` | 全データ抽出: 要約・判断・パターン・ルール |
133
144
  | `/memoria:plan [トピック]` | 記憶参照 + ソクラティック質問 + タスク分割 |
134
145
  | `/memoria:resume [id]` | セッションを再開(ID省略で一覧表示) |
@@ -227,14 +238,27 @@ flowchart TB
227
238
 
228
239
  ## データ保存
229
240
 
230
- すべてのデータは `.memoria/` ディレクトリに保存
241
+ memoriaは**ハイブリッドストレージ**方式でプライバシーと共有を両立:
242
+
243
+ | ストレージ | 用途 | 共有 |
244
+ |-----------|------|------|
245
+ | **JSON** | 要約、決定、パターン、ルール | Git管理(チーム共有) |
246
+ | **SQLite** | 会話履歴、バックアップ | ローカル専用(.gitignore) |
247
+
248
+ **なぜハイブリッド?**
249
+ - **プライバシー**: 会話履歴(interactions)は各開発者のローカルのみ
250
+ - **軽量化**: JSONファイルが100KB+から約5KBに軽量化(interactions除外)
251
+ - **将来対応**: セマンティック検索用のembeddingsテーブル準備済み
252
+
253
+ ### ディレクトリ構成
231
254
 
232
255
  ```text
233
256
  .memoria/
257
+ ├── local.db # SQLite(ローカル専用、.gitignore)
234
258
  ├── tags.json # タグマスターファイル(93タグ、表記揺れ防止)
235
- ├── sessions/ # セッション履歴(自動 + 手動保存)
259
+ ├── sessions/ # セッションメタデータ
236
260
  │ └── YYYY/MM/
237
- │ └── {id}.json
261
+ │ └── {id}.json # メタデータのみ(interactionsはSQLite)
238
262
  ├── decisions/ # 技術的な判断(/saveから)
239
263
  │ └── YYYY/MM/
240
264
  │ └── {id}.json
@@ -245,11 +269,11 @@ flowchart TB
245
269
  └── reports/ # 週次レポート (YYYY-MM)
246
270
  ```
247
271
 
248
- Gitでバージョン管理可能です。`.gitignore` に追加するかはプロジェクトに応じて判断してください。
272
+ Gitでバージョン管理可能です。`local.db`は自動で`.gitignore`に追加されます。
249
273
 
250
274
  ### セッションJSONスキーマ
251
275
 
252
- 全てのセッションデータは単一のJSONファイルに保存されます:
276
+ セッションメタデータはJSONに保存(interactionsはプライバシー保護のためSQLiteに保存):
253
277
 
254
278
  ```json
255
279
  {
@@ -264,15 +288,6 @@ Gitでバージョン管理可能です。`.gitignore` に追加するかはプ
264
288
  "projectDir": "/path/to/project",
265
289
  "user": { "name": "tanaka", "email": "tanaka@example.com" }
266
290
  },
267
- "interactions": [
268
- {
269
- "id": "int-001",
270
- "timestamp": "2026-01-27T10:15:00Z",
271
- "user": "認証機能を実装して",
272
- "thinking": "思考プロセスの重要なポイント",
273
- "assistant": "RS256署名でJWT認証を実装しました"
274
- }
275
- ],
276
291
  "metrics": {
277
292
  "userMessages": 5,
278
293
  "assistantResponses": 5,
@@ -282,7 +297,6 @@ Gitでバージョン管理可能です。`.gitignore` に追加するかはプ
282
297
  "files": [
283
298
  { "path": "src/auth/jwt.ts", "action": "create" }
284
299
  ],
285
- "preCompactBackups": [],
286
300
  "resumedFrom": "def45678",
287
301
  "status": "complete",
288
302
 
package/README.md CHANGED
@@ -74,6 +74,16 @@ Run the following in Claude Code:
74
74
  /plugin install memoria@memoria-marketplace
75
75
  ```
76
76
 
77
+ Then initialize memoria in your project:
78
+
79
+ ```bash
80
+ # In Claude Code
81
+ /memoria:init
82
+
83
+ # Or from terminal
84
+ npx @hir4ta/memoria --init
85
+ ```
86
+
77
87
  Restart Claude Code to complete installation.
78
88
 
79
89
  ## Update
@@ -129,6 +139,7 @@ Continue from a previous session? Use `/memoria:resume <id>`
129
139
 
130
140
  | Command | Description |
131
141
  |---------|-------------|
142
+ | `/memoria:init` | Initialize memoria in current project |
132
143
  | `/memoria:save` | Extract all data: summary, decisions, patterns, rules |
133
144
  | `/memoria:plan [topic]` | Memory-informed design + Socratic questions + task breakdown |
134
145
  | `/memoria:resume [id]` | Resume session (show list if ID omitted) |
@@ -227,14 +238,27 @@ flowchart TB
227
238
 
228
239
  ## Data Storage
229
240
 
230
- All data is stored in `.memoria/` directory:
241
+ memoria uses a **hybrid storage** approach for privacy and collaboration:
242
+
243
+ | Storage | Purpose | Sharing |
244
+ |---------|---------|---------|
245
+ | **JSON** | Summaries, decisions, patterns, rules | Git-managed (team shared) |
246
+ | **SQLite** | Interactions, backups | Local only (.gitignore) |
247
+
248
+ **Why hybrid?**
249
+ - **Privacy**: Conversation history (interactions) stays local to each developer
250
+ - **Lightweight**: JSON files reduced from 100KB+ to ~5KB (interactions excluded)
251
+ - **Future-ready**: Embeddings table prepared for semantic search
252
+
253
+ ### Directory Structure
231
254
 
232
255
  ```text
233
256
  .memoria/
257
+ ├── local.db # SQLite (local only, .gitignore)
234
258
  ├── tags.json # Tag master file (93 tags, prevents notation variations)
235
- ├── sessions/ # Session history (YYYY/MM)
259
+ ├── sessions/ # Session metadata (YYYY/MM)
236
260
  │ └── YYYY/MM/
237
- │ └── {id}.json # All session data (auto + manual save)
261
+ │ └── {id}.json # Metadata only (interactions in SQLite)
238
262
  ├── decisions/ # Technical decisions (from /save)
239
263
  │ └── YYYY/MM/
240
264
  │ └── {id}.json
@@ -245,11 +269,11 @@ All data is stored in `.memoria/` directory:
245
269
  └── reports/ # Weekly reports (YYYY-MM)
246
270
  ```
247
271
 
248
- Git-manageable. Add to `.gitignore` based on your project needs.
272
+ Git-manageable. The `local.db` file is automatically added to `.gitignore`.
249
273
 
250
274
  ### Session JSON Schema
251
275
 
252
- All session data is stored in a single JSON file:
276
+ Session metadata is stored in JSON (interactions are stored in SQLite for privacy):
253
277
 
254
278
  ```json
255
279
  {
@@ -264,15 +288,6 @@ All session data is stored in a single JSON file:
264
288
  "projectDir": "/path/to/project",
265
289
  "user": { "name": "tanaka", "email": "tanaka@example.com" }
266
290
  },
267
- "interactions": [
268
- {
269
- "id": "int-001",
270
- "timestamp": "2026-01-27T10:15:00Z",
271
- "user": "Implement authentication",
272
- "thinking": "Key insights from thinking process",
273
- "assistant": "Implemented JWT auth with RS256 signing"
274
- }
275
- ],
276
291
  "metrics": {
277
292
  "userMessages": 5,
278
293
  "assistantResponses": 5,
@@ -282,7 +297,6 @@ All session data is stored in a single JSON file:
282
297
  "files": [
283
298
  { "path": "src/auth/jwt.ts", "action": "create" }
284
299
  ],
285
- "preCompactBackups": [],
286
300
  "resumedFrom": "def45678",
287
301
  "status": "complete",
288
302
 
package/bin/memoria.js CHANGED
@@ -5,6 +5,18 @@ import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
+ // Suppress Node.js SQLite experimental warning (must be before dynamic import)
9
+ const originalEmit = process.emit;
10
+ process.emit = function (name, data, ...args) {
11
+ if (name === "warning" && data?.name === "ExperimentalWarning" && data?.message?.includes("SQLite")) {
12
+ return false;
13
+ }
14
+ return originalEmit.call(process, name, data, ...args);
15
+ };
16
+
17
+ // Dynamic import to ensure warning suppression is active
18
+ const { DatabaseSync } = await import("node:sqlite");
19
+
8
20
  const __filename = fileURLToPath(import.meta.url);
9
21
  const __dirname = path.dirname(__filename);
10
22
 
@@ -17,6 +29,7 @@ function showHelp() {
17
29
  memoria - Claude Code Long-term Memory Plugin
18
30
 
19
31
  Usage:
32
+ memoria --init Initialize .memoria directory in current project
20
33
  memoria --dashboard Start the web dashboard
21
34
  memoria -d Same as above (short form)
22
35
  memoria --port <port> Specify port (default: 7777)
@@ -24,6 +37,7 @@ Usage:
24
37
 
25
38
  Examples:
26
39
  cd /path/to/your/project
40
+ npx @hir4ta/memoria --init
27
41
  npx @hir4ta/memoria --dashboard
28
42
  npx @hir4ta/memoria -d --port 8080
29
43
  `);
@@ -33,11 +47,81 @@ function checkMemoriaDir() {
33
47
  const memoriaDir = path.join(projectRoot, ".memoria");
34
48
  if (!fs.existsSync(memoriaDir)) {
35
49
  console.log(`\nWARNING: .memoria directory not found: ${projectRoot}`);
36
- console.log(
37
- " Run a Claude Code session with the memoria plugin installed",
38
- );
39
- console.log(" in this project to create the data directory.");
50
+ console.log(" Run: npx @hir4ta/memoria --init");
51
+ }
52
+ }
53
+
54
+ function initMemoria() {
55
+ const memoriaDir = path.join(projectRoot, ".memoria");
56
+ const sessionsDir = path.join(memoriaDir, "sessions");
57
+ const rulesDir = path.join(memoriaDir, "rules");
58
+ const patternsDir = path.join(memoriaDir, "patterns");
59
+ const tagsPath = path.join(memoriaDir, "tags.json");
60
+ const dbPath = path.join(memoriaDir, "local.db");
61
+
62
+ // Check if already initialized
63
+ if (fs.existsSync(memoriaDir)) {
64
+ console.log(`memoria is already initialized: ${memoriaDir}`);
65
+ return;
66
+ }
67
+
68
+ // Create directories
69
+ fs.mkdirSync(sessionsDir, { recursive: true });
70
+ fs.mkdirSync(rulesDir, { recursive: true });
71
+ fs.mkdirSync(patternsDir, { recursive: true });
72
+
73
+ // Copy default tags.json
74
+ const defaultTagsPath = path.join(packageDir, "hooks", "default-tags.json");
75
+ if (fs.existsSync(defaultTagsPath)) {
76
+ fs.copyFileSync(defaultTagsPath, tagsPath);
77
+ }
78
+
79
+ // Initialize rules files
80
+ const now = new Date().toISOString();
81
+ const rulesTemplate = JSON.stringify(
82
+ {
83
+ schemaVersion: 1,
84
+ createdAt: now,
85
+ updatedAt: now,
86
+ items: [],
87
+ },
88
+ null,
89
+ 2,
90
+ );
91
+
92
+ fs.writeFileSync(
93
+ path.join(rulesDir, "review-guidelines.json"),
94
+ rulesTemplate,
95
+ );
96
+ fs.writeFileSync(path.join(rulesDir, "dev-rules.json"), rulesTemplate);
97
+
98
+ // Initialize SQLite database
99
+ const schemaPath = path.join(packageDir, "lib", "schema.sql");
100
+ try {
101
+ const db = new DatabaseSync(dbPath);
102
+ db.exec("PRAGMA journal_mode = WAL");
103
+ if (fs.existsSync(schemaPath)) {
104
+ const schema = fs.readFileSync(schemaPath, "utf-8");
105
+ db.exec(schema);
106
+ }
107
+ db.close();
108
+ } catch (error) {
109
+ console.error(`Warning: Failed to initialize SQLite database: ${error.message}`);
40
110
  }
111
+
112
+ console.log(`memoria initialized: ${memoriaDir}`);
113
+ console.log(`
114
+ Created:
115
+ ${sessionsDir}/
116
+ ${rulesDir}/
117
+ ${patternsDir}/
118
+ ${tagsPath}
119
+ ${rulesDir}/review-guidelines.json
120
+ ${rulesDir}/dev-rules.json
121
+ ${dbPath}
122
+
123
+ You can now use memoria with Claude Code in this project.
124
+ `);
41
125
  }
42
126
 
43
127
  function getPort() {
@@ -90,7 +174,9 @@ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
90
174
  process.exit(0);
91
175
  }
92
176
 
93
- if (args.includes("--dashboard") || args.includes("-d")) {
177
+ if (args.includes("--init")) {
178
+ initMemoria();
179
+ } else if (args.includes("--dashboard") || args.includes("-d")) {
94
180
  startDashboard();
95
181
  } else {
96
182
  console.error(`ERROR: Unknown option: ${args.join(" ")}`);
package/dist/lib/db.js ADDED
@@ -0,0 +1,167 @@
1
+ // lib/db.ts
2
+ import { execSync } from "node:child_process";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ var originalEmit = process.emit;
7
+ process.emit = function(name, data, ...args) {
8
+ if (name === "warning" && typeof data === "object" && data?.name === "ExperimentalWarning" && data?.message?.includes("SQLite")) {
9
+ return false;
10
+ }
11
+ return originalEmit.call(process, name, data, ...args);
12
+ };
13
+ var { DatabaseSync } = await import("node:sqlite");
14
+ var __filename = fileURLToPath(import.meta.url);
15
+ var __dirname = dirname(__filename);
16
+ function getCurrentUser() {
17
+ try {
18
+ return execSync("git config user.name", { encoding: "utf-8" }).trim();
19
+ } catch {
20
+ try {
21
+ return execSync("whoami", { encoding: "utf-8" }).trim();
22
+ } catch {
23
+ return "unknown";
24
+ }
25
+ }
26
+ }
27
+ function getDbPath(memoriaDir) {
28
+ return join(memoriaDir, "local.db");
29
+ }
30
+ function initDatabase(memoriaDir) {
31
+ const dbPath = getDbPath(memoriaDir);
32
+ const db = new DatabaseSync(dbPath);
33
+ db.exec("PRAGMA journal_mode = WAL");
34
+ const schemaPath = join(__dirname, "schema.sql");
35
+ if (existsSync(schemaPath)) {
36
+ const schema = readFileSync(schemaPath, "utf-8");
37
+ db.exec(schema);
38
+ }
39
+ return db;
40
+ }
41
+ function openDatabase(memoriaDir) {
42
+ const dbPath = getDbPath(memoriaDir);
43
+ if (!existsSync(dbPath)) {
44
+ return null;
45
+ }
46
+ const db = new DatabaseSync(dbPath);
47
+ db.exec("PRAGMA journal_mode = WAL");
48
+ return db;
49
+ }
50
+ function insertInteractions(db, interactions) {
51
+ const insert = db.prepare(`
52
+ INSERT INTO interactions (session_id, owner, role, content, thinking, tool_calls, timestamp, is_compact_summary)
53
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
54
+ `);
55
+ db.exec("BEGIN TRANSACTION");
56
+ try {
57
+ for (const item of interactions) {
58
+ insert.run(
59
+ item.session_id,
60
+ item.owner,
61
+ item.role,
62
+ item.content,
63
+ item.thinking || null,
64
+ item.tool_calls || null,
65
+ item.timestamp,
66
+ item.is_compact_summary || 0
67
+ );
68
+ }
69
+ db.exec("COMMIT");
70
+ } catch (error) {
71
+ db.exec("ROLLBACK");
72
+ throw error;
73
+ }
74
+ }
75
+ function getInteractions(db, sessionId) {
76
+ const stmt = db.prepare(`
77
+ SELECT * FROM interactions
78
+ WHERE session_id = ?
79
+ ORDER BY timestamp ASC
80
+ `);
81
+ return stmt.all(sessionId);
82
+ }
83
+ function getInteractionsByOwner(db, sessionId, owner) {
84
+ const stmt = db.prepare(`
85
+ SELECT * FROM interactions
86
+ WHERE session_id = ? AND owner = ?
87
+ ORDER BY timestamp ASC
88
+ `);
89
+ return stmt.all(sessionId, owner);
90
+ }
91
+ function hasInteractions(db, sessionId, owner) {
92
+ const stmt = db.prepare(`
93
+ SELECT COUNT(*) as count FROM interactions
94
+ WHERE session_id = ? AND owner = ?
95
+ `);
96
+ const result = stmt.get(sessionId, owner);
97
+ return result.count > 0;
98
+ }
99
+ function insertPreCompactBackup(db, backup) {
100
+ const stmt = db.prepare(`
101
+ INSERT INTO pre_compact_backups (session_id, owner, interactions)
102
+ VALUES (?, ?, ?)
103
+ `);
104
+ stmt.run(backup.session_id, backup.owner, backup.interactions);
105
+ }
106
+ function getLatestBackup(db, sessionId) {
107
+ const stmt = db.prepare(`
108
+ SELECT * FROM pre_compact_backups
109
+ WHERE session_id = ?
110
+ ORDER BY created_at DESC
111
+ LIMIT 1
112
+ `);
113
+ return stmt.get(sessionId) || null;
114
+ }
115
+ function getAllBackups(db, sessionId) {
116
+ const stmt = db.prepare(`
117
+ SELECT * FROM pre_compact_backups
118
+ WHERE session_id = ?
119
+ ORDER BY created_at ASC
120
+ `);
121
+ return stmt.all(sessionId);
122
+ }
123
+ function searchInteractions(db, query, limit = 10) {
124
+ const stmt = db.prepare(`
125
+ SELECT i.session_id, i.content, i.thinking
126
+ FROM interactions_fts fts
127
+ JOIN interactions i ON fts.rowid = i.id
128
+ WHERE interactions_fts MATCH ?
129
+ LIMIT ?
130
+ `);
131
+ return stmt.all(query, limit);
132
+ }
133
+ function deleteInteractions(db, sessionId) {
134
+ const stmt = db.prepare("DELETE FROM interactions WHERE session_id = ?");
135
+ stmt.run(sessionId);
136
+ }
137
+ function deleteBackups(db, sessionId) {
138
+ const stmt = db.prepare(
139
+ "DELETE FROM pre_compact_backups WHERE session_id = ?"
140
+ );
141
+ stmt.run(sessionId);
142
+ }
143
+ function getDbStats(db) {
144
+ const interactionsCount = db.prepare("SELECT COUNT(*) as count FROM interactions").get();
145
+ const backupsCount = db.prepare("SELECT COUNT(*) as count FROM pre_compact_backups").get();
146
+ return {
147
+ interactions: interactionsCount.count,
148
+ backups: backupsCount.count
149
+ };
150
+ }
151
+ export {
152
+ deleteBackups,
153
+ deleteInteractions,
154
+ getAllBackups,
155
+ getCurrentUser,
156
+ getDbPath,
157
+ getDbStats,
158
+ getInteractions,
159
+ getInteractionsByOwner,
160
+ getLatestBackup,
161
+ hasInteractions,
162
+ initDatabase,
163
+ insertInteractions,
164
+ insertPreCompactBackup,
165
+ openDatabase,
166
+ searchInteractions
167
+ };