cc-star 0.1.0__tar.gz

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.
@@ -0,0 +1,32 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - name: Set up Python
16
+ uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.12"
19
+
20
+ - name: Install build tools
21
+ run: |
22
+ python -m pip install --upgrade pip
23
+ pip install build
24
+
25
+ - name: Build wheel and sdist
26
+ run: python -m build
27
+
28
+ - name: Publish to PyPI
29
+ uses: pypa/gh-action-pypi-publish@release/v1
30
+ with:
31
+ password: ${{ secrets.PYPI_TOKEN }}
32
+ skip-existing: true
@@ -0,0 +1,23 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.so
5
+ *.egg-info/
6
+
7
+ # Build
8
+ dist/
9
+ build/
10
+ *.egg
11
+
12
+ # Test
13
+ .pytest_cache/
14
+ .coverage
15
+ htmlcov/
16
+
17
+ # IDE
18
+ .vscode/
19
+ .idea/
20
+
21
+ # OS
22
+ .DS_Store
23
+ Thumbs.db
cc_star-0.1.0/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ GNU AFFERO GENERAL PUBLIC LICENSE
2
+ Version 3, 19 November 2007
3
+
4
+ Copyright (C) 2026 jigeagent
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU Affero General Public License as published
8
+ by the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU Affero General Public License for more details.
15
+
16
+ You should have received a copy of the GNU Affero General Public License
17
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
cc_star-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: cc-star
3
+ Version: 0.1.0
4
+ Summary: Upgrade Claude Code native memory to digital-life memory — SQLite hot storage + FTS5 retrieval + optional OpenViking cold sync
5
+ Project-URL: Homepage, https://github.com/jigeagent/cc-star
6
+ License: AGPL-3.0-or-later
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: httpx>=0.28
10
+ Requires-Dist: pyyaml>=6.0
@@ -0,0 +1,101 @@
1
+ # cc-star
2
+
3
+ **Claude Code memory upgrade kit.**
4
+
5
+ Upgrade Claude Code's native `MEMORY.md` (a plain text file that gets constantly truncated) into a **digital-life memory system** — local SQLite hot storage + FTS5 retrieval + optional OpenViking cold sync.
6
+
7
+ ```
8
+ pip install cc-star
9
+ cc-star init
10
+ # 30 seconds → permanent, searchable, offline-capable memory
11
+ ```
12
+
13
+ ## Features
14
+
15
+ - **Persistent storage** — every conversation turn saved to local SQLite database
16
+ - **Full-text search** — FTS5-powered memory retrieval across all past conversations
17
+ - **Context injection** — automatically injects relevant past memories before each prompt
18
+ - **Compression protection** — preserves critical context (MEMORY.md, STATUS.md) across Claude Code compaction events
19
+ - **Optional OpenViking sync** — cold storage with semantic search (install with `cc-star[ov]`)
20
+ - **Zero Claude Code config** — `cc-star init` handles all hook registration
21
+
22
+ ## Quick Start
23
+
24
+ ```bash
25
+ # Install
26
+ pip install cc-star
27
+
28
+ # Initialize (30 seconds)
29
+ cc-star init
30
+
31
+ # Start a new Claude Code session — memories will be automatically
32
+ # stored, searched, and injected
33
+
34
+ # Search your memory
35
+ cc-star search "how did we fix the auth bug?"
36
+
37
+ # Check status
38
+ cc-star status
39
+ ```
40
+
41
+ ## Commands
42
+
43
+ | Command | Description |
44
+ |---------|-------------|
45
+ | `cc-star init` | Initialize the memory system |
46
+ | `cc-star status` | Show memory system status |
47
+ | `cc-star search <query>` | Search local memory |
48
+ | `cc-star config` | View all configuration |
49
+ | `cc-star config <key> <value>` | Update configuration |
50
+ | `cc-star uninstall` | Remove hooks from Claude Code settings |
51
+
52
+ ## Configuration
53
+
54
+ Config file: `~/.cc-star/config.yaml`
55
+
56
+ ```yaml
57
+ agent:
58
+ name: assistant
59
+ tags: ["claude-code"]
60
+ storage:
61
+ path: ~/.cc-star/data
62
+ memory:
63
+ max_inject: 5
64
+ ov:
65
+ enabled: false
66
+ url: ""
67
+ sync_batch: 50
68
+ hooks:
69
+ timeout_inject: 10
70
+ timeout_store: 15
71
+ timeout_summary: 30
72
+ timeout_session_start: 10
73
+ timeout_compact_save: 5
74
+ timeout_compact_restore: 10
75
+ ```
76
+
77
+ ## Architecture
78
+
79
+ ```
80
+ Claude Code → 5 Hook Scripts → cache.db (SQLite+FTS5) → [optional] OpenViking
81
+
82
+ cc-star init
83
+
84
+ string.Template → ~/.cc-star/hooks/*.py
85
+ ```
86
+
87
+ - **SessionStart** — checks OV health, shows last session summary
88
+ - **UserPromptSubmit (inject)** — FTS5 + optional OV semantic search, RRF merge, injects as `additionalContext`
89
+ - **Stop (store)** — reads transcript, extracts last turn, writes to cache.db
90
+ - **SessionEnd (summary)** — extracts session summary, batch syncs to OV
91
+ - **PreCompact/PostCompact (compact)** — preserves MEMORY.md / STATUS.md / OV snapshot across compression
92
+
93
+ ## Dependencies
94
+
95
+ - **hermes-next** (>=0.2) — SQLite cache + FTS5 retrieval engine
96
+ - **pyyaml** (>=6.0) — YAML config parsing
97
+ - **openviking** (optional, >=0.3.22) — OpenViking cold storage client
98
+
99
+ ## License
100
+
101
+ AGPL-3.0 — see [LICENSE](LICENSE)
@@ -0,0 +1,3 @@
1
+ """cc-star — Claude Code memory upgrade kit."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ """SQLite local cache layer."""
@@ -0,0 +1,100 @@
1
+ """SQLite connection management with performance optimizations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ import threading
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+
11
+ # Shared statement cache across threads
12
+ _STMT_CACHE: dict[str, sqlite3.Cursor] = {}
13
+
14
+
15
+ class CacheConnection:
16
+ """Thread-safe SQLite connection manager with performance tuning.
17
+
18
+ Optimizations:
19
+ - WAL mode for concurrent reads
20
+ - 64MB cache for hot data
21
+ - memory-mapped I/O (256MB)
22
+ - Lazy pragma initialization (deferred until first query)
23
+ """
24
+
25
+ def __init__(self, db_path: str, wal_mode: bool = True):
26
+ self._db_path = str(Path(db_path).expanduser())
27
+ self._wal = wal_mode
28
+ self._local = threading.local()
29
+ self._lock = threading.Lock()
30
+ self._initialized = False
31
+
32
+ # Ensure parent directory exists
33
+ Path(self._db_path).parent.mkdir(parents=True, exist_ok=True)
34
+
35
+ def _ensure_init(self) -> None:
36
+ """Apply performance pragmas once per connection."""
37
+ if self._initialized:
38
+ return
39
+ conn = self._get_conn_raw()
40
+ if self._wal:
41
+ conn.execute("PRAGMA journal_mode=WAL")
42
+ conn.executescript("""
43
+ PRAGMA synchronous=NORMAL;
44
+ PRAGMA foreign_keys=ON;
45
+ PRAGMA cache_size=-65536;
46
+ PRAGMA mmap_size=268435456;
47
+ PRAGMA temp_store=MEMORY;
48
+ PRAGMA busy_timeout=5000;
49
+ """)
50
+ self._initialized = True
51
+
52
+ def _get_conn_raw(self) -> sqlite3.Connection:
53
+ """Create a raw connection without pragma setup."""
54
+ if not hasattr(self._local, "conn") or self._local.conn is None:
55
+ conn = sqlite3.connect(
56
+ self._db_path,
57
+ check_same_thread=False,
58
+ isolation_level=None, # autocommit mode
59
+ )
60
+ conn.row_factory = sqlite3.Row
61
+ self._local.conn = conn
62
+ return self._local.conn
63
+
64
+ @property
65
+ def conn(self) -> sqlite3.Connection:
66
+ self._ensure_init()
67
+ return self._get_conn_raw()
68
+
69
+ def execute(self, sql: str, params: tuple = ()) -> sqlite3.Cursor:
70
+ """Execute with automatic pragma init."""
71
+ self._ensure_init()
72
+ return self._get_conn_raw().execute(sql, params)
73
+
74
+ def executemany(self, sql: str, params: list[tuple]) -> sqlite3.Cursor:
75
+ """Batch execute with automatic pragma init."""
76
+ self._ensure_init()
77
+ return self._get_conn_raw().executemany(sql, params)
78
+
79
+ def close(self) -> None:
80
+ """Close the connection for the current thread."""
81
+ if hasattr(self._local, "conn") and self._local.conn is not None:
82
+ try:
83
+ self._local.conn.execute("PRAGMA optimize")
84
+ except Exception:
85
+ pass
86
+ self._local.conn.close()
87
+ self._local.conn = None
88
+ self._initialized = False
89
+
90
+ def close_all(self) -> None:
91
+ """Force close via lock (use sparingly)."""
92
+ with self._lock:
93
+ if hasattr(self._local, "conn") and self._local.conn is not None:
94
+ try:
95
+ self._local.conn.execute("PRAGMA optimize")
96
+ except Exception:
97
+ pass
98
+ self._local.conn.close()
99
+ self._local.conn = None
100
+ self._initialized = False
@@ -0,0 +1,94 @@
1
+ """Policy repository — local SQLite CRUD for policies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, Optional
7
+
8
+ from cc_star.cache.connection import CacheConnection
9
+ from cc_star.cache.schema import ensure_schema
10
+ from cc_star.memos.types import PolicyRow
11
+
12
+
13
+ class PolicyRepository:
14
+ """Persist and query policies locally."""
15
+
16
+ def __init__(self, cache: CacheConnection):
17
+ self._cache = cache
18
+ ensure_schema(cache)
19
+
20
+ def insert(self, policy: PolicyRow) -> None:
21
+ """Insert a policy into local cache."""
22
+ conn = self._cache.conn
23
+ conn.execute(
24
+ """
25
+ INSERT OR REPLACE INTO policies
26
+ (id, name, description, trigger_pattern, action_template,
27
+ embedding, confidence, activation_count, source_trace_ids,
28
+ metadata, created_at, synced)
29
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
30
+ """,
31
+ (
32
+ policy.id,
33
+ policy.name,
34
+ policy.description,
35
+ policy.trigger_pattern,
36
+ policy.action_template,
37
+ json.dumps(policy.embedding) if policy.embedding else None,
38
+ policy.confidence,
39
+ policy.activation_count,
40
+ json.dumps(policy.source_trace_ids, ensure_ascii=False),
41
+ json.dumps(policy.metadata, ensure_ascii=False, default=str),
42
+ policy.created_at,
43
+ 0,
44
+ ),
45
+ )
46
+ conn.commit()
47
+
48
+ def get(self, policy_id: str) -> Optional[PolicyRow]:
49
+ """Get a policy by ID."""
50
+ row = self._cache.conn.execute(
51
+ "SELECT * FROM policies WHERE id = ?", (policy_id,)
52
+ ).fetchone()
53
+ if row is None:
54
+ return None
55
+ return self._row_to_policy(row)
56
+
57
+ def list_active(self, min_confidence: float = 0.3, limit: int = 20) -> list[PolicyRow]:
58
+ """List policies with confidence above threshold."""
59
+ rows = self._cache.conn.execute(
60
+ "SELECT * FROM policies WHERE confidence >= ? ORDER BY confidence DESC LIMIT ?",
61
+ (min_confidence, limit),
62
+ ).fetchall()
63
+ return [self._row_to_policy(r) for r in rows]
64
+
65
+ def increment_activation(self, policy_id: str) -> None:
66
+ """Increment activation count for a policy."""
67
+ self._cache.conn.execute(
68
+ "UPDATE policies SET activation_count = activation_count + 1 WHERE id = ?",
69
+ (policy_id,),
70
+ )
71
+ self._cache.conn.commit()
72
+
73
+ def count(self) -> int:
74
+ """Total policy count."""
75
+ row = self._cache.conn.execute("SELECT COUNT(*) as cnt FROM policies").fetchone()
76
+ return row["cnt"] if row else 0
77
+
78
+ @staticmethod
79
+ def _row_to_policy(row: Any) -> PolicyRow:
80
+ return PolicyRow(
81
+ id=row["id"],
82
+ name=row["name"],
83
+ description=row["description"],
84
+ trigger_pattern=row["trigger_pattern"],
85
+ action_template=row["action_template"],
86
+ embedding=json.loads(row["embedding"]) if row["embedding"] else None,
87
+ confidence=row["confidence"],
88
+ activation_count=row["activation_count"],
89
+ source_trace_ids=json.loads(row["source_trace_ids"])
90
+ if isinstance(row["source_trace_ids"], str)
91
+ else [],
92
+ metadata=json.loads(row["metadata"]) if isinstance(row["metadata"], str) else {},
93
+ created_at=row["created_at"],
94
+ )
@@ -0,0 +1,100 @@
1
+ """SQLite schema — traces, policies, skills tables with FTS5."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from cc_star.cache.connection import CacheConnection
6
+
7
+
8
+ def ensure_schema(conn_or_cache: CacheConnection) -> None:
9
+ """Create all tables and indexes if they don't exist."""
10
+ conn = conn_or_cache.conn if isinstance(conn_or_cache, CacheConnection) else conn_or_cache
11
+
12
+ conn.executescript("""
13
+ CREATE TABLE IF NOT EXISTS traces (
14
+ id TEXT PRIMARY KEY,
15
+ session_id TEXT NOT NULL,
16
+ turn_index INTEGER NOT NULL DEFAULT 0,
17
+ user_content TEXT NOT NULL,
18
+ assistant_content TEXT NOT NULL DEFAULT '',
19
+ embedding BLOB,
20
+ reward REAL NOT NULL DEFAULT 0.0,
21
+ tags TEXT DEFAULT '',
22
+ metadata TEXT DEFAULT '{}',
23
+ created_at TEXT NOT NULL,
24
+ synced INTEGER NOT NULL DEFAULT 0
25
+ );
26
+
27
+ CREATE INDEX IF NOT EXISTS idx_traces_session
28
+ ON traces(session_id);
29
+ CREATE INDEX IF NOT EXISTS idx_traces_created
30
+ ON traces(created_at);
31
+ CREATE INDEX IF NOT EXISTS idx_traces_synced
32
+ ON traces(synced);
33
+
34
+ CREATE TABLE IF NOT EXISTS policies (
35
+ id TEXT PRIMARY KEY,
36
+ name TEXT NOT NULL,
37
+ description TEXT NOT NULL DEFAULT '',
38
+ trigger_pattern TEXT NOT NULL DEFAULT '',
39
+ action_template TEXT NOT NULL DEFAULT '',
40
+ embedding BLOB,
41
+ confidence REAL NOT NULL DEFAULT 0.0,
42
+ activation_count INTEGER NOT NULL DEFAULT 0,
43
+ source_trace_ids TEXT DEFAULT '[]',
44
+ metadata TEXT DEFAULT '{}',
45
+ created_at TEXT NOT NULL,
46
+ synced INTEGER NOT NULL DEFAULT 0
47
+ );
48
+
49
+ CREATE INDEX IF NOT EXISTS idx_policies_confidence
50
+ ON policies(confidence DESC);
51
+
52
+ CREATE TABLE IF NOT EXISTS skills (
53
+ name TEXT PRIMARY KEY,
54
+ description TEXT NOT NULL DEFAULT '',
55
+ usage_guide TEXT NOT NULL DEFAULT '',
56
+ source_policy_ids TEXT DEFAULT '[]',
57
+ version INTEGER NOT NULL DEFAULT 1,
58
+ metadata TEXT DEFAULT '{}',
59
+ created_at TEXT NOT NULL
60
+ );
61
+
62
+ CREATE VIRTUAL TABLE IF NOT EXISTS traces_fts
63
+ USING fts5(
64
+ user_content,
65
+ assistant_content,
66
+ tags,
67
+ content='traces',
68
+ content_rowid='rowid'
69
+ );
70
+
71
+ CREATE TRIGGER IF NOT EXISTS traces_ai AFTER INSERT ON traces BEGIN
72
+ INSERT INTO traces_fts(rowid, user_content, assistant_content, tags)
73
+ VALUES (new.rowid, new.user_content, new.assistant_content, new.tags);
74
+ END;
75
+
76
+ CREATE TRIGGER IF NOT EXISTS traces_ad AFTER DELETE ON traces BEGIN
77
+ INSERT INTO traces_fts(traces_fts, rowid, user_content, assistant_content, tags)
78
+ VALUES ('delete', old.rowid, old.user_content, old.assistant_content, old.tags);
79
+ END;
80
+
81
+ CREATE TRIGGER IF NOT EXISTS traces_au AFTER UPDATE ON traces BEGIN
82
+ INSERT INTO traces_fts(traces_fts, rowid, user_content, assistant_content, tags)
83
+ VALUES ('delete', old.rowid, old.user_content, old.assistant_content, old.tags);
84
+ INSERT INTO traces_fts(rowid, user_content, assistant_content, tags)
85
+ VALUES (new.rowid, new.user_content, new.assistant_content, new.tags);
86
+ END;
87
+ """)
88
+ conn.commit()
89
+
90
+
91
+ def drop_schema(conn_or_cache: CacheConnection) -> None:
92
+ """Drop all tables (for testing)."""
93
+ conn = conn_or_cache.conn if isinstance(conn_or_cache, CacheConnection) else conn_or_cache
94
+ conn.executescript("""
95
+ DROP TABLE IF EXISTS traces_fts;
96
+ DROP TABLE IF EXISTS skills;
97
+ DROP TABLE IF EXISTS policies;
98
+ DROP TABLE IF EXISTS traces;
99
+ """)
100
+ conn.commit()
@@ -0,0 +1,89 @@
1
+ """Skill repository — local SQLite CRUD for skills."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, Optional
7
+
8
+ from cc_star.cache.connection import CacheConnection
9
+ from cc_star.cache.schema import ensure_schema
10
+ from cc_star.memos.types import SkillRow
11
+
12
+
13
+ class SkillRepository:
14
+ """Persist and query skills locally."""
15
+
16
+ def __init__(self, cache: CacheConnection):
17
+ self._cache = cache
18
+ ensure_schema(cache)
19
+
20
+ def insert(self, skill: SkillRow) -> None:
21
+ """Insert a skill into local cache."""
22
+ conn = self._cache.conn
23
+ conn.execute(
24
+ """
25
+ INSERT OR REPLACE INTO skills
26
+ (name, description, usage_guide, source_policy_ids,
27
+ version, metadata, created_at)
28
+ VALUES (?, ?, ?, ?, ?, ?, ?)
29
+ """,
30
+ (
31
+ skill.name,
32
+ skill.description,
33
+ skill.usage_guide,
34
+ json.dumps(skill.source_policy_ids, ensure_ascii=False),
35
+ skill.version,
36
+ json.dumps(skill.metadata, ensure_ascii=False, default=str),
37
+ skill.created_at,
38
+ ),
39
+ )
40
+ conn.commit()
41
+
42
+ def get(self, name: str) -> Optional[SkillRow]:
43
+ """Get a skill by name."""
44
+ row = self._cache.conn.execute(
45
+ "SELECT * FROM skills WHERE name = ?", (name,)
46
+ ).fetchone()
47
+ if row is None:
48
+ return None
49
+ return self._row_to_skill(row)
50
+
51
+ def list_all(self) -> list[SkillRow]:
52
+ """List all skills."""
53
+ rows = self._cache.conn.execute(
54
+ "SELECT * FROM skills ORDER BY name ASC"
55
+ ).fetchall()
56
+ return [self._row_to_skill(r) for r in rows]
57
+
58
+ def search(self, query: str, limit: int = 10) -> list[SkillRow]:
59
+ """Search skills by name or description."""
60
+ like = f"%{query}%"
61
+ rows = self._cache.conn.execute(
62
+ "SELECT * FROM skills WHERE name LIKE ? OR description LIKE ? LIMIT ?",
63
+ (like, like, limit),
64
+ ).fetchall()
65
+ return [self._row_to_skill(r) for r in rows]
66
+
67
+ def delete(self, name: str) -> None:
68
+ """Delete a skill by name."""
69
+ self._cache.conn.execute("DELETE FROM skills WHERE name = ?", (name,))
70
+ self._cache.conn.commit()
71
+
72
+ def count(self) -> int:
73
+ """Total skill count."""
74
+ row = self._cache.conn.execute("SELECT COUNT(*) as cnt FROM skills").fetchone()
75
+ return row["cnt"] if row else 0
76
+
77
+ @staticmethod
78
+ def _row_to_skill(row: Any) -> SkillRow:
79
+ return SkillRow(
80
+ name=row["name"],
81
+ description=row["description"],
82
+ usage_guide=row["usage_guide"],
83
+ source_policy_ids=json.loads(row["source_policy_ids"])
84
+ if isinstance(row["source_policy_ids"], str)
85
+ else [],
86
+ version=row["version"],
87
+ metadata=json.loads(row["metadata"]) if isinstance(row["metadata"], str) else {},
88
+ created_at=row["created_at"],
89
+ )