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.
- cc_star-0.1.0/.github/workflows/publish.yml +32 -0
- cc_star-0.1.0/.gitignore +23 -0
- cc_star-0.1.0/LICENSE +17 -0
- cc_star-0.1.0/PKG-INFO +10 -0
- cc_star-0.1.0/README.md +101 -0
- cc_star-0.1.0/cc_star/__init__.py +3 -0
- cc_star-0.1.0/cc_star/cache/__init__.py +1 -0
- cc_star-0.1.0/cc_star/cache/connection.py +100 -0
- cc_star-0.1.0/cc_star/cache/policies.py +94 -0
- cc_star-0.1.0/cc_star/cache/schema.py +100 -0
- cc_star-0.1.0/cc_star/cache/skills.py +89 -0
- cc_star-0.1.0/cc_star/cache/traces.py +163 -0
- cc_star-0.1.0/cc_star/cache/vector.py +58 -0
- cc_star-0.1.0/cc_star/cli.py +286 -0
- cc_star-0.1.0/cc_star/config.py +146 -0
- cc_star-0.1.0/cc_star/installer.py +311 -0
- cc_star-0.1.0/cc_star/memos/__init__.py +1 -0
- cc_star-0.1.0/cc_star/memos/id.py +79 -0
- cc_star-0.1.0/cc_star/memos/types.py +148 -0
- cc_star-0.1.0/cc_star/ov/__init__.py +1 -0
- cc_star-0.1.0/cc_star/ov/client.py +184 -0
- cc_star-0.1.0/cc_star/retrieval/__init__.py +1 -0
- cc_star-0.1.0/cc_star/retrieval/ranker.py +112 -0
- cc_star-0.1.0/cc_star/templates/compact.py +157 -0
- cc_star-0.1.0/cc_star/templates/inject.py +169 -0
- cc_star-0.1.0/cc_star/templates/session_start.py +66 -0
- cc_star-0.1.0/cc_star/templates/store.py +197 -0
- cc_star-0.1.0/cc_star/templates/summary.py +168 -0
- cc_star-0.1.0/pyproject.toml +26 -0
- cc_star-0.1.0/test.txt +1 -0
|
@@ -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
|
cc_star-0.1.0/.gitignore
ADDED
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
|
cc_star-0.1.0/README.md
ADDED
|
@@ -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 @@
|
|
|
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
|
+
)
|