think-cli 0.2.0__py3-none-any.whl
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.
- think/__init__.py +3 -0
- think/__main__.py +6 -0
- think/cli.py +28 -0
- think/commands/__init__.py +5 -0
- think/commands/add.py +72 -0
- think/commands/delete.py +48 -0
- think/commands/get.py +34 -0
- think/commands/init.py +42 -0
- think/commands/ls.py +145 -0
- think/commands/review.py +72 -0
- think/commands/search.py +100 -0
- think/commands/stats.py +88 -0
- think/commands/update.py +107 -0
- think/config.py +32 -0
- think/const.py +25 -0
- think/core/models.py +85 -0
- think/db/__init__.py +10 -0
- think/db/connection.py +38 -0
- think/db/schema.py +55 -0
- think_cli-0.2.0.dist-info/METADATA +90 -0
- think_cli-0.2.0.dist-info/RECORD +25 -0
- think_cli-0.2.0.dist-info/WHEEL +5 -0
- think_cli-0.2.0.dist-info/entry_points.txt +2 -0
- think_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- think_cli-0.2.0.dist-info/top_level.txt +1 -0
think/__init__.py
ADDED
think/__main__.py
ADDED
think/cli.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""CLI 入口"""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from . import const
|
|
5
|
+
|
|
6
|
+
app = typer.Typer(
|
|
7
|
+
name="think",
|
|
8
|
+
help=f"思咚咚 - 记录灵感和想法 (v{const.VERSION})"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# 导入命令
|
|
12
|
+
from .commands import init, add, ls, get, delete, search, update, review, stats
|
|
13
|
+
|
|
14
|
+
app.command()(init.init)
|
|
15
|
+
app.command()(add.add)
|
|
16
|
+
app.command(name="list")(ls.list_ideas)
|
|
17
|
+
app.command()(get.get)
|
|
18
|
+
app.command()(delete.delete)
|
|
19
|
+
app.command()(search.search)
|
|
20
|
+
app.command()(update.update)
|
|
21
|
+
app.command()(review.review)
|
|
22
|
+
app.command()(stats.stats)
|
|
23
|
+
|
|
24
|
+
def main():
|
|
25
|
+
app()
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
main()
|
think/commands/add.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""add 命令"""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
import json
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from ..core.models import Idea
|
|
8
|
+
from ..db import get_connection
|
|
9
|
+
from ..const import PRIORITIES, STATUSES
|
|
10
|
+
from dong import json_output, ValidationError
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@json_output
|
|
18
|
+
def add(
|
|
19
|
+
content: str = typer.Argument(..., help="想法内容"),
|
|
20
|
+
tag: str = typer.Option(None, "--tag", "-t", help="标签,多个用逗号分隔"),
|
|
21
|
+
priority: str = typer.Option("normal", "--priority", "-p",
|
|
22
|
+
help="优先级: low/normal/high"),
|
|
23
|
+
context: str = typer.Option(None, "--context", "-c", help="上下文"),
|
|
24
|
+
source_agent: str = typer.Option(None, "--source", "-s", help="来源智能体"),
|
|
25
|
+
note: str = typer.Option(None, "--note", "-n", help="备注"),
|
|
26
|
+
):
|
|
27
|
+
"""记录想法"""
|
|
28
|
+
if not content or not content.strip():
|
|
29
|
+
raise ValidationError("content", "想法内容不能为空")
|
|
30
|
+
|
|
31
|
+
conn = get_connection()
|
|
32
|
+
|
|
33
|
+
# 处理标签
|
|
34
|
+
tags = []
|
|
35
|
+
if tag:
|
|
36
|
+
tags = [t.strip() for t in tag.split(",") if t.strip()]
|
|
37
|
+
|
|
38
|
+
# 创建想法
|
|
39
|
+
idea = Idea(
|
|
40
|
+
content=content.strip(),
|
|
41
|
+
tags=tags if tags else None,
|
|
42
|
+
priority=priority,
|
|
43
|
+
context=context,
|
|
44
|
+
source_agent=source_agent,
|
|
45
|
+
note=note,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# 插入数据库
|
|
49
|
+
cursor = conn.cursor()
|
|
50
|
+
cursor.execute(
|
|
51
|
+
"""
|
|
52
|
+
INSERT INTO ideas (content, tags, priority, context, source_agent, note, created_at, updated_at)
|
|
53
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
54
|
+
""",
|
|
55
|
+
(
|
|
56
|
+
idea.content,
|
|
57
|
+
json.dumps(idea.tags) if idea.tags else None,
|
|
58
|
+
idea.priority,
|
|
59
|
+
idea.context,
|
|
60
|
+
idea.source_agent,
|
|
61
|
+
idea.note,
|
|
62
|
+
datetime.now().isoformat(),
|
|
63
|
+
datetime.now().isoformat(),
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
conn.commit()
|
|
67
|
+
|
|
68
|
+
idea_id = cursor.lastrowid
|
|
69
|
+
conn.close()
|
|
70
|
+
|
|
71
|
+
idea.id = idea_id
|
|
72
|
+
return idea.to_dict()
|
think/commands/delete.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""delete 命令"""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
import json
|
|
5
|
+
from ..core.models import Idea
|
|
6
|
+
from ..db import get_connection
|
|
7
|
+
from dong import json_output, NotFoundError
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
@json_output
|
|
13
|
+
def delete(
|
|
14
|
+
idea_id: int = typer.Argument(..., help="想法 ID"),
|
|
15
|
+
force: bool = typer.Option(False, "--force", "-f", help="强制删除,不提示"),
|
|
16
|
+
):
|
|
17
|
+
"""删除想法
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
idea_id: 想法 ID
|
|
21
|
+
force: 强制删除,不提示
|
|
22
|
+
"""
|
|
23
|
+
conn = get_connection()
|
|
24
|
+
cursor = conn.cursor()
|
|
25
|
+
cursor.execute("SELECT * FROM ideas WHERE id = ?", (idea_id,))
|
|
26
|
+
row = cursor.fetchone()
|
|
27
|
+
|
|
28
|
+
if not row:
|
|
29
|
+
conn.close()
|
|
30
|
+
raise NotFoundError("Idea", idea_id, message=f"未找到 ID 为 {idea_id} 的想法")
|
|
31
|
+
|
|
32
|
+
idea = Idea.from_row(row)
|
|
33
|
+
|
|
34
|
+
if not force:
|
|
35
|
+
confirm = typer.confirm(f"确定要删除想法吗?\n{idea.content}")
|
|
36
|
+
if not confirm:
|
|
37
|
+
conn.close()
|
|
38
|
+
return {"cancelled": True, "message": "已取消删除"}
|
|
39
|
+
|
|
40
|
+
cursor.execute("DELETE FROM ideas WHERE id = ?", (idea_id,))
|
|
41
|
+
conn.commit()
|
|
42
|
+
conn.close()
|
|
43
|
+
|
|
44
|
+
return {"deleted": True, "id": idea_id}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
delete()
|
think/commands/get.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""get 命令"""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
import json
|
|
5
|
+
from ..core.models import Idea
|
|
6
|
+
from ..db import get_connection
|
|
7
|
+
from dong import json_output, NotFoundError
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
@json_output
|
|
13
|
+
def get(
|
|
14
|
+
idea_id: int = typer.Argument(..., help="想法 ID"),
|
|
15
|
+
):
|
|
16
|
+
"""获取想法详情
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
idea_id: 想法 ID """
|
|
20
|
+
conn = get_connection()
|
|
21
|
+
cursor = conn.cursor()
|
|
22
|
+
cursor.execute("SELECT * FROM ideas WHERE id = ?", (idea_id,))
|
|
23
|
+
row = cursor.fetchone()
|
|
24
|
+
conn.close()
|
|
25
|
+
|
|
26
|
+
if not row:
|
|
27
|
+
raise NotFoundError("Idea", idea_id, message=f"未找到 ID 为 {idea_id} 的想法")
|
|
28
|
+
|
|
29
|
+
idea = Idea.from_row(row)
|
|
30
|
+
return idea.to_dict()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
get()
|
think/commands/init.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""初始化命令"""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from ..db import init_database
|
|
7
|
+
from ..const import APP_NAME, VERSION, DATA_DIR
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
def init(
|
|
12
|
+
db_path: Path = None,
|
|
13
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="不提示,直接初始化"),
|
|
14
|
+
):
|
|
15
|
+
"""初始化数据库
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
db_path: 数据库文件路径,默认使用 ~/.think/think.db
|
|
19
|
+
yes: 不提示,直接初始化
|
|
20
|
+
"""
|
|
21
|
+
if db_path is None:
|
|
22
|
+
db_path = DATA_DIR / "think.db"
|
|
23
|
+
|
|
24
|
+
# 检查是否已存在
|
|
25
|
+
if db_path.exists():
|
|
26
|
+
if not yes:
|
|
27
|
+
confirm = typer.confirm(
|
|
28
|
+
f"{db_path} 已存在,是否覆盖?",
|
|
29
|
+
default=False,
|
|
30
|
+
)
|
|
31
|
+
if not confirm:
|
|
32
|
+
console.print("[yellow]已取消初始化[/yellow]")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
init_database(db_path)
|
|
37
|
+
console.print(f"[green]✅ {APP_NAME} 初始化成功[/green]")
|
|
38
|
+
console.print(f" 数据库路径: {db_path}")
|
|
39
|
+
console.print(f" 版本: v{VERSION}")
|
|
40
|
+
except Exception as e:
|
|
41
|
+
console.print(f"[red]❌ 初始化失败: {e}[/red]")
|
|
42
|
+
raise typer.Exit(code=1)
|
think/commands/ls.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""ls 命令"""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
import json
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from ..core.models import Idea
|
|
7
|
+
from ..db import get_connection
|
|
8
|
+
from ..const import DEFAULT_LIMIT, PRIORITIES, STATUSES
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def list_ideas(
|
|
17
|
+
limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-l", help="显示数量"),
|
|
18
|
+
today: bool = typer.Option(False, "--today", help="只显示今天的"),
|
|
19
|
+
week: bool = typer.Option(False, "--week", help="只显示本周的"),
|
|
20
|
+
tag: str = typer.Option(None, "--tag", help="按标签筛选"),
|
|
21
|
+
priority: str = typer.Option(None, "--priority", help="按优先级筛选: low/normal/high"),
|
|
22
|
+
status: str = typer.Option(None, "--status", help="按状态筛选"),
|
|
23
|
+
json_output: bool = typer.Option(False, "--json", help="JSON 输出"),
|
|
24
|
+
):
|
|
25
|
+
"""列出想法
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
limit: 显示数量
|
|
29
|
+
today: 只显示今天的
|
|
30
|
+
week: 只显示本周的
|
|
31
|
+
tag: 按标签筛选
|
|
32
|
+
priority: 按优先级筛选
|
|
33
|
+
status: 按状态筛选
|
|
34
|
+
json_output: JSON 输出
|
|
35
|
+
"""
|
|
36
|
+
conn = get_connection()
|
|
37
|
+
cursor = conn.cursor()
|
|
38
|
+
|
|
39
|
+
# 构建查询条件
|
|
40
|
+
conditions = []
|
|
41
|
+
params = []
|
|
42
|
+
|
|
43
|
+
if today:
|
|
44
|
+
today_str = datetime.now().strftime("%Y-%m-%d")
|
|
45
|
+
conditions.append("date(created_at) = ?")
|
|
46
|
+
params.append(today_str)
|
|
47
|
+
|
|
48
|
+
if week:
|
|
49
|
+
from datetime import timedelta
|
|
50
|
+
week_ago = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
|
|
51
|
+
conditions.append("date(created_at) >= ?")
|
|
52
|
+
params.append(week_ago)
|
|
53
|
+
|
|
54
|
+
if tag:
|
|
55
|
+
conditions.append("tags LIKE ?")
|
|
56
|
+
params.append(f"%{tag}%")
|
|
57
|
+
|
|
58
|
+
if priority:
|
|
59
|
+
if priority not in PRIORITIES:
|
|
60
|
+
console.print(f"[yellow]⚠️ 无效的优先级: {priority}[/yellow]")
|
|
61
|
+
priority = None
|
|
62
|
+
else:
|
|
63
|
+
conditions.append("priority = ?")
|
|
64
|
+
params.append(priority)
|
|
65
|
+
|
|
66
|
+
if status:
|
|
67
|
+
if status not in STATUSES:
|
|
68
|
+
console.print(f"[yellow]⚠️ 无效的状态: {status}[/yellow]")
|
|
69
|
+
status = None
|
|
70
|
+
else:
|
|
71
|
+
conditions.append("status = ?")
|
|
72
|
+
params.append(status)
|
|
73
|
+
|
|
74
|
+
where_clause = " AND ".join(conditions) if conditions else "1=1"
|
|
75
|
+
params.extend([limit, 0]) # limit 和 offset
|
|
76
|
+
|
|
77
|
+
query = f"""
|
|
78
|
+
SELECT * FROM ideas
|
|
79
|
+
WHERE {where_clause}
|
|
80
|
+
ORDER BY created_at DESC
|
|
81
|
+
LIMIT ? OFFSET ?
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
cursor.execute(query, params)
|
|
85
|
+
rows = cursor.fetchall()
|
|
86
|
+
conn.close()
|
|
87
|
+
|
|
88
|
+
if json_output:
|
|
89
|
+
ideas = [Idea.from_row(row).to_dict() for row in rows]
|
|
90
|
+
console.print(json.dumps(ideas, ensure_ascii=False, indent=2))
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
if not rows:
|
|
94
|
+
console.print("[yellow]没有想法记录[/yellow]")
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
# 创建表格
|
|
98
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
99
|
+
table.add_column("ID", style="dim", width=6)
|
|
100
|
+
table.add_column("内容", width=40)
|
|
101
|
+
table.add_column("标签", width=20)
|
|
102
|
+
table.add_column("优先级", width=10)
|
|
103
|
+
table.add_column("状态", width=10)
|
|
104
|
+
table.add_column("时间", style="dim")
|
|
105
|
+
|
|
106
|
+
for row in rows:
|
|
107
|
+
idea = Idea.from_row(row)
|
|
108
|
+
tags_str = ", ".join(idea.tags) if idea.tags else ""
|
|
109
|
+
time_str = idea.created_at.split("T")[0] if idea.created_at else ""
|
|
110
|
+
|
|
111
|
+
# 根据优先级设置颜色
|
|
112
|
+
priority_style = {
|
|
113
|
+
"high": "bold red",
|
|
114
|
+
"normal": "bold white",
|
|
115
|
+
"low": "yellow",
|
|
116
|
+
}.get(idea.priority, "")
|
|
117
|
+
|
|
118
|
+
# 根据状态设置颜色
|
|
119
|
+
status_style = {
|
|
120
|
+
"done": "green",
|
|
121
|
+
"doing": "cyan",
|
|
122
|
+
"todo": "yellow",
|
|
123
|
+
"idea": "dim",
|
|
124
|
+
}.get(idea.status, "")
|
|
125
|
+
|
|
126
|
+
table.add_row(
|
|
127
|
+
str(idea.id),
|
|
128
|
+
idea.content[:40] + "..." if len(idea.content) > 40 else idea.content,
|
|
129
|
+
tags_str[:20],
|
|
130
|
+
f"[{priority_style}]{idea.priority}[/{priority_style}]" if priority_style else idea.priority,
|
|
131
|
+
f"[{status_style}]{idea.status}[/{status_style}]" if status_style else idea.status,
|
|
132
|
+
time_str,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
console.print(table)
|
|
136
|
+
|
|
137
|
+
# 统计信息(重新查询,因为连接已关闭)
|
|
138
|
+
total_conn = get_connection()
|
|
139
|
+
total = total_conn.execute("SELECT COUNT(*) FROM ideas").fetchone()[0]
|
|
140
|
+
total_conn.close()
|
|
141
|
+
console.print(f"\n总计: {total} 条想法")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
list_ideas()
|
think/commands/review.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""review 命令"""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
import json
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from ..core.models import Idea
|
|
7
|
+
from ..db import get_connection
|
|
8
|
+
from ..const import STATUSES, PRIORITIES, APP_NAME
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def review(
|
|
16
|
+
today: bool = typer.Option(False, "--today", help="只显示今天的想法"),
|
|
17
|
+
week: bool = typer.Option(False, "--week", help="只显示本周的想法"),
|
|
18
|
+
random: bool = typer.Option(False, "--random", help="随机显示一条想法"),
|
|
19
|
+
):
|
|
20
|
+
"""回顾想法
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
today: 只显示今天的想法
|
|
24
|
+
week: 只显示本周的想法
|
|
25
|
+
random: 随机显示一条想法
|
|
26
|
+
"""
|
|
27
|
+
conn = get_connection()
|
|
28
|
+
cursor = conn.cursor()
|
|
29
|
+
|
|
30
|
+
query = "SELECT * FROM ideas ORDER BY RANDOM() LIMIT 1"
|
|
31
|
+
params = []
|
|
32
|
+
|
|
33
|
+
if today:
|
|
34
|
+
today_str = datetime.now().strftime("%Y-%m-%d")
|
|
35
|
+
query = "SELECT * FROM ideas WHERE date(created_at) = ? ORDER BY RANDOM() LIMIT 1"
|
|
36
|
+
params = [today_str]
|
|
37
|
+
elif week:
|
|
38
|
+
week_ago = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
|
|
39
|
+
query = "SELECT * FROM ideas WHERE date(created_at) >= ? ORDER BY RANDOM() LIMIT 1"
|
|
40
|
+
params = [week_ago]
|
|
41
|
+
|
|
42
|
+
cursor.execute(query, params)
|
|
43
|
+
row = cursor.fetchone()
|
|
44
|
+
conn.close()
|
|
45
|
+
|
|
46
|
+
if not row:
|
|
47
|
+
if today:
|
|
48
|
+
console.print(f"[yellow]{APP_NAME} 今天还没有想法记录[/yellow]")
|
|
49
|
+
elif week:
|
|
50
|
+
console.print(f"[yellow]{APP_NAME} 本周还没有想法记录[/yellow]")
|
|
51
|
+
else:
|
|
52
|
+
console.print(f"[yellow]{APP_NAME} 还没有想法记录,快去记一条吧!💡[/yellow]")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
idea = Idea.from_row(row)
|
|
56
|
+
|
|
57
|
+
# 使用 Rich 显示
|
|
58
|
+
console.print(Panel(
|
|
59
|
+
f"{idea.content}\n\n[italic]#{idea.id} | {idea.created_at.split('T')[0]}[/italic]",
|
|
60
|
+
title="💡 思考时间",
|
|
61
|
+
title_align="left",
|
|
62
|
+
))
|
|
63
|
+
|
|
64
|
+
if idea.tags:
|
|
65
|
+
tags_str = ", ".join(idea.tags)
|
|
66
|
+
console.print(f"[dim]标签: {tags_str}[/dim]")
|
|
67
|
+
|
|
68
|
+
console.print(f"[dim]上下文: {idea.context or '无'}[/dim]")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
review()
|
think/commands/search.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""search 命令"""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
import json
|
|
5
|
+
from ..core.models import Idea
|
|
6
|
+
from ..db import get_connection
|
|
7
|
+
from ..const import PRIORITIES, STATUSES
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def search(
|
|
15
|
+
keyword: str = typer.Argument(..., help="搜索关键词"),
|
|
16
|
+
tag: str = typer.Option(None, "--tag", help="按标签筛选"),
|
|
17
|
+
priority: str = typer.Option(None, "--priority", help="按优先级筛选: low/normal/high"),
|
|
18
|
+
status: str = typer.Option(None, "--status", help="按状态筛选"),
|
|
19
|
+
json_output: bool = typer.Option(False, "--json", help="JSON 输出"),
|
|
20
|
+
):
|
|
21
|
+
"""搜索想法
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
keyword: 搜索关键词
|
|
25
|
+
tag: 按标签筛选
|
|
26
|
+
priority: 按优先级筛选
|
|
27
|
+
status: 按状态筛选
|
|
28
|
+
json_output: JSON 输出
|
|
29
|
+
"""
|
|
30
|
+
conn = get_connection()
|
|
31
|
+
cursor = conn.cursor()
|
|
32
|
+
|
|
33
|
+
# 构建查询条件
|
|
34
|
+
conditions = ["content LIKE ?"] # 关键词搜索
|
|
35
|
+
params = [f"%{keyword}%"]
|
|
36
|
+
|
|
37
|
+
if tag:
|
|
38
|
+
conditions.append("tags LIKE ?")
|
|
39
|
+
params.append(f"%{tag}%")
|
|
40
|
+
|
|
41
|
+
if priority:
|
|
42
|
+
if priority not in PRIORITIES:
|
|
43
|
+
console.print(f"[yellow]⚠️ 无效的优先级: {priority}[/yellow]")
|
|
44
|
+
else:
|
|
45
|
+
conditions.append("priority = ?")
|
|
46
|
+
params.append(priority)
|
|
47
|
+
|
|
48
|
+
if status:
|
|
49
|
+
if status not in STATUSES:
|
|
50
|
+
console.print(f"[yellow]⚠️ 无效的状态: {status}[/yellow]")
|
|
51
|
+
else:
|
|
52
|
+
conditions.append("status = ?")
|
|
53
|
+
params.append(status)
|
|
54
|
+
|
|
55
|
+
where_clause = " AND ".join(conditions) if conditions else "1=1"
|
|
56
|
+
|
|
57
|
+
query = f"""
|
|
58
|
+
SELECT * FROM ideas
|
|
59
|
+
WHERE {where_clause}
|
|
60
|
+
ORDER BY created_at DESC
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
cursor.execute(query, params)
|
|
64
|
+
rows = cursor.fetchall()
|
|
65
|
+
conn.close()
|
|
66
|
+
|
|
67
|
+
if json_output:
|
|
68
|
+
ideas = [Idea.from_row(row).to_dict() for row in rows]
|
|
69
|
+
console.print(json.dumps(ideas, ensure_ascii=False, indent=2))
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
if not rows:
|
|
73
|
+
console.print(f"[yellow]未找到包含 '{keyword}' 的想法[/yellow]")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
# 创建表格
|
|
77
|
+
table = Table(show_header=True, header_style="bold magenta")
|
|
78
|
+
table.add_column("ID", style="dim", width=6)
|
|
79
|
+
table.add_column("内容", width=40)
|
|
80
|
+
table.add_column("标签", width=20)
|
|
81
|
+
table.add_column("时间", style="dim")
|
|
82
|
+
|
|
83
|
+
for row in rows:
|
|
84
|
+
idea = Idea.from_row(row)
|
|
85
|
+
tags_str = ", ".join(idea.tags) if idea.tags else ""
|
|
86
|
+
time_str = idea.created_at.split("T")[0] if idea.created_at else ""
|
|
87
|
+
|
|
88
|
+
table.add_row(
|
|
89
|
+
str(idea.id),
|
|
90
|
+
idea.content[:40] + "..." if len(idea.content) > 40 else idea.content,
|
|
91
|
+
tags_str[:20],
|
|
92
|
+
time_str,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
console.print(table)
|
|
96
|
+
console.print(f"[dim]找到 {len(rows)} 条结果[/dim]")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
search()
|
think/commands/stats.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""stats 命令"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from ..core.models import Idea
|
|
6
|
+
from ..db import get_connection
|
|
7
|
+
from ..const import APP_NAME, PRIORITIES, STATUSES
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.progress import Progress
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def stats():
|
|
16
|
+
"""统计想法信息"""
|
|
17
|
+
conn = get_connection()
|
|
18
|
+
cursor = conn.cursor()
|
|
19
|
+
|
|
20
|
+
# 总数
|
|
21
|
+
cursor.execute("SELECT COUNT(*) FROM ideas")
|
|
22
|
+
total = cursor.fetchone()[0]
|
|
23
|
+
|
|
24
|
+
# 按状态统计
|
|
25
|
+
cursor.execute("""
|
|
26
|
+
SELECT status, COUNT(*) as count
|
|
27
|
+
FROM ideas
|
|
28
|
+
GROUP BY status
|
|
29
|
+
""")
|
|
30
|
+
status_stats = {row["status"]: row["count"] for row in cursor.fetchall()}
|
|
31
|
+
|
|
32
|
+
# 按优先级统计
|
|
33
|
+
cursor.execute("""
|
|
34
|
+
SELECT priority, COUNT(*) as count
|
|
35
|
+
FROM ideas
|
|
36
|
+
GROUP BY priority
|
|
37
|
+
""")
|
|
38
|
+
priority_stats = {row["priority"]: row["count"] for row in cursor.fetchall()}
|
|
39
|
+
|
|
40
|
+
# 最近7天
|
|
41
|
+
week_ago = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
|
|
42
|
+
cursor.execute("SELECT COUNT(*) FROM ideas WHERE date(created_at) >= ?", [week_ago])
|
|
43
|
+
this_week = cursor.fetchone()[0]
|
|
44
|
+
|
|
45
|
+
# 最近30天
|
|
46
|
+
month_ago = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
|
|
47
|
+
cursor.execute("SELECT COUNT(*) FROM ideas WHERE date(created_at) >= ?", [month_ago])
|
|
48
|
+
this_month = cursor.fetchone()[0]
|
|
49
|
+
|
|
50
|
+
conn.close()
|
|
51
|
+
|
|
52
|
+
# 显示统计表
|
|
53
|
+
table = Table(title=f"📊 {APP_NAME} 统计", show_header=True, header_style="bold magenta")
|
|
54
|
+
table.add_column("指标", style="cyan")
|
|
55
|
+
table.add_column("数值", style="bold green")
|
|
56
|
+
|
|
57
|
+
table.add_row("总想法数", str(total))
|
|
58
|
+
table.add_row("本周新增", str(this_week))
|
|
59
|
+
table.add_row("本月新增", str(this_month))
|
|
60
|
+
table.add_row("", "")
|
|
61
|
+
|
|
62
|
+
# 状态统计
|
|
63
|
+
for status in STATUSES:
|
|
64
|
+
count = status_stats.get(status, 0)
|
|
65
|
+
if count > 0:
|
|
66
|
+
table.add_row(f" - {status}", str(count))
|
|
67
|
+
|
|
68
|
+
table.add_row("", "")
|
|
69
|
+
|
|
70
|
+
# 优先级统计
|
|
71
|
+
for priority in PRIORITIES:
|
|
72
|
+
count = priority_stats.get(priority, 0)
|
|
73
|
+
if count > 0:
|
|
74
|
+
table.add_row(f" - {priority}", str(count))
|
|
75
|
+
|
|
76
|
+
console.print(table)
|
|
77
|
+
|
|
78
|
+
# 显示建议
|
|
79
|
+
if total == 0:
|
|
80
|
+
console.print(f"\n💡 提示: 还没有想法记录,快去记一条吧!")
|
|
81
|
+
elif this_week == 0:
|
|
82
|
+
console.print(f"\n💡 提示: 本周还没有想法记录,快去记一条吧!")
|
|
83
|
+
else:
|
|
84
|
+
console.print(f"\n💡 提示: 记录想法,让你的思考有迹可循!")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
stats()
|
think/commands/update.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""update 命令"""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
import json
|
|
5
|
+
from ..core.models import Idea
|
|
6
|
+
from ..db import get_connection
|
|
7
|
+
from ..const import PRIORITIES, STATUSES
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def update(
|
|
14
|
+
idea_id: int = typer.Argument(..., help="想法 ID"),
|
|
15
|
+
status: str = typer.Option(None, "--status", help="更新状态"),
|
|
16
|
+
priority: str = typer.Option(None, "--priority", help="更新优先级: low/normal/high"),
|
|
17
|
+
tag_add: str = typer.Option(None, "--add-tag", help="添加标签"),
|
|
18
|
+
tag_remove: str = typer.Option(None, "--remove-tag", help="移除标签"),
|
|
19
|
+
note: str = typer.Option(None, "--note", help="更新备注"),
|
|
20
|
+
):
|
|
21
|
+
"""更新想法
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
idea_id: 想法 ID
|
|
25
|
+
status: 更新状态
|
|
26
|
+
priority: 更新优先级
|
|
27
|
+
tag_add: 添加标签
|
|
28
|
+
tag_remove: 移除标签
|
|
29
|
+
note: 更新备注
|
|
30
|
+
"""
|
|
31
|
+
conn = get_connection()
|
|
32
|
+
cursor = conn.cursor()
|
|
33
|
+
|
|
34
|
+
# 获取当前想法
|
|
35
|
+
cursor.execute("SELECT * FROM ideas WHERE id = ?", (idea_id,))
|
|
36
|
+
row = cursor.fetchone()
|
|
37
|
+
|
|
38
|
+
if not row:
|
|
39
|
+
console.print(f"[red]❌ 未找到 ID 为 {idea_id} 的想法[/red]")
|
|
40
|
+
conn.close()
|
|
41
|
+
raise typer.Exit(code=1)
|
|
42
|
+
|
|
43
|
+
idea = Idea.from_row(row)
|
|
44
|
+
|
|
45
|
+
# 解析当前标签
|
|
46
|
+
current_tags = idea.tags or []
|
|
47
|
+
|
|
48
|
+
# 更新状态
|
|
49
|
+
if status:
|
|
50
|
+
if status not in STATUSES:
|
|
51
|
+
console.print(f"[yellow]⚠️ 无效的状态: {status}[/yellow]")
|
|
52
|
+
else:
|
|
53
|
+
idea.status = status
|
|
54
|
+
|
|
55
|
+
# 更新优先级
|
|
56
|
+
if priority:
|
|
57
|
+
if priority not in PRIORITIES:
|
|
58
|
+
console.print(f"[yellow]⚠️ 无效的优先级: {priority}[/yellow]")
|
|
59
|
+
else:
|
|
60
|
+
idea.priority = priority
|
|
61
|
+
|
|
62
|
+
# 添加标签
|
|
63
|
+
if tag_add:
|
|
64
|
+
new_tag = tag_add.strip()
|
|
65
|
+
if new_tag and new_tag not in current_tags:
|
|
66
|
+
current_tags.append(new_tag)
|
|
67
|
+
idea.tags = current_tags
|
|
68
|
+
|
|
69
|
+
# 移除标签
|
|
70
|
+
if tag_remove:
|
|
71
|
+
tag_to_remove = tag_remove.strip()
|
|
72
|
+
if tag_to_remove in current_tags:
|
|
73
|
+
current_tags.remove(tag_to_remove)
|
|
74
|
+
idea.tags = current_tags if current_tags else None
|
|
75
|
+
|
|
76
|
+
# 更新备注
|
|
77
|
+
if note is not None:
|
|
78
|
+
idea.note = note
|
|
79
|
+
|
|
80
|
+
# 更新数据库
|
|
81
|
+
cursor.execute(
|
|
82
|
+
"""
|
|
83
|
+
UPDATE ideas
|
|
84
|
+
SET tags = ?, priority = ?, status = ?, note = ?, updated_at = ?
|
|
85
|
+
WHERE id = ?
|
|
86
|
+
""",
|
|
87
|
+
(
|
|
88
|
+
json.dumps(current_tags) if current_tags else None,
|
|
89
|
+
idea.priority,
|
|
90
|
+
idea.status,
|
|
91
|
+
idea.note,
|
|
92
|
+
idea.updated_at, # 会重新赋值
|
|
93
|
+
idea_id,
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
conn.commit()
|
|
97
|
+
conn.close()
|
|
98
|
+
|
|
99
|
+
console.print(f"[green]✅ 已更新想法 #{idea_id}[/green]")
|
|
100
|
+
if idea.status:
|
|
101
|
+
console.print(f" 状态: {idea.status}")
|
|
102
|
+
if idea.priority:
|
|
103
|
+
console.print(f" 优先级: {idea.priority}")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
update()
|
think/config.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""配置管理模块
|
|
2
|
+
|
|
3
|
+
继承 dong.config.Config,管理 think-cli 的用户配置。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dong.config import Config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ThinkConfig(Config):
|
|
10
|
+
"""思咚咚配置类"""
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def get_name(cls) -> str:
|
|
14
|
+
return "think"
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def get_defaults(cls) -> dict:
|
|
18
|
+
return {
|
|
19
|
+
"default_tags": ["idea", "insight"],
|
|
20
|
+
"default_limit": 20,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# 便捷函数
|
|
25
|
+
def get_config() -> dict:
|
|
26
|
+
return ThinkConfig.load()
|
|
27
|
+
|
|
28
|
+
def get_default_tags() -> list:
|
|
29
|
+
return ThinkConfig.get("default_tags", ["idea", "insight"])
|
|
30
|
+
|
|
31
|
+
def get_default_limit() -> int:
|
|
32
|
+
return ThinkConfig.get("default_limit", 20)
|
think/const.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""常量定义"""
|
|
2
|
+
|
|
3
|
+
VERSION = "0.1.0"
|
|
4
|
+
APP_NAME = "思咚咚"
|
|
5
|
+
|
|
6
|
+
# 数据目录 - 统一放在 ~/.dong/ 下
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
DATA_DIR = Path.home() / ".dong" / "think"
|
|
9
|
+
DB_PATH = DATA_DIR / "think.db"
|
|
10
|
+
|
|
11
|
+
# 默认值
|
|
12
|
+
DEFAULT_LIMIT = 20
|
|
13
|
+
|
|
14
|
+
# 优先级
|
|
15
|
+
PRIORITY_LOW = "low"
|
|
16
|
+
PRIORITY_NORMAL = "normal"
|
|
17
|
+
PRIORITY_HIGH = "high"
|
|
18
|
+
PRIORITIES = [PRIORITY_LOW, PRIORITY_NORMAL, PRIORITY_HIGH]
|
|
19
|
+
|
|
20
|
+
# 状态
|
|
21
|
+
STATUS_IDEA = "idea"
|
|
22
|
+
STATUS_TODO = "todo"
|
|
23
|
+
STATUS_DOING = "doing"
|
|
24
|
+
STATUS_DONE = "done"
|
|
25
|
+
STATUSES = [STATUS_IDEA, STATUS_TODO, STATUS_DOING, STATUS_DONE]
|
think/core/models.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""数据模型"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, List, Dict, Any
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from ..const import PRIORITIES, STATUSES
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Idea:
|
|
9
|
+
"""想法模型"""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
id: Optional[int] = None,
|
|
14
|
+
content: str = "",
|
|
15
|
+
tags: Optional[List[str]] = None,
|
|
16
|
+
priority: str = "normal",
|
|
17
|
+
status: str = "idea",
|
|
18
|
+
context: Optional[str] = None,
|
|
19
|
+
source_agent: Optional[str] = None,
|
|
20
|
+
note: Optional[str] = None,
|
|
21
|
+
created_at: Optional[str] = None,
|
|
22
|
+
updated_at: Optional[str] = None,
|
|
23
|
+
):
|
|
24
|
+
self.id = id
|
|
25
|
+
self.content = content
|
|
26
|
+
self.tags = tags or []
|
|
27
|
+
self.priority = priority
|
|
28
|
+
self.status = status
|
|
29
|
+
self.context = context
|
|
30
|
+
self.source_agent = source_agent
|
|
31
|
+
self.note = note
|
|
32
|
+
self.created_at = created_at
|
|
33
|
+
self.updated_at = updated_at
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_row(cls, row) -> "Idea":
|
|
37
|
+
"""从数据库行创建实例"""
|
|
38
|
+
tags = None
|
|
39
|
+
if row["tags"]:
|
|
40
|
+
import json
|
|
41
|
+
tags = json.loads(row["tags"])
|
|
42
|
+
|
|
43
|
+
return cls(
|
|
44
|
+
id=row["id"],
|
|
45
|
+
content=row["content"],
|
|
46
|
+
tags=tags,
|
|
47
|
+
priority=row["priority"],
|
|
48
|
+
status=row["status"],
|
|
49
|
+
context=row["context"],
|
|
50
|
+
source_agent=row["source_agent"],
|
|
51
|
+
note=row["note"],
|
|
52
|
+
created_at=row["created_at"],
|
|
53
|
+
updated_at=row["updated_at"],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
57
|
+
"""转换为字典"""
|
|
58
|
+
import json
|
|
59
|
+
return {
|
|
60
|
+
"id": self.id,
|
|
61
|
+
"content": self.content,
|
|
62
|
+
"tags": self.tags,
|
|
63
|
+
"priority": self.priority,
|
|
64
|
+
"status": self.status,
|
|
65
|
+
"context": self.context,
|
|
66
|
+
"source_agent": self.source_agent,
|
|
67
|
+
"note": self.note,
|
|
68
|
+
"created_at": self.created_at,
|
|
69
|
+
"updated_at": self.updated_at,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Idea":
|
|
74
|
+
"""从字典创建实例"""
|
|
75
|
+
return cls(**data)
|
|
76
|
+
|
|
77
|
+
def validate(self) -> bool:
|
|
78
|
+
"""验证数据"""
|
|
79
|
+
if not self.content or not self.content.strip():
|
|
80
|
+
return False
|
|
81
|
+
if self.priority not in PRIORITIES:
|
|
82
|
+
self.priority = PRIORITY_NORMAL
|
|
83
|
+
if self.status not in STATUSES:
|
|
84
|
+
self.status = STATUS_IDEA
|
|
85
|
+
return True
|
think/db/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""数据库层"""
|
|
2
|
+
|
|
3
|
+
from .connection import ThinkDatabase, get_connection, close_connection, get_cursor, get_db_path
|
|
4
|
+
from .schema import ThinkSchemaManager, SCHEMA_VERSION, get_schema_version, set_schema_version, is_initialized, init_database
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"ThinkDatabase", "ThinkSchemaManager",
|
|
8
|
+
"get_connection", "close_connection", "get_cursor", "get_db_path",
|
|
9
|
+
"SCHEMA_VERSION", "get_schema_version", "set_schema_version", "is_initialized", "init_database",
|
|
10
|
+
]
|
think/db/connection.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""数据库连接管理模块
|
|
2
|
+
|
|
3
|
+
继承 dong.db.Database,提供 think-cli 专用数据库访问。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sqlite3
|
|
7
|
+
from typing import Iterator
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
|
|
10
|
+
from dong.db import Database as DongDatabase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ThinkDatabase(DongDatabase):
|
|
14
|
+
"""思咚咚数据库类 - 继承自 dong.db.Database
|
|
15
|
+
|
|
16
|
+
数据库路径: ~/.think/think.db
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def get_name(cls) -> str:
|
|
21
|
+
"""返回 CLI 名称"""
|
|
22
|
+
return "think"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# 兼容性函数
|
|
26
|
+
def get_connection(db_path=None):
|
|
27
|
+
return ThinkDatabase.get_connection()
|
|
28
|
+
|
|
29
|
+
def close_connection():
|
|
30
|
+
ThinkDatabase.close_connection()
|
|
31
|
+
|
|
32
|
+
@contextmanager
|
|
33
|
+
def get_cursor() -> Iterator[sqlite3.Cursor]:
|
|
34
|
+
with ThinkDatabase.get_cursor() as cur:
|
|
35
|
+
yield cur
|
|
36
|
+
|
|
37
|
+
def get_db_path():
|
|
38
|
+
return ThinkDatabase.get_db_path()
|
think/db/schema.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""数据库 Schema 定义和版本管理
|
|
2
|
+
|
|
3
|
+
继承 dong.db.SchemaManager,管理 think-cli 的数据库 schema。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dong.db import SchemaManager
|
|
7
|
+
from .connection import ThinkDatabase
|
|
8
|
+
|
|
9
|
+
SCHEMA_VERSION = "1.0.0"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ThinkSchemaManager(SchemaManager):
|
|
13
|
+
"""思咚咚 Schema 管理器"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
super().__init__(
|
|
17
|
+
db_class=ThinkDatabase,
|
|
18
|
+
current_version=SCHEMA_VERSION
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def init_schema(self) -> None:
|
|
22
|
+
self._create_thoughts_table()
|
|
23
|
+
self._create_indexes()
|
|
24
|
+
|
|
25
|
+
def _create_thoughts_table(self) -> None:
|
|
26
|
+
with ThinkDatabase.get_cursor() as cur:
|
|
27
|
+
cur.execute("""
|
|
28
|
+
CREATE TABLE IF NOT EXISTS thoughts (
|
|
29
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
30
|
+
content TEXT NOT NULL,
|
|
31
|
+
tags TEXT,
|
|
32
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
33
|
+
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
34
|
+
)
|
|
35
|
+
""")
|
|
36
|
+
|
|
37
|
+
def _create_indexes(self) -> None:
|
|
38
|
+
with ThinkDatabase.get_cursor() as cur:
|
|
39
|
+
cur.execute("CREATE INDEX IF NOT EXISTS idx_thoughts_tags ON thoughts(tags)")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# 兼容性函数
|
|
43
|
+
def get_schema_version() -> str | None:
|
|
44
|
+
return ThinkSchemaManager().get_stored_version()
|
|
45
|
+
|
|
46
|
+
def set_schema_version(version: str) -> None:
|
|
47
|
+
ThinkDatabase.set_meta(ThinkSchemaManager.VERSION_KEY, version)
|
|
48
|
+
|
|
49
|
+
def is_initialized() -> bool:
|
|
50
|
+
return ThinkSchemaManager().is_initialized()
|
|
51
|
+
|
|
52
|
+
def init_database() -> None:
|
|
53
|
+
schema = ThinkSchemaManager()
|
|
54
|
+
if not schema.is_initialized():
|
|
55
|
+
schema.initialize()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: think-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: 思咚咚 - 记录灵感和想法的 CLI 工具
|
|
5
|
+
Author-email: gudong <gudong@example.com>
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: typer>=0.9.0
|
|
10
|
+
Requires-Dist: rich>=13.0.0
|
|
11
|
+
Requires-Dist: dong-core>=0.3.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# 思咚咚
|
|
18
|
+
|
|
19
|
+
> 💡 记录灵感和思考的 CLI 工具
|
|
20
|
+
|
|
21
|
+
## 安装
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install think-cli
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 快速开始
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 初始化
|
|
31
|
+
think init
|
|
32
|
+
|
|
33
|
+
# 记录想法
|
|
34
|
+
think add "Agent Hub 可以去中心化"
|
|
35
|
+
think add "想法:用 IPFS 存 Agent 包" --tag ipfs
|
|
36
|
+
|
|
37
|
+
# 列出想法
|
|
38
|
+
think list
|
|
39
|
+
think list --today
|
|
40
|
+
think list --tag agenthub
|
|
41
|
+
|
|
42
|
+
# 搜索
|
|
43
|
+
think search "去中心化"
|
|
44
|
+
|
|
45
|
+
# 回顾
|
|
46
|
+
think review --week
|
|
47
|
+
|
|
48
|
+
# 统计
|
|
49
|
+
think stats
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 命令
|
|
53
|
+
|
|
54
|
+
| 命令 | 说明 |
|
|
55
|
+
|------|------|
|
|
56
|
+
| `think init` | 初始化数据库 |
|
|
57
|
+
| `think add` | 记录想法 |
|
|
58
|
+
| `think list` | 列出想法 |
|
|
59
|
+
| `think get` | 获取详情 |
|
|
60
|
+
| `think search` | 搜索想法 |
|
|
61
|
+
| `think update` | 更新想法 |
|
|
62
|
+
| `think delete` | 删除想法 |
|
|
63
|
+
| `think review` | 回顾想法 |
|
|
64
|
+
| `think stats` | 统计信息 |
|
|
65
|
+
|
|
66
|
+
## 数据存储
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
~/.think/think.db
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## 开发
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# 克隆
|
|
76
|
+
git clone https://github.com/gudong/think-cli.git
|
|
77
|
+
cd think-cli
|
|
78
|
+
|
|
79
|
+
# 安装依赖
|
|
80
|
+
python -m venv venv
|
|
81
|
+
source venv/bin/activate
|
|
82
|
+
pip install -e ".[dev]"
|
|
83
|
+
|
|
84
|
+
# 运行测试
|
|
85
|
+
pytest
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
think/__init__.py,sha256=S5qYqf0ie6IRHe3mtP7C3P4VpDZFULVMYXzEDHDiTUg,77
|
|
2
|
+
think/__main__.py,sha256=gwBYhb7v8Vboj9OdpCaIlzb3IZtWKVDxgLp01sSerAA,80
|
|
3
|
+
think/cli.py,sha256=HgFJsPKZM1-XHNIq6tDwYd4pZ0DHFINSpB4s53OG3hI,569
|
|
4
|
+
think/config.py,sha256=vVgbMv_2ojpXUlull4-RfpAVxGwTEopzXy--8mivieY,665
|
|
5
|
+
think/const.py,sha256=BK-iWHjjuFQ2-ngiX4FMG5jrIrNIVDuSOrxofqVSOeU,542
|
|
6
|
+
think/commands/__init__.py,sha256=H7u0No2W0MN1V5fJQb9kq-8Fu9FtZ-SqBzPm6qS3D1M,190
|
|
7
|
+
think/commands/add.py,sha256=t_v-fU1BWTetD4CA4pd-alhMLKvSkTGOU3cCbXXJU6w,2018
|
|
8
|
+
think/commands/delete.py,sha256=ZIomQchsA7zMb72W3NI9KAPciUoBRABN368DdXQEhfg,1207
|
|
9
|
+
think/commands/get.py,sha256=MAZCojA2RHgsKBySh7Sgf9SG80jAHcSqa_GgodPlUK8,733
|
|
10
|
+
think/commands/init.py,sha256=TlxKyQuN6qPsUZ5p6PBcUoCauZ2D3LaWaowjWJGVGvs,1219
|
|
11
|
+
think/commands/ls.py,sha256=w5yaRYyH6yogbSUsZbmaDgCpV9ZTnFNLhacconSfUNc,4486
|
|
12
|
+
think/commands/review.py,sha256=hsh2Eb1ieSJcCrlDYz8GAxvlHfGSAWudjoVcWf-GTZI,2157
|
|
13
|
+
think/commands/search.py,sha256=RxPYoUQnq4w2vzEQQvczEZ8RRjF3KhjzDnKApHGiDj8,2875
|
|
14
|
+
think/commands/stats.py,sha256=8au7ap8QovyOL6xG7HEYW1AiLFUaPvEoK18pjGja3OE,2579
|
|
15
|
+
think/commands/update.py,sha256=2PvAAPOstGEKwVAeayfnQCSRd0WmMTQkJH3ZPnaMYw8,2933
|
|
16
|
+
think/core/models.py,sha256=vz5BHl_CDYiN48O7z2QhfNR1anQKpqaEJ8wg6mzN2co,2446
|
|
17
|
+
think/db/__init__.py,sha256=xE4Ey9ZKdPJCTg9cQQT9h4yrUKxOSTvs34Fy_pHcuTI,473
|
|
18
|
+
think/db/connection.py,sha256=E-n06UlPgJiuE_Zp8FqccrrWsd1S0NJT2BpvMER-mh8,820
|
|
19
|
+
think/db/schema.py,sha256=HfNzsdnlLb4ztMKdLOZnX-vu7ski9T6V5f9fdW5Axjo,1625
|
|
20
|
+
think_cli-0.2.0.dist-info/licenses/LICENSE,sha256=yx0ZLJJzA6p8qblMHjC8z10DetflV_asazG5AEu_zZE,1063
|
|
21
|
+
think_cli-0.2.0.dist-info/METADATA,sha256=tM68PQXvtfGArm6fxcPQzMBk90-HuzZ3qhEfgUNp6W8,1506
|
|
22
|
+
think_cli-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
23
|
+
think_cli-0.2.0.dist-info/entry_points.txt,sha256=TUhYRfcy3P0oP9k0KLpxg6gGtIjFae7WC9Jwx2VJ9hQ,40
|
|
24
|
+
think_cli-0.2.0.dist-info/top_level.txt,sha256=6rgE_Ok-kkHHwom-vkQYg4xwobkPqk7k8NM8L1NbSIk,6
|
|
25
|
+
think_cli-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 gudong
|
|
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
|
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
21
|
+
IN THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
think
|