libero-mcp 0.1.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.
- package/LICENSE +21 -0
- package/README.md +21 -0
- package/data/migrations/001_initial_schema.sql +43 -0
- package/data/migrations/002_seed_categories.sql +18 -0
- package/data/migrations/003_add_rating.sql +4 -0
- package/data/migrations/004_timer_sessions.sql +21 -0
- package/data/migrations/005_profile_settings.sql +5 -0
- package/data/migrations/006_add_timeblock_status.sql +1 -0
- package/data/schema.sql +43 -0
- package/dist/data/db.js +61 -0
- package/dist/data/migrations/001_initial_schema.sql +43 -0
- package/dist/data/migrations/002_seed_categories.sql +18 -0
- package/dist/data/migrations/003_add_rating.sql +4 -0
- package/dist/data/migrations/004_timer_sessions.sql +21 -0
- package/dist/data/migrations/005_profile_settings.sql +5 -0
- package/dist/data/migrations/006_add_timeblock_status.sql +1 -0
- package/dist/data/schema.sql +43 -0
- package/dist/mcp/server-core.js +182 -0
- package/dist/mcp/server.js +30 -0
- package/dist/mcp/src/categories/index.js +12 -0
- package/dist/mcp/src/clock/index.js +57 -0
- package/dist/mcp/src/matrix/index.js +15 -0
- package/dist/mcp/src/profile/index.js +18 -0
- package/dist/mcp/src/repo/in-memory.js +15 -0
- package/dist/mcp/src/repo/sqlite-helpers.js +39 -0
- package/dist/mcp/src/repo/sqlite.js +461 -0
- package/dist/mcp/src/repo/types.js +4 -0
- package/dist/mcp/src/stats/index.js +26 -0
- package/dist/mcp/src/timeblock/index.js +60 -0
- package/dist/mcp/src/timer/index.js +116 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 amosyuan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# libero 2.0
|
|
2
|
+
|
|
3
|
+
AI 辅助人类精力管理:观察实绩 → 发现问题 → 对齐方案 → AI 辅助达成,PDCA 闭环。
|
|
4
|
+
|
|
5
|
+
2.0 = 一组 Skills(脑)+ 一层 MCP 工具(手)+ 一个数据后端(记忆),骑 Openclaw,本地优先。
|
|
6
|
+
|
|
7
|
+
## 快速开始
|
|
8
|
+
|
|
9
|
+
> MV-0 排雷中,runbook 待补充。
|
|
10
|
+
|
|
11
|
+
## 仓库结构
|
|
12
|
+
|
|
13
|
+
- `skills/` — 脑(prompt + markdown)
|
|
14
|
+
- `mcp/` — 手(确定性 MCP 工具)
|
|
15
|
+
- `data/` — 本地 DB schema + migrations
|
|
16
|
+
- `eval/` — Skill 评估与压力测试
|
|
17
|
+
- `docs/` — spec / plan / 决策记录
|
|
18
|
+
|
|
19
|
+
## 协作宪法
|
|
20
|
+
|
|
21
|
+
见 [AGENTS.md](AGENTS.md)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- 001_initial_schema: libero 2.0 核心表
|
|
2
|
+
-- K1 合规:所有业务表带 UUID PK + created_at + updated_at + deleted_at(软删除)
|
|
3
|
+
|
|
4
|
+
CREATE TABLE IF NOT EXISTS user_profile (
|
|
5
|
+
id TEXT PRIMARY KEY,
|
|
6
|
+
display_name TEXT NOT NULL DEFAULT '',
|
|
7
|
+
expectations TEXT,
|
|
8
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
9
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
10
|
+
deleted_at TEXT
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE TABLE IF NOT EXISTS time_blocks (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
user_id TEXT NOT NULL,
|
|
16
|
+
title TEXT NOT NULL,
|
|
17
|
+
category_id TEXT,
|
|
18
|
+
start_time TEXT NOT NULL,
|
|
19
|
+
end_time TEXT NOT NULL,
|
|
20
|
+
duration_minutes INTEGER,
|
|
21
|
+
notes TEXT,
|
|
22
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
23
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
24
|
+
deleted_at TEXT,
|
|
25
|
+
FOREIGN KEY (user_id) REFERENCES user_profile(id)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS categories (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
name TEXT NOT NULL,
|
|
31
|
+
emoji TEXT DEFAULT '',
|
|
32
|
+
color TEXT DEFAULT '',
|
|
33
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
34
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
35
|
+
deleted_at TEXT,
|
|
36
|
+
UNIQUE(name)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
40
|
+
version INTEGER PRIMARY KEY,
|
|
41
|
+
name TEXT NOT NULL,
|
|
42
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
43
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- 002_seed_categories: libero 2.0 默认 15 分类
|
|
2
|
+
|
|
3
|
+
INSERT INTO categories (id, name, emoji, color) VALUES
|
|
4
|
+
('cat-work', '工作', '💼', '#4A90D9'),
|
|
5
|
+
('cat-study', '学习', '📚', '#7B68EE'),
|
|
6
|
+
('cat-create', '创作', '✍️', '#E67E22'),
|
|
7
|
+
('cat-sleep', '睡眠', '😴', '#34495E'),
|
|
8
|
+
('cat-eat', '餐饮', '🍽️', '#E74C3C'),
|
|
9
|
+
('cat-chore', '内务', '🏠', '#95A5A6'),
|
|
10
|
+
('cat-commute', '通勤', '🚗', '#1ABC9C'),
|
|
11
|
+
('cat-exercise', '运动', '🏃', '#2ECC71'),
|
|
12
|
+
('cat-meditate', '冥想', '🧘', '#9B59B6'),
|
|
13
|
+
('cat-medical', '医疗', '🏥', '#E91E63'),
|
|
14
|
+
('cat-social', '社交', '👥', '#FF6F00'),
|
|
15
|
+
('cat-family', '陪伴', '👨👩👧', '#00BCD4'),
|
|
16
|
+
('cat-play', '娱乐', '🎮', '#F39C12'),
|
|
17
|
+
('cat-idle', '闲散', '☕', '#BDC3C7'),
|
|
18
|
+
('cat-other', '其他', '❓', '#7F8C8D');
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- 004_timer_sessions: multi-user persistent timer state
|
|
2
|
+
-- Eliminates module-level global `let state` (architecture pitfall A2).
|
|
3
|
+
-- Stores started_at + accumulated_ms invariants; elapsed is always recomputed.
|
|
4
|
+
|
|
5
|
+
CREATE TABLE IF NOT EXISTS timer_sessions (
|
|
6
|
+
id TEXT PRIMARY KEY,
|
|
7
|
+
user_id TEXT NOT NULL,
|
|
8
|
+
event TEXT NOT NULL,
|
|
9
|
+
category_id TEXT,
|
|
10
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
11
|
+
started_at TEXT NOT NULL,
|
|
12
|
+
accumulated_ms INTEGER NOT NULL DEFAULT 0,
|
|
13
|
+
ended_at TEXT,
|
|
14
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
15
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
16
|
+
FOREIGN KEY (user_id) REFERENCES user_profile(id)
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
-- Speed up "find active session for user" (at most one running/paused per user)
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_timer_active
|
|
21
|
+
ON timer_sessions (user_id, status);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE time_blocks ADD COLUMN status TEXT NOT NULL DEFAULT 'completed';
|
package/data/schema.sql
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- 001_initial_schema: libero 2.0 核心表
|
|
2
|
+
-- K1 合规:所有业务表带 UUID PK + created_at + updated_at + deleted_at(软删除)
|
|
3
|
+
|
|
4
|
+
CREATE TABLE IF NOT EXISTS user_profile (
|
|
5
|
+
id TEXT PRIMARY KEY,
|
|
6
|
+
display_name TEXT NOT NULL DEFAULT '',
|
|
7
|
+
expectations TEXT,
|
|
8
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
9
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
10
|
+
deleted_at TEXT
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE TABLE IF NOT EXISTS time_blocks (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
user_id TEXT NOT NULL,
|
|
16
|
+
title TEXT NOT NULL,
|
|
17
|
+
category_id TEXT,
|
|
18
|
+
start_time TEXT NOT NULL,
|
|
19
|
+
end_time TEXT NOT NULL,
|
|
20
|
+
duration_minutes INTEGER,
|
|
21
|
+
notes TEXT,
|
|
22
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
23
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
24
|
+
deleted_at TEXT,
|
|
25
|
+
FOREIGN KEY (user_id) REFERENCES user_profile(id)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS categories (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
name TEXT NOT NULL,
|
|
31
|
+
emoji TEXT DEFAULT '',
|
|
32
|
+
color TEXT DEFAULT '',
|
|
33
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
34
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
35
|
+
deleted_at TEXT,
|
|
36
|
+
UNIQUE(name)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
40
|
+
version INTEGER PRIMARY KEY,
|
|
41
|
+
name TEXT NOT NULL,
|
|
42
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
43
|
+
);
|
package/dist/data/db.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
let instance = null;
|
|
6
|
+
function runMigrations(db) {
|
|
7
|
+
db.exec(`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
9
|
+
version INTEGER PRIMARY KEY,
|
|
10
|
+
name TEXT NOT NULL,
|
|
11
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
12
|
+
);
|
|
13
|
+
`);
|
|
14
|
+
const migrationsDir = join(import.meta.dirname, "migrations");
|
|
15
|
+
const files = readdirSync(migrationsDir)
|
|
16
|
+
.filter((f) => f.endsWith(".sql"))
|
|
17
|
+
.sort();
|
|
18
|
+
const applied = new Set(db.prepare("SELECT version FROM _migrations").all().map((r) => r.version));
|
|
19
|
+
for (const file of files) {
|
|
20
|
+
const match = file.match(/^(\d+)_/);
|
|
21
|
+
if (!match)
|
|
22
|
+
continue;
|
|
23
|
+
const version = parseInt(match[1], 10);
|
|
24
|
+
if (applied.has(version))
|
|
25
|
+
continue;
|
|
26
|
+
const sql = readFileSync(join(migrationsDir, file), "utf-8");
|
|
27
|
+
db.exec(sql);
|
|
28
|
+
db.prepare("INSERT INTO _migrations (version, name) VALUES (?, ?)").run(version, file);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function initDb(dbPath) {
|
|
32
|
+
if (instance) {
|
|
33
|
+
instance.close();
|
|
34
|
+
instance = null;
|
|
35
|
+
}
|
|
36
|
+
const db = new Database(dbPath);
|
|
37
|
+
db.pragma("journal_mode = WAL");
|
|
38
|
+
db.pragma("foreign_keys = ON");
|
|
39
|
+
runMigrations(db);
|
|
40
|
+
instance = db;
|
|
41
|
+
return db;
|
|
42
|
+
}
|
|
43
|
+
export function getDb() {
|
|
44
|
+
if (!instance) {
|
|
45
|
+
throw new Error("Database not initialized. Call initDb() first.");
|
|
46
|
+
}
|
|
47
|
+
return instance;
|
|
48
|
+
}
|
|
49
|
+
export function closeDb() {
|
|
50
|
+
if (instance) {
|
|
51
|
+
instance.close();
|
|
52
|
+
instance = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const entry = process.argv[1];
|
|
56
|
+
if (entry && fileURLToPath(import.meta.url) === entry) {
|
|
57
|
+
const dbPath = process.env.DB_PATH ?? join(import.meta.dirname, "libero.db");
|
|
58
|
+
initDb(dbPath);
|
|
59
|
+
console.log(`DB initialized at ${dbPath}`);
|
|
60
|
+
closeDb();
|
|
61
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- 001_initial_schema: libero 2.0 核心表
|
|
2
|
+
-- K1 合规:所有业务表带 UUID PK + created_at + updated_at + deleted_at(软删除)
|
|
3
|
+
|
|
4
|
+
CREATE TABLE IF NOT EXISTS user_profile (
|
|
5
|
+
id TEXT PRIMARY KEY,
|
|
6
|
+
display_name TEXT NOT NULL DEFAULT '',
|
|
7
|
+
expectations TEXT,
|
|
8
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
9
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
10
|
+
deleted_at TEXT
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE TABLE IF NOT EXISTS time_blocks (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
user_id TEXT NOT NULL,
|
|
16
|
+
title TEXT NOT NULL,
|
|
17
|
+
category_id TEXT,
|
|
18
|
+
start_time TEXT NOT NULL,
|
|
19
|
+
end_time TEXT NOT NULL,
|
|
20
|
+
duration_minutes INTEGER,
|
|
21
|
+
notes TEXT,
|
|
22
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
23
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
24
|
+
deleted_at TEXT,
|
|
25
|
+
FOREIGN KEY (user_id) REFERENCES user_profile(id)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS categories (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
name TEXT NOT NULL,
|
|
31
|
+
emoji TEXT DEFAULT '',
|
|
32
|
+
color TEXT DEFAULT '',
|
|
33
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
34
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
35
|
+
deleted_at TEXT,
|
|
36
|
+
UNIQUE(name)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
40
|
+
version INTEGER PRIMARY KEY,
|
|
41
|
+
name TEXT NOT NULL,
|
|
42
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
43
|
+
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- 002_seed_categories: libero 2.0 默认 15 分类
|
|
2
|
+
|
|
3
|
+
INSERT INTO categories (id, name, emoji, color) VALUES
|
|
4
|
+
('cat-work', '工作', '💼', '#4A90D9'),
|
|
5
|
+
('cat-study', '学习', '📚', '#7B68EE'),
|
|
6
|
+
('cat-create', '创作', '✍️', '#E67E22'),
|
|
7
|
+
('cat-sleep', '睡眠', '😴', '#34495E'),
|
|
8
|
+
('cat-eat', '餐饮', '🍽️', '#E74C3C'),
|
|
9
|
+
('cat-chore', '内务', '🏠', '#95A5A6'),
|
|
10
|
+
('cat-commute', '通勤', '🚗', '#1ABC9C'),
|
|
11
|
+
('cat-exercise', '运动', '🏃', '#2ECC71'),
|
|
12
|
+
('cat-meditate', '冥想', '🧘', '#9B59B6'),
|
|
13
|
+
('cat-medical', '医疗', '🏥', '#E91E63'),
|
|
14
|
+
('cat-social', '社交', '👥', '#FF6F00'),
|
|
15
|
+
('cat-family', '陪伴', '👨👩👧', '#00BCD4'),
|
|
16
|
+
('cat-play', '娱乐', '🎮', '#F39C12'),
|
|
17
|
+
('cat-idle', '闲散', '☕', '#BDC3C7'),
|
|
18
|
+
('cat-other', '其他', '❓', '#7F8C8D');
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- 004_timer_sessions: multi-user persistent timer state
|
|
2
|
+
-- Eliminates module-level global `let state` (architecture pitfall A2).
|
|
3
|
+
-- Stores started_at + accumulated_ms invariants; elapsed is always recomputed.
|
|
4
|
+
|
|
5
|
+
CREATE TABLE IF NOT EXISTS timer_sessions (
|
|
6
|
+
id TEXT PRIMARY KEY,
|
|
7
|
+
user_id TEXT NOT NULL,
|
|
8
|
+
event TEXT NOT NULL,
|
|
9
|
+
category_id TEXT,
|
|
10
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
11
|
+
started_at TEXT NOT NULL,
|
|
12
|
+
accumulated_ms INTEGER NOT NULL DEFAULT 0,
|
|
13
|
+
ended_at TEXT,
|
|
14
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
15
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
16
|
+
FOREIGN KEY (user_id) REFERENCES user_profile(id)
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
-- Speed up "find active session for user" (at most one running/paused per user)
|
|
20
|
+
CREATE INDEX IF NOT EXISTS idx_timer_active
|
|
21
|
+
ON timer_sessions (user_id, status);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE time_blocks ADD COLUMN status TEXT NOT NULL DEFAULT 'completed';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- 001_initial_schema: libero 2.0 核心表
|
|
2
|
+
-- K1 合规:所有业务表带 UUID PK + created_at + updated_at + deleted_at(软删除)
|
|
3
|
+
|
|
4
|
+
CREATE TABLE IF NOT EXISTS user_profile (
|
|
5
|
+
id TEXT PRIMARY KEY,
|
|
6
|
+
display_name TEXT NOT NULL DEFAULT '',
|
|
7
|
+
expectations TEXT,
|
|
8
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
9
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
10
|
+
deleted_at TEXT
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE TABLE IF NOT EXISTS time_blocks (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
user_id TEXT NOT NULL,
|
|
16
|
+
title TEXT NOT NULL,
|
|
17
|
+
category_id TEXT,
|
|
18
|
+
start_time TEXT NOT NULL,
|
|
19
|
+
end_time TEXT NOT NULL,
|
|
20
|
+
duration_minutes INTEGER,
|
|
21
|
+
notes TEXT,
|
|
22
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
23
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
24
|
+
deleted_at TEXT,
|
|
25
|
+
FOREIGN KEY (user_id) REFERENCES user_profile(id)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS categories (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
name TEXT NOT NULL,
|
|
31
|
+
emoji TEXT DEFAULT '',
|
|
32
|
+
color TEXT DEFAULT '',
|
|
33
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
34
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
35
|
+
deleted_at TEXT,
|
|
36
|
+
UNIQUE(name)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
40
|
+
version INTEGER PRIMARY KEY,
|
|
41
|
+
name TEXT NOT NULL,
|
|
42
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
43
|
+
);
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { SqliteCategoryRepo, SqliteTimeBlockRepo, SqliteStatsRepo, SqliteTimerRepo, SqliteProfileRepo, } from "./src/repo/sqlite.js";
|
|
4
|
+
import { createCategoryTools } from "./src/categories/index.js";
|
|
5
|
+
import { createTimeBlockTools } from "./src/timeblock/index.js";
|
|
6
|
+
import { createStatsTools } from "./src/stats/index.js";
|
|
7
|
+
import { createMatrixTools } from "./src/matrix/index.js";
|
|
8
|
+
import { createTimerTools } from "./src/timer/index.js";
|
|
9
|
+
import { createProfileTools } from "./src/profile/index.js";
|
|
10
|
+
import { nowLocal, formatTime, parseTime } from "./src/clock/index.js";
|
|
11
|
+
/** Wrap any tool result as MCP text content (JSON). Void results become {ok:true}. */
|
|
12
|
+
function asContent(result) {
|
|
13
|
+
return {
|
|
14
|
+
content: [
|
|
15
|
+
{
|
|
16
|
+
type: "text",
|
|
17
|
+
text: JSON.stringify(result !== undefined ? result : { ok: true }),
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build the libero MCP server on a given DB connection.
|
|
24
|
+
* Tools throw Error on business violations; handlers let them propagate —
|
|
25
|
+
* the SDK turns thrown errors into MCP tool errors (isError) preserving the message.
|
|
26
|
+
*/
|
|
27
|
+
export function buildServer(db) {
|
|
28
|
+
// ── construct repos (S10 §3.3) ──
|
|
29
|
+
const categoryRepo = new SqliteCategoryRepo(db);
|
|
30
|
+
const timeblockRepo = new SqliteTimeBlockRepo(db);
|
|
31
|
+
const statsRepo = new SqliteStatsRepo(db);
|
|
32
|
+
const timerRepo = new SqliteTimerRepo(db);
|
|
33
|
+
const profileRepo = new SqliteProfileRepo(db);
|
|
34
|
+
// ── inject tool factories ──
|
|
35
|
+
const categories = createCategoryTools(categoryRepo);
|
|
36
|
+
const tb = createTimeBlockTools(timeblockRepo);
|
|
37
|
+
const stats = createStatsTools(statsRepo);
|
|
38
|
+
const matrix = createMatrixTools(statsRepo); // matrix reuses StatsRepo
|
|
39
|
+
const timer = createTimerTools(timerRepo);
|
|
40
|
+
const profile = createProfileTools(profileRepo);
|
|
41
|
+
const server = new McpServer({ name: "libero-mcp", version: "0.1.0" });
|
|
42
|
+
// ============================================================
|
|
43
|
+
// categories (2 tools)
|
|
44
|
+
// ============================================================
|
|
45
|
+
server.registerTool("categories.getAll", { description: "获取全部时间分类(15 类)", inputSchema: {} }, async () => asContent(await categories.getAllCategories()));
|
|
46
|
+
server.registerTool("categories.getById", { description: "按 id 获取单个分类", inputSchema: { id: z.string().min(1) } }, async ({ id }) => asContent(await categories.getCategoryById(id)));
|
|
47
|
+
// ============================================================
|
|
48
|
+
// timer (6 tools — positional userId args, R4 pattern)
|
|
49
|
+
// ============================================================
|
|
50
|
+
server.registerTool("timer.start", {
|
|
51
|
+
description: "开始一个计时会话",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
user_id: z.string().min(1),
|
|
54
|
+
event: z.string().min(1),
|
|
55
|
+
categoryId: z.string().optional(),
|
|
56
|
+
},
|
|
57
|
+
}, async ({ user_id, event, categoryId }) => asContent(await timer.start(user_id, event, categoryId)));
|
|
58
|
+
server.registerTool("timer.pause", { description: "暂停当前计时", inputSchema: { user_id: z.string().min(1) } }, async ({ user_id }) => asContent(await timer.pause(user_id)));
|
|
59
|
+
server.registerTool("timer.resume", { description: "恢复已暂停的计时", inputSchema: { user_id: z.string().min(1) } }, async ({ user_id }) => asContent(await timer.resume(user_id)));
|
|
60
|
+
server.registerTool("timer.stop", { description: "停止计时并返回记录", inputSchema: { user_id: z.string().min(1) } }, async ({ user_id }) => asContent(await timer.stop(user_id)));
|
|
61
|
+
server.registerTool("timer.status", { description: "查询某用户当前计时状态", inputSchema: { user_id: z.string().min(1) } }, async ({ user_id }) => asContent(await timer.status(user_id)));
|
|
62
|
+
server.registerTool("timer.elapsed", { description: "查询当前计时已过毫秒数", inputSchema: { user_id: z.string().min(1) } }, async ({ user_id }) => asContent(await timer.elapsed(user_id)));
|
|
63
|
+
// ============================================================
|
|
64
|
+
// timeblock (5 tools — single-object args)
|
|
65
|
+
// ============================================================
|
|
66
|
+
server.registerTool("timeblock.create", {
|
|
67
|
+
description: "创建一条时间记录",
|
|
68
|
+
inputSchema: {
|
|
69
|
+
user_id: z.string().min(1),
|
|
70
|
+
title: z.string().min(1),
|
|
71
|
+
category_id: z.string().optional(),
|
|
72
|
+
start_time: z.string(),
|
|
73
|
+
end_time: z.string(),
|
|
74
|
+
duration_minutes: z.number().optional(),
|
|
75
|
+
rating: z.number().min(1).max(5).optional(),
|
|
76
|
+
rating_reason: z.string().optional(),
|
|
77
|
+
notes: z.string().optional(),
|
|
78
|
+
},
|
|
79
|
+
}, async (input) => asContent(await tb.create(input)));
|
|
80
|
+
server.registerTool("timeblock.getById", { description: "按 id 获取单条时间记录", inputSchema: { id: z.string().min(1) } }, async ({ id }) => asContent(await tb.getById(id)));
|
|
81
|
+
server.registerTool("timeblock.update", {
|
|
82
|
+
description: "更新一条时间记录",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
id: z.string().min(1),
|
|
85
|
+
title: z.string().optional(),
|
|
86
|
+
category_id: z.string().optional(),
|
|
87
|
+
start_time: z.string().optional(),
|
|
88
|
+
end_time: z.string().optional(),
|
|
89
|
+
duration_minutes: z.number().optional(),
|
|
90
|
+
rating: z.number().min(1).max(5).optional(),
|
|
91
|
+
rating_reason: z.string().optional(),
|
|
92
|
+
notes: z.string().optional(),
|
|
93
|
+
},
|
|
94
|
+
}, async ({ id, ...rest }) => asContent(await tb.update(id, rest)));
|
|
95
|
+
server.registerTool("timeblock.remove", { description: "软删除一条时间记录", inputSchema: { id: z.string().min(1) } }, async ({ id }) => asContent(await tb.remove(id)));
|
|
96
|
+
server.registerTool("timeblock.restore", { description: "恢复一条已软删除的时间记录", inputSchema: { id: z.string().min(1) } }, async ({ id }) => asContent(await tb.restore(id)));
|
|
97
|
+
server.registerTool("timeblock.list", {
|
|
98
|
+
description: "按条件查询时间记录列表",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
user_id: z.string().min(1),
|
|
101
|
+
date: z.string().optional(),
|
|
102
|
+
from: z.string().optional(),
|
|
103
|
+
to: z.string().optional(),
|
|
104
|
+
category_id: z.string().optional(),
|
|
105
|
+
include_deleted: z.boolean().optional(),
|
|
106
|
+
limit: z.number().optional(),
|
|
107
|
+
offset: z.number().optional(),
|
|
108
|
+
},
|
|
109
|
+
}, async (input) => asContent(await tb.list(input)));
|
|
110
|
+
// ============================================================
|
|
111
|
+
// stats (2 tools — single-object filters)
|
|
112
|
+
// ============================================================
|
|
113
|
+
server.registerTool("stats.summarize", {
|
|
114
|
+
description: "按分类汇总时间统计(占比/记录数)",
|
|
115
|
+
inputSchema: {
|
|
116
|
+
user_id: z.string().min(1),
|
|
117
|
+
from: z.string().optional(),
|
|
118
|
+
to: z.string().optional(),
|
|
119
|
+
category_id: z.string().optional(),
|
|
120
|
+
},
|
|
121
|
+
}, async (input) => asContent(await stats.summarize(input)));
|
|
122
|
+
server.registerTool("stats.dailyBreakdown", {
|
|
123
|
+
description: "按天×分类拆分时间明细",
|
|
124
|
+
inputSchema: {
|
|
125
|
+
user_id: z.string().min(1),
|
|
126
|
+
from: z.string().optional(),
|
|
127
|
+
to: z.string().optional(),
|
|
128
|
+
category_id: z.string().optional(),
|
|
129
|
+
},
|
|
130
|
+
}, async (input) => asContent(await stats.dailyBreakdown(input)));
|
|
131
|
+
// ============================================================
|
|
132
|
+
// matrix (1 tool — single-object filters, reuses StatsRepo)
|
|
133
|
+
// ============================================================
|
|
134
|
+
server.registerTool("matrix.renderMatrix", {
|
|
135
|
+
description: "时间×评分四象限矩阵(能耗黑洞/保持区/亮点区/可放弃)",
|
|
136
|
+
inputSchema: {
|
|
137
|
+
user_id: z.string().min(1),
|
|
138
|
+
from: z.string().optional(),
|
|
139
|
+
to: z.string().optional(),
|
|
140
|
+
},
|
|
141
|
+
}, async (input) => asContent(await matrix.renderMatrix(input)));
|
|
142
|
+
// ============================================================
|
|
143
|
+
// clock (3 tools — pure functions, no repo)
|
|
144
|
+
// ============================================================
|
|
145
|
+
server.registerTool("clock.now", {
|
|
146
|
+
description: "返回当前 ISO 时间字符串(可选时区偏移)",
|
|
147
|
+
inputSchema: {
|
|
148
|
+
offsetMinutes: z.number().optional(),
|
|
149
|
+
},
|
|
150
|
+
}, async ({ offsetMinutes }) => asContent(offsetMinutes !== undefined ? nowLocal(offsetMinutes) : nowLocal()));
|
|
151
|
+
server.registerTool("clock.formatTime", {
|
|
152
|
+
description: "按指定格式格式化 ISO 时间",
|
|
153
|
+
inputSchema: {
|
|
154
|
+
iso: z.string().min(1),
|
|
155
|
+
format: z.string().min(1),
|
|
156
|
+
},
|
|
157
|
+
}, async ({ iso, format }) => asContent(formatTime(iso, format)));
|
|
158
|
+
server.registerTool("clock.parseTime", {
|
|
159
|
+
description: "解析 ISO 时间字符串为结构化字段",
|
|
160
|
+
inputSchema: { iso: z.string().min(1) },
|
|
161
|
+
}, async ({ iso }) => asContent(parseTime(iso)));
|
|
162
|
+
// ============================================================
|
|
163
|
+
// profile (2 tools — get / upsert)
|
|
164
|
+
// ============================================================
|
|
165
|
+
server.registerTool("profile.get", {
|
|
166
|
+
description: "获取用户 profile(含结构化约定)",
|
|
167
|
+
inputSchema: { user_id: z.string().min(1) },
|
|
168
|
+
}, async ({ user_id }) => asContent(await profile.get(user_id)));
|
|
169
|
+
server.registerTool("profile.upsert", {
|
|
170
|
+
description: "创建或更新用户 profile(结构化约定存入 settings JSON 列)",
|
|
171
|
+
inputSchema: {
|
|
172
|
+
user_id: z.string().min(1),
|
|
173
|
+
display_name: z.string().optional(),
|
|
174
|
+
expectations: z.string().optional(),
|
|
175
|
+
settings: z
|
|
176
|
+
.object({})
|
|
177
|
+
.passthrough()
|
|
178
|
+
.optional(),
|
|
179
|
+
},
|
|
180
|
+
}, async (input) => asContent(await profile.upsert(input)));
|
|
181
|
+
return server;
|
|
182
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* libero MCP server — stdio transport entry point (S10 §3.3).
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* DB_PATH=/path/to/libero.db node mcp/server.ts
|
|
7
|
+
* DB_PATH=/path/to/libero.db npx tsx mcp/server.ts
|
|
8
|
+
* libero-mcp (if package.json bin + tsx is configured)
|
|
9
|
+
*/
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import { initDb, closeDb } from "../data/db.js";
|
|
12
|
+
import { buildServer } from "./server-core.js";
|
|
13
|
+
const DB_PATH = process.env.DB_PATH ?? "./data/libero.db";
|
|
14
|
+
if (!DB_PATH) {
|
|
15
|
+
console.error("FATAL: DB_PATH environment variable not set.");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const db = initDb(DB_PATH);
|
|
19
|
+
console.error(`[libero-mcp] DB initialized at ${DB_PATH}`);
|
|
20
|
+
const server = buildServer(db);
|
|
21
|
+
const transport = new StdioServerTransport();
|
|
22
|
+
await server.connect(transport);
|
|
23
|
+
console.error(`[libero-mcp] Server connected via stdio`);
|
|
24
|
+
function shutdown() {
|
|
25
|
+
console.error("[libero-mcp] Shutting down…");
|
|
26
|
+
closeDb();
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
process.on("SIGINT", shutdown);
|
|
30
|
+
process.on("SIGTERM", shutdown);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Create categories tools backed by the given repository.
|
|
2
|
+
* ADR-0001: Dependency-injected — the caller provides the repo, not getDb(). */
|
|
3
|
+
export function createCategoryTools(repo) {
|
|
4
|
+
return {
|
|
5
|
+
async getAllCategories() {
|
|
6
|
+
return repo.getAll();
|
|
7
|
+
},
|
|
8
|
+
async getCategoryById(id) {
|
|
9
|
+
return repo.getById(id);
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|