taskai-cli 0.1.3__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.
- taskai/cli.py +312 -0
- taskai/config.py +36 -0
- taskai/help_menu.py +35 -0
- taskai/json_dir_database.py +176 -0
- taskai/models.py +57 -0
- taskai/services/ai.py +179 -0
- taskai/services/user_setup.py +63 -0
- taskai/views.py +93 -0
- taskai_cli-0.1.3.dist-info/METADATA +47 -0
- taskai_cli-0.1.3.dist-info/RECORD +12 -0
- taskai_cli-0.1.3.dist-info/WHEEL +4 -0
- taskai_cli-0.1.3.dist-info/entry_points.txt +2 -0
taskai/cli.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# standard lib
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
import builtins
|
|
6
|
+
|
|
7
|
+
# local
|
|
8
|
+
from taskai.json_dir_database import JsonDirectoryDatabase
|
|
9
|
+
from taskai.views import view_lists, view_item, view_items
|
|
10
|
+
from taskai.models import Base, TodoItem, TodoList, Comment
|
|
11
|
+
from taskai.services.ai import ai_headstart_service, ai_natural_language_service
|
|
12
|
+
from taskai.services.user_setup import user_setup_service
|
|
13
|
+
from taskai.help_menu import help_menu
|
|
14
|
+
from taskai.config import GlobalConfig
|
|
15
|
+
|
|
16
|
+
# external
|
|
17
|
+
from rich import print
|
|
18
|
+
|
|
19
|
+
# config
|
|
20
|
+
DB_PATH = ".taskai/task_db"
|
|
21
|
+
USER = os.getenv("USER")
|
|
22
|
+
db = JsonDirectoryDatabase(
|
|
23
|
+
DB_PATH,
|
|
24
|
+
USER
|
|
25
|
+
)
|
|
26
|
+
db.connect()
|
|
27
|
+
|
|
28
|
+
og_help = builtins.help
|
|
29
|
+
def new_help(*args, **kwargs):
|
|
30
|
+
print(f"help: args: {args}, kwargs: {kwargs}")
|
|
31
|
+
return og_help(*args, **kwargs)
|
|
32
|
+
builtins.help = new_help
|
|
33
|
+
|
|
34
|
+
class Controller:
|
|
35
|
+
|
|
36
|
+
# utilities
|
|
37
|
+
def _find_model_by_substring(attr: str, value: str) -> TodoItem|TodoList|Comment|None:
|
|
38
|
+
for id_ in [*db.lists.keys(),*db.items.keys(),*db.comments.keys()]:
|
|
39
|
+
model = db.read(id_)
|
|
40
|
+
if (
|
|
41
|
+
hasattr(model, attr) and
|
|
42
|
+
isinstance(getattr(model, attr), str) and
|
|
43
|
+
value in getattr(model, attr)
|
|
44
|
+
):
|
|
45
|
+
return model
|
|
46
|
+
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _debug(args, kwargs):
|
|
51
|
+
print("args:", args)
|
|
52
|
+
print("kwargs:", kwargs)
|
|
53
|
+
|
|
54
|
+
# CRUD
|
|
55
|
+
def show_all(show_done=False):
|
|
56
|
+
view_lists(db, db.lists, show_done=show_done)
|
|
57
|
+
|
|
58
|
+
def show_by_id(id_, show_done=False):
|
|
59
|
+
if id_ in db.items:
|
|
60
|
+
Controller.show_item(id_)
|
|
61
|
+
elif id_ in db.lists:
|
|
62
|
+
Controller.show_list(id_, show_done=show_done)
|
|
63
|
+
|
|
64
|
+
def show_by_list_name(value: str, show_done=False):
|
|
65
|
+
model = Controller._find_model_by_substring("name", value)
|
|
66
|
+
if isinstance(model, TodoList):
|
|
67
|
+
Controller.show_list(model.id, show_done=show_done)
|
|
68
|
+
else:
|
|
69
|
+
print(f"Could not find list with substring '{value}'")
|
|
70
|
+
|
|
71
|
+
def show_list(list_id: int|str, show_done=False):
|
|
72
|
+
view_lists(db, [list_id], show_done=show_done)
|
|
73
|
+
|
|
74
|
+
def show_item(item_id: int|str):
|
|
75
|
+
view_item(db, item_id)
|
|
76
|
+
|
|
77
|
+
def show_items(item_ids: str):
|
|
78
|
+
item_ids = item_ids.split(",")
|
|
79
|
+
view_items(db, item_ids)
|
|
80
|
+
|
|
81
|
+
def show_examples():
|
|
82
|
+
...
|
|
83
|
+
print("Not implemented yet")
|
|
84
|
+
|
|
85
|
+
def create_list(name: str):
|
|
86
|
+
list = TodoList(name=name)
|
|
87
|
+
list_id = db.create(list)
|
|
88
|
+
db.commit()
|
|
89
|
+
print(f"Creating list {list_id} - {list.name}")
|
|
90
|
+
|
|
91
|
+
def create_item(list_id: int|str, title: str, **kwargs):
|
|
92
|
+
try:
|
|
93
|
+
int(list_id)
|
|
94
|
+
except ValueError:
|
|
95
|
+
list_ = Controller._find_model_by_substring("name", list_id)
|
|
96
|
+
# TODO this should be just lists, not models
|
|
97
|
+
list_id = list_.id
|
|
98
|
+
|
|
99
|
+
item = TodoItem(title=title, list_id=list_id)
|
|
100
|
+
|
|
101
|
+
for k, v in kwargs.items():
|
|
102
|
+
if v is None:
|
|
103
|
+
continue
|
|
104
|
+
match k:
|
|
105
|
+
case "completed": item.completed = bool(v)
|
|
106
|
+
case "description": item.description = str(v)
|
|
107
|
+
case "due_by": item.due_by = datetime.strptime(v, "%Y-%m-%d")
|
|
108
|
+
case "parent": item.parent = str(v)
|
|
109
|
+
case "priority": item.priority = int(v)
|
|
110
|
+
case "depends_on": item.dependency_ids.extend(v.split(","))
|
|
111
|
+
# TODO handle recurrence
|
|
112
|
+
db.create(item)
|
|
113
|
+
db.commit()
|
|
114
|
+
|
|
115
|
+
def create_comment(item_id: int|str, content: str):
|
|
116
|
+
comment = Comment(
|
|
117
|
+
item_id=item_id,
|
|
118
|
+
content=content
|
|
119
|
+
)
|
|
120
|
+
db.create(comment)
|
|
121
|
+
db.commit()
|
|
122
|
+
|
|
123
|
+
def update_item(item_id: int|str, **kwargs):
|
|
124
|
+
item: TodoItem = db.read(item_id)
|
|
125
|
+
for k, v in kwargs.items():
|
|
126
|
+
if v is None:
|
|
127
|
+
continue
|
|
128
|
+
match k:
|
|
129
|
+
case "title": item.title = str(v)
|
|
130
|
+
case "list_id": item.list_id = str(v)
|
|
131
|
+
case "completed": item.completed = bool(v)
|
|
132
|
+
case "description": item.description = str(v)
|
|
133
|
+
case "due_by": item.due_by = datetime.strptime(v, "%Y-%m-%d")
|
|
134
|
+
case "parent": item.parent = str(v)
|
|
135
|
+
case "priority": item.priority = int(v)
|
|
136
|
+
# TODO handle recurrence
|
|
137
|
+
|
|
138
|
+
db.update(item)
|
|
139
|
+
db.commit()
|
|
140
|
+
|
|
141
|
+
def delete(id_: int|str):
|
|
142
|
+
db.delete(id_)
|
|
143
|
+
db.commit()
|
|
144
|
+
print(f"Deleted {id_}")
|
|
145
|
+
|
|
146
|
+
def delete_completed():
|
|
147
|
+
for item_id in db.items.copy():
|
|
148
|
+
item: TodoItem = db.read(item_id)
|
|
149
|
+
if item.completed:
|
|
150
|
+
db.delete(item_id)
|
|
151
|
+
db.commit()
|
|
152
|
+
|
|
153
|
+
def ai_headstart(item_id: int|str):
|
|
154
|
+
ai_response_text = ai_headstart_service(db, item_id)
|
|
155
|
+
comment_content = f"AI: {ai_response_text}"
|
|
156
|
+
Controller.create_comment(item_id, comment_content)
|
|
157
|
+
print(comment_content)
|
|
158
|
+
|
|
159
|
+
def ai_natural_language(prompt: str):
|
|
160
|
+
ai_natural_language_service(db, prompt)
|
|
161
|
+
|
|
162
|
+
def throw_error(error_description: str, *args, **kwargs):
|
|
163
|
+
print(f"[red]ERROR: {error_description}[/red]\nargs={args}\nkwargs={kwargs}")
|
|
164
|
+
|
|
165
|
+
def get_config_value(key: str):
|
|
166
|
+
print(db.get_config_value(key))
|
|
167
|
+
|
|
168
|
+
def list_config():
|
|
169
|
+
for k, v in db.get_config().items():
|
|
170
|
+
print(f"{k}={v}")
|
|
171
|
+
|
|
172
|
+
def set_config_value(key: str, value: any):
|
|
173
|
+
db.set_config_value(key, value)
|
|
174
|
+
db.commit()
|
|
175
|
+
print(f"setting {key}={value}")
|
|
176
|
+
|
|
177
|
+
def remove_config_value(key: str):
|
|
178
|
+
db.config.pop(key)
|
|
179
|
+
db.commit()
|
|
180
|
+
|
|
181
|
+
def run_setup_service():
|
|
182
|
+
user_setup_service(db)
|
|
183
|
+
|
|
184
|
+
# utilities
|
|
185
|
+
def _parse_remaining(remaining_args: list[str]) -> tuple[list, dict]:
|
|
186
|
+
|
|
187
|
+
for i, _arg in enumerate(remaining_args):
|
|
188
|
+
remaining_args[i] = _arg.replace(" ", "+-*/")
|
|
189
|
+
remaining_args = " ".join(remaining_args).replace("="," ").split(" ")
|
|
190
|
+
for i, _arg in enumerate(remaining_args):
|
|
191
|
+
remaining_args[i] = _arg.replace("+-*/", " ")
|
|
192
|
+
|
|
193
|
+
# outputs
|
|
194
|
+
args = []
|
|
195
|
+
kwargs = {}
|
|
196
|
+
|
|
197
|
+
while remaining_args:
|
|
198
|
+
next_arg = remaining_args.pop(0)
|
|
199
|
+
if next_arg.startswith("--"):
|
|
200
|
+
assert remaining_args, "kwarg specified with no value provided"
|
|
201
|
+
kwargs[next_arg[2:]] = remaining_args.pop(0)
|
|
202
|
+
else:
|
|
203
|
+
args.append(next_arg)
|
|
204
|
+
|
|
205
|
+
return args, kwargs
|
|
206
|
+
|
|
207
|
+
def _is_int(val: any) -> bool:
|
|
208
|
+
try:
|
|
209
|
+
int(val)
|
|
210
|
+
return True
|
|
211
|
+
except:
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
def _matches_string(val: str, target: str) -> bool:
|
|
215
|
+
# TODO make this follow linux rules
|
|
216
|
+
if val in target:
|
|
217
|
+
return True
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def entry_point():
|
|
223
|
+
|
|
224
|
+
arg_parser = argparse.ArgumentParser()
|
|
225
|
+
_, argv = arg_parser.parse_known_args()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
if not argv:
|
|
229
|
+
print(help_menu["general"])
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
if argv[0] in ("help","--help"):
|
|
233
|
+
print(help_menu["general"])
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
args, kwargs = _parse_remaining(argv)
|
|
237
|
+
GlobalConfig.load_dict(db.config)
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
match args[0]:
|
|
241
|
+
case "setup":
|
|
242
|
+
Controller.run_setup_service()
|
|
243
|
+
|
|
244
|
+
case "show":
|
|
245
|
+
match args[1]:
|
|
246
|
+
case "all": Controller.show_all(*args[2:], **kwargs)
|
|
247
|
+
case "list": Controller.show_list(*args[2:], **kwargs)
|
|
248
|
+
case "item": Controller.show_item(*args[2:], **kwargs)
|
|
249
|
+
case "items": Controller.show_item(*args[2:], **kwargs)
|
|
250
|
+
case "examples": Controller.show_examples()
|
|
251
|
+
case _ if _is_int(args[1]): Controller.show_by_id(*args[1:], **kwargs)
|
|
252
|
+
case _: Controller.show_by_list_name(args[1], **kwargs)
|
|
253
|
+
|
|
254
|
+
case "create":
|
|
255
|
+
match args[1]:
|
|
256
|
+
case "item": Controller.create_item(*args[2:], **kwargs)
|
|
257
|
+
case "list": Controller.create_list(*args[2:], **kwargs)
|
|
258
|
+
case "comment": Controller.create_comment(*args[2:], **kwargs)
|
|
259
|
+
case _: Controller.throw_error("uncrecognized create command", *args, **kwargs)
|
|
260
|
+
|
|
261
|
+
case "update":
|
|
262
|
+
match args[1]:
|
|
263
|
+
case _ if _is_int(args[1]): Controller.update_item(args[1], **kwargs)
|
|
264
|
+
case _: Controller.throw_error("uncregnozed update command", *args, **kwargs)
|
|
265
|
+
|
|
266
|
+
case "delete" | "remove":
|
|
267
|
+
match args[1]:
|
|
268
|
+
case _ if _is_int(args[1]): Controller.delete(args[1])
|
|
269
|
+
case "item": Controller.delete(args[2])
|
|
270
|
+
case "list": Controller.delete(args[2])
|
|
271
|
+
case "completed" | "done": Controller.delete_completed()
|
|
272
|
+
case _: Controller.throw_error("unrecognized delete command", *args, **kwargs)
|
|
273
|
+
|
|
274
|
+
case "comment":
|
|
275
|
+
match args[1]:
|
|
276
|
+
case _ if _is_int(args[1]): Controller.create_comment(*args[1:], **kwargs)
|
|
277
|
+
|
|
278
|
+
case "config":
|
|
279
|
+
match args[1]:
|
|
280
|
+
case "set": Controller.set_config_value(key=args[2], value=args[3])
|
|
281
|
+
case "get": Controller.get_config_value(key=args[2])
|
|
282
|
+
case "list"|"show": Controller.list_config()
|
|
283
|
+
case "pop": Controller.remove_config_value(key=args[2])
|
|
284
|
+
case _: Controller.throw_error("unrecognized command", *args, **kwargs)
|
|
285
|
+
|
|
286
|
+
case "ai":
|
|
287
|
+
match args[1]:
|
|
288
|
+
case "headstart": Controller.ai_headstart(*args[2:], **kwargs)
|
|
289
|
+
case _: Controller.ai_natural_language(" ".join(args[1:]))
|
|
290
|
+
|
|
291
|
+
case "nuke":
|
|
292
|
+
db.remove()
|
|
293
|
+
|
|
294
|
+
case "add":
|
|
295
|
+
Controller.create_item(*args[1:], **kwargs)
|
|
296
|
+
|
|
297
|
+
case "complete" | "done":
|
|
298
|
+
match args[1]:
|
|
299
|
+
case _ if _is_int(args[1]): Controller.update_item(args[1], completed=True)
|
|
300
|
+
case _: Controller.throw_error("unrecognized complete command", *args, **kwargs)
|
|
301
|
+
|
|
302
|
+
case "examples":
|
|
303
|
+
Controller.show_examples()
|
|
304
|
+
|
|
305
|
+
case _: Controller.throw_error("unrecognized command", *args, **kwargs)
|
|
306
|
+
except Exception as e:
|
|
307
|
+
Controller.throw_error(f"encountered exception '{e}'", *args, **kwargs)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
if __name__ == "__main__":
|
|
312
|
+
entry_point()
|
taskai/config.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
class GlobalConfig:
|
|
4
|
+
"""Global store for configuration values"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_data_store: dict[str, any]
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def load_dict(cls, d: dict):
|
|
11
|
+
"""loads values from a configuration"""
|
|
12
|
+
cls._data_store = d
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def get(cls, key: str):
|
|
16
|
+
"""Gets a value from a configuration"""
|
|
17
|
+
if key not in cls._data_store:
|
|
18
|
+
return None
|
|
19
|
+
return cls._data_store[key]
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def set(cls, key: str, value: any):
|
|
23
|
+
cls._data_store[key] = value
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def config(config_key: str, kwarg_name: str):
|
|
28
|
+
def decorator(fn):
|
|
29
|
+
def inner(*args, **kwargs):
|
|
30
|
+
kwargs[kwarg_name] = GlobalConfig.get(config_key)
|
|
31
|
+
result = fn(*args, **kwargs)
|
|
32
|
+
return result
|
|
33
|
+
return inner
|
|
34
|
+
return decorator
|
|
35
|
+
|
|
36
|
+
|
taskai/help_menu.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
help_general = """
|
|
5
|
+
|
|
6
|
+
Welcome to Task! Here's what you can do:
|
|
7
|
+
|
|
8
|
+
'task show all' --> show all of your lists and item titles, with their respective IDs prepended
|
|
9
|
+
'task show {id or substring}' --> find the list or item matching your identifier and show it using its respective type's show command
|
|
10
|
+
'task show list {id or substring}' --> show the list and all of its item titles
|
|
11
|
+
'task show item {id}' --> show the associated item and all of its specified information
|
|
12
|
+
'task show items {id1},{id2},...,{idx}' --> show the associated items and all of their specified information
|
|
13
|
+
'task create item {list id or substring} {title} {**kwargs}' --> Create a new item for the associated list. Can specify kwargs as --optional cli arguments.
|
|
14
|
+
'task create list {name}' --> create a new list by that name
|
|
15
|
+
'task delete {id}' --> deletes the list or item associated with that id
|
|
16
|
+
'task delete item {id}' --> deletes the item associated with that id
|
|
17
|
+
'task delete list {id}' --> deletes the list associated with that id
|
|
18
|
+
'task delete completed' --> deletes all items that have been completed
|
|
19
|
+
'task remove ...' --> aliases directly to 'task delete ...
|
|
20
|
+
'task update {item id} {**kwargs}' --> updates the associated attributes on the item
|
|
21
|
+
'task comment {item id} {content}' --> adds a comment to that items comment thread
|
|
22
|
+
'task ai {prompt}' --> Feeds a prompt directly to an LLM, which constructs and executes a series of task commands according to its interpretation of the prompt
|
|
23
|
+
'task ai headstart {item id}' --> Feeds the item context to an LLM, which responds with a concise description of the next step to perform. The response is added as a comment in the items comment thread
|
|
24
|
+
'task nuke' --> deletes all of your task data, letting you have a fresh start
|
|
25
|
+
'task add ...' -> aliases directly to 'task create item ...'
|
|
26
|
+
'task complete {item id}' --> sets the associated item's .completed attribute to true
|
|
27
|
+
|
|
28
|
+
Run 'task show examples' to print a comprehensive set of examples, and grep for the ones that you're interested in!
|
|
29
|
+
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
help_menu = {
|
|
34
|
+
"general": help_general,
|
|
35
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# standard lib
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import shutil
|
|
5
|
+
|
|
6
|
+
# local
|
|
7
|
+
from taskai.models import *
|
|
8
|
+
|
|
9
|
+
# external
|
|
10
|
+
import orjson as json
|
|
11
|
+
|
|
12
|
+
class JsonDirectoryDatabase:
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
dirpath: os.PathLike,
|
|
17
|
+
user: str
|
|
18
|
+
):
|
|
19
|
+
|
|
20
|
+
self.db_dir = Path(dirpath)
|
|
21
|
+
self.user_name = user
|
|
22
|
+
self.user_data_path = Path(dirpath) / (user.strip(" ") + ".json")
|
|
23
|
+
self.user_data = None
|
|
24
|
+
|
|
25
|
+
if not os.path.exists(self.db_dir):
|
|
26
|
+
self._setup_directory()
|
|
27
|
+
|
|
28
|
+
if not os.path.exists(self.user_data_path):
|
|
29
|
+
self._setup_user_data()
|
|
30
|
+
|
|
31
|
+
def remove(self):
|
|
32
|
+
if os.path.exists(self.db_dir):
|
|
33
|
+
shutil.rmtree(self.db_dir)
|
|
34
|
+
|
|
35
|
+
# setup utils
|
|
36
|
+
def _setup_directory(self):
|
|
37
|
+
os.makedirs(self.db_dir, exist_ok=True)
|
|
38
|
+
|
|
39
|
+
def _setup_user_data(self):
|
|
40
|
+
user_data = User(
|
|
41
|
+
id="0",
|
|
42
|
+
name=self.user_name
|
|
43
|
+
).model_dump()
|
|
44
|
+
|
|
45
|
+
user_data[TodoList.__name__] = {}
|
|
46
|
+
user_data[TodoItem.__name__] = {}
|
|
47
|
+
user_data[Comment.__name__] = {}
|
|
48
|
+
user_data["config"] = {}
|
|
49
|
+
|
|
50
|
+
self.user_data = user_data
|
|
51
|
+
self.commit()
|
|
52
|
+
|
|
53
|
+
# data accessors
|
|
54
|
+
@property
|
|
55
|
+
def lists(self) -> dict:
|
|
56
|
+
return self.user_data[TodoList.__name__]
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def items(self) -> dict:
|
|
60
|
+
return self.user_data[TodoItem.__name__]
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def comments(self) -> dict:
|
|
64
|
+
return self.user_data[Comment.__name__]
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def user_id(self):
|
|
68
|
+
return self.user_data["id"]
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def config(self) -> dict:
|
|
72
|
+
return self.user_data["config"]
|
|
73
|
+
|
|
74
|
+
# io
|
|
75
|
+
def connect(self):
|
|
76
|
+
with open(self.user_data_path, "rb") as f:
|
|
77
|
+
json_bstring = f.read()
|
|
78
|
+
self.user_data = json.loads(json_bstring)
|
|
79
|
+
|
|
80
|
+
def commit(self):
|
|
81
|
+
with open(self.user_data_path, "wb") as f:
|
|
82
|
+
json_bstring = json.dumps(self.user_data)
|
|
83
|
+
f.write(json_bstring)
|
|
84
|
+
|
|
85
|
+
def flush(self):
|
|
86
|
+
self.user_data = None
|
|
87
|
+
|
|
88
|
+
def close(self):
|
|
89
|
+
self.flush()
|
|
90
|
+
|
|
91
|
+
# crud
|
|
92
|
+
def create(self, record: Base) -> int:
|
|
93
|
+
|
|
94
|
+
record.id = str(self.user_data["id_counter"])
|
|
95
|
+
record.user_id = self.user_id
|
|
96
|
+
|
|
97
|
+
if isinstance(record, TodoList):
|
|
98
|
+
self.lists[record.id] = record.model_dump()
|
|
99
|
+
|
|
100
|
+
elif isinstance(record, TodoItem):
|
|
101
|
+
self.items[record.id] = record.model_dump()
|
|
102
|
+
self.lists[record.list_id]["item_ids"].append(record.id)
|
|
103
|
+
|
|
104
|
+
elif isinstance(record, Comment):
|
|
105
|
+
self.comments[record.id] = record.model_dump()
|
|
106
|
+
self.items[record.item_id]["comment_ids"].append(record.id)
|
|
107
|
+
else:
|
|
108
|
+
raise RuntimeError("don't know what you're creating")
|
|
109
|
+
|
|
110
|
+
self.user_data["id_counter"] += 1
|
|
111
|
+
return record.id
|
|
112
|
+
|
|
113
|
+
def read(self, id_: int|str) -> Base:
|
|
114
|
+
id_ = str(id_)
|
|
115
|
+
if id_ in self.user_data[TodoList.__name__]:
|
|
116
|
+
return TodoList(**self.lists[id_])
|
|
117
|
+
elif id_ in self.user_data[TodoItem.__name__]:
|
|
118
|
+
return TodoItem(**self.items[id_])
|
|
119
|
+
elif id_ in self.user_data[Comment.__name__]:
|
|
120
|
+
return Comment(**self.comments[id_])
|
|
121
|
+
else:
|
|
122
|
+
raise RuntimeError("Can't find record to read")
|
|
123
|
+
|
|
124
|
+
def update(self, record: Base) -> None:
|
|
125
|
+
|
|
126
|
+
if isinstance(record, TodoItem):
|
|
127
|
+
|
|
128
|
+
assert record.id in self.items, "item doesn't exist"
|
|
129
|
+
|
|
130
|
+
# update dependencies
|
|
131
|
+
old_item: TodoItem = self.items[record.id]
|
|
132
|
+
if old_item["list_id"] != record.list_id:
|
|
133
|
+
old_list: TodoList = self.read(old_item["list_id"])
|
|
134
|
+
old_list.item_ids.remove(record.id)
|
|
135
|
+
new_list: TodoList = self.read(record.list_id)
|
|
136
|
+
new_list.item_ids.append(record.id)
|
|
137
|
+
|
|
138
|
+
self.items[record.id] = record.model_dump()
|
|
139
|
+
|
|
140
|
+
else:
|
|
141
|
+
raise RuntimeError("don't know what you're creating")
|
|
142
|
+
|
|
143
|
+
def delete(self, id_: int|str) -> None:
|
|
144
|
+
id_ = str(id_)
|
|
145
|
+
if id_ in self.user_data[TodoList.__name__]:
|
|
146
|
+
list_: TodoList = TodoList(**self.lists.pop(id_))
|
|
147
|
+
for item_id in list_.item_ids:
|
|
148
|
+
self.items.pop(item_id)
|
|
149
|
+
elif id_ in self.user_data[TodoItem.__name__]:
|
|
150
|
+
item: TodoItem = TodoItem(**self.items.pop(id_))
|
|
151
|
+
self.lists[item.list_id]["item_ids"].remove(id_)
|
|
152
|
+
elif id_ in self.user_data[Comment.__name__]:
|
|
153
|
+
comment: Comment = Comment(**self.comments.pop(id_))
|
|
154
|
+
self.items[comment.item_id].comment_ids.remove(id_)
|
|
155
|
+
else:
|
|
156
|
+
raise RuntimeError("Don't know what you're deleting")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# Utilities
|
|
160
|
+
def get_list_by_name(self, name: str):
|
|
161
|
+
for list_id in self.lists:
|
|
162
|
+
list_: TodoList = self.read(list_id)
|
|
163
|
+
if list_.name.lower() == name.lower():
|
|
164
|
+
return list_
|
|
165
|
+
raise RuntimeError("Don't recognize list")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# Config
|
|
169
|
+
def get_config_value(self, key: str) -> any:
|
|
170
|
+
return self.user_data["config"].get(key)
|
|
171
|
+
|
|
172
|
+
def get_config(self) -> dict:
|
|
173
|
+
return self.user_data.get("config", {})
|
|
174
|
+
|
|
175
|
+
def set_config_value(self, key: str, value: any):
|
|
176
|
+
self.user_data["config"][key] = value
|
taskai/models.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# standard lib
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
|
|
6
|
+
# external
|
|
7
|
+
from pydantic import Field, BaseModel
|
|
8
|
+
|
|
9
|
+
class Base(BaseModel):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class PostgresConfig:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
class User(Base):
|
|
17
|
+
id: Optional[str] = Field(default=None, primary_key=True)
|
|
18
|
+
name: str
|
|
19
|
+
|
|
20
|
+
id_counter: int = 1
|
|
21
|
+
# password stuff
|
|
22
|
+
|
|
23
|
+
class TodoList(Base):
|
|
24
|
+
name: str
|
|
25
|
+
|
|
26
|
+
id: Optional[str] = Field(default=None, primary_key=True)
|
|
27
|
+
user_id: Optional[str] = None
|
|
28
|
+
item_ids: Optional[list[str]] = Field(default_factory=list)
|
|
29
|
+
|
|
30
|
+
class TodoItem(Base):
|
|
31
|
+
title: str = Field(index=True)
|
|
32
|
+
|
|
33
|
+
id: Optional[str] = Field(default=None, primary_key=True)
|
|
34
|
+
list_id: Optional[str] = None
|
|
35
|
+
user_id: Optional[str] = None
|
|
36
|
+
created_on: datetime = Field(default_factory=datetime.now)
|
|
37
|
+
completed: bool = False
|
|
38
|
+
description: str = ""
|
|
39
|
+
due_by: Optional[datetime] = None
|
|
40
|
+
parent: Optional[str] = None
|
|
41
|
+
comment_ids: list[str] = Field(default_factory=list)
|
|
42
|
+
dependency_ids: list[str] = Field(default_factory=list)
|
|
43
|
+
priority: int = 0
|
|
44
|
+
recurs_every: Optional[list[timedelta]] = None
|
|
45
|
+
recurs_until: Optional[datetime] = None
|
|
46
|
+
recur_keep_incomplete: bool = False
|
|
47
|
+
|
|
48
|
+
class Comment(Base):
|
|
49
|
+
content: str
|
|
50
|
+
|
|
51
|
+
item_id: Optional[str] = None
|
|
52
|
+
id: Optional[str] = Field(default=None, primary_key=True)
|
|
53
|
+
user_id: Optional[str] = None
|
|
54
|
+
created_on: datetime = Field(default_factory=datetime.now)
|
|
55
|
+
|
|
56
|
+
class LLMConfig(Base):
|
|
57
|
+
pass
|
taskai/services/ai.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This file defines services for calling an LLM.
|
|
3
|
+
|
|
4
|
+
All services are idempotent
|
|
5
|
+
"""
|
|
6
|
+
# standard lib
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
# local
|
|
10
|
+
from taskai.json_dir_database import JsonDirectoryDatabase
|
|
11
|
+
from taskai.models import TodoItem, TodoList, Comment
|
|
12
|
+
from taskai.config import config
|
|
13
|
+
from taskai.help_menu import help_general
|
|
14
|
+
|
|
15
|
+
@config("GEMINI_API_KEY", "api_key")
|
|
16
|
+
@config("GEMINI_MODEL", "model_name")
|
|
17
|
+
def ai_headstart_service(
|
|
18
|
+
db: JsonDirectoryDatabase,
|
|
19
|
+
item_id: str,
|
|
20
|
+
api_key: str,
|
|
21
|
+
model_name: str
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
This service queries an LLM for your task and asks it to give you
|
|
25
|
+
a headstart on a given task. It will perform the following operations:
|
|
26
|
+
|
|
27
|
+
- Compile the context for the query, including task description, dependencies
|
|
28
|
+
and previous comments
|
|
29
|
+
- Query the LLM
|
|
30
|
+
- Parse and return its response
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
item:TodoItem = db.read(item_id)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
from google import genai
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# contruct prompt
|
|
42
|
+
prompt = f"""
|
|
43
|
+
Hi Gemini, you're job is to provide a very succinct headstart for a taskai item.
|
|
44
|
+
This will involve the following:
|
|
45
|
+
- parsing the task information, including description, comments and dependencies
|
|
46
|
+
- performing any necessary internet searches in order to acquire relevant information
|
|
47
|
+
- deciding on what the next immediate step to be taken is
|
|
48
|
+
- returning a very succinct command to the user, with the information necessary to execute that command
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
Examples of good responses:
|
|
52
|
+
|
|
53
|
+
"Call the florist: 443-869-2158"
|
|
54
|
+
"Email HR @ hr@comapany.com 'Hi, I won't be able to make it in today'"
|
|
55
|
+
"Write the natural language service interface:\ndef natural_language_service(db:):\n\t..."
|
|
56
|
+
|
|
57
|
+
These should be short and to the point. Your response should contain NOTHING but the comment for the user.
|
|
58
|
+
|
|
59
|
+
Here's the relevant information:
|
|
60
|
+
|
|
61
|
+
task title: {item.title}
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
if item.description:
|
|
65
|
+
prompt += f"\ntask description: {item.description}"
|
|
66
|
+
if item.comment_ids:
|
|
67
|
+
|
|
68
|
+
prompt += f"\ntask comments:"
|
|
69
|
+
for comment_id in item.comment_ids:
|
|
70
|
+
comment: Comment = db.read(comment_id)
|
|
71
|
+
prompt += f"\n\t- {comment.content}"
|
|
72
|
+
|
|
73
|
+
# return "Survey says go fuck yourself"
|
|
74
|
+
|
|
75
|
+
# query model
|
|
76
|
+
client = genai.Client(api_key=api_key)
|
|
77
|
+
response = client.models.generate_content(
|
|
78
|
+
model=model_name,
|
|
79
|
+
contents=prompt
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return response.text
|
|
83
|
+
|
|
84
|
+
@config("GEMINI_API_KEY", "api_key")
|
|
85
|
+
@config("GEMINI_MODEL", "model_name")
|
|
86
|
+
def ai_natural_language_service(
|
|
87
|
+
db: JsonDirectoryDatabase,
|
|
88
|
+
prompt: str,
|
|
89
|
+
api_key: str,
|
|
90
|
+
model_name: str
|
|
91
|
+
):
|
|
92
|
+
"""
|
|
93
|
+
This service queries an LLM with a natural language
|
|
94
|
+
prompt from the user. The response is a series of terminal commands
|
|
95
|
+
called directly
|
|
96
|
+
"""
|
|
97
|
+
print(f"Ai prompt: {prompt}")
|
|
98
|
+
|
|
99
|
+
# build user info
|
|
100
|
+
user_info = []
|
|
101
|
+
for id_ in db.lists:
|
|
102
|
+
list: TodoList = db.read(id_)
|
|
103
|
+
user_info.append(f"{list.id} {list.name}")
|
|
104
|
+
for item_id in list.item_ids:
|
|
105
|
+
|
|
106
|
+
item: TodoItem = db.read(item_id)
|
|
107
|
+
if not item.completed:
|
|
108
|
+
user_info.append(f"\t {item.id} {item.title}")
|
|
109
|
+
user_info = "\n".join(user_info)
|
|
110
|
+
|
|
111
|
+
# build ai prompt
|
|
112
|
+
ai_prompt = f"""
|
|
113
|
+
You are a todo-list agent. Your job is to convert a natural language description from a user into a set
|
|
114
|
+
of CLI operations using our app. Here are a comprehensive list of operations that can be performed
|
|
115
|
+
|
|
116
|
+
{help_general}
|
|
117
|
+
|
|
118
|
+
Here are all of the user's list and task titles, each prepended with their id
|
|
119
|
+
|
|
120
|
+
{user_info}
|
|
121
|
+
|
|
122
|
+
Here is the user's prompt:
|
|
123
|
+
|
|
124
|
+
{prompt}
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
ai_prompt += """
|
|
128
|
+
Your response should be a JSON output with a valid list of commands, as specified by the description above.
|
|
129
|
+
Each command should be structured in the following format:
|
|
130
|
+
{
|
|
131
|
+
"command": ... -> the subcommand you want to use (omit the first 'task' here)
|
|
132
|
+
"args": [...] -> the positional arguments to use
|
|
133
|
+
"kwargs": {"..." : "...", ...} -> the keyword arguments to use. All keyword argument names should be prepended with '--'
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
Here is an example of a response that you could return:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
[
|
|
140
|
+
{
|
|
141
|
+
"command": "add",
|
|
142
|
+
"args": ["Daily", "Take out the trash"],
|
|
143
|
+
"kwargs": {"--description": "some description"}
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
"command": "add",
|
|
147
|
+
"args": ["Daily", "Walk the dog"]
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"command": "delete",
|
|
151
|
+
"args": ["Old Daily List"]
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
It is ABSOLUTELY IMPERATIVE that your response be valid json, as your output is going to be parsed directly. Return NOTHING but the json output.
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
from google import genai
|
|
162
|
+
|
|
163
|
+
print(ai_prompt)
|
|
164
|
+
client = genai.Client(api_key=api_key)
|
|
165
|
+
response = client.models.generate_content(
|
|
166
|
+
model=model_name,
|
|
167
|
+
contents=prompt
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
response_json = json.loads(response.text)
|
|
172
|
+
print(response_json)
|
|
173
|
+
except json.JSONDecodeError:
|
|
174
|
+
print(response.text)
|
|
175
|
+
print("\n\ndecode error")
|
|
176
|
+
import sys
|
|
177
|
+
sys.exit(-1)
|
|
178
|
+
|
|
179
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This service will handle user setup and configuration
|
|
3
|
+
|
|
4
|
+
Basically, we're going to have a set of steps that we're going to iterate through
|
|
5
|
+
"""
|
|
6
|
+
from taskai.json_dir_database import JsonDirectoryDatabase
|
|
7
|
+
|
|
8
|
+
from rich.prompt import Prompt
|
|
9
|
+
from rich import print
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def user_setup_service(
|
|
14
|
+
db: JsonDirectoryDatabase
|
|
15
|
+
):
|
|
16
|
+
|
|
17
|
+
config = db.config
|
|
18
|
+
# setup gemini model
|
|
19
|
+
print("Beginning setup")
|
|
20
|
+
if "GEMINI_API_KEY" not in config:
|
|
21
|
+
api_key = _get_gemini_api_key()
|
|
22
|
+
if api_key:
|
|
23
|
+
config["GEMINI_API_KEY"] = api_key
|
|
24
|
+
else:
|
|
25
|
+
print("gemini key already specified")
|
|
26
|
+
|
|
27
|
+
if "GEMINI_MODEL" not in config:
|
|
28
|
+
model = _select_gemini_model()
|
|
29
|
+
if model:
|
|
30
|
+
config["GEMINI_MODEL"] = model
|
|
31
|
+
else:
|
|
32
|
+
print("gemini model already specified")
|
|
33
|
+
print("Setup complete! Use 'task config set|get|list' to interact with your configuration options")
|
|
34
|
+
db.commit()
|
|
35
|
+
|
|
36
|
+
def _get_gemini_api_key() -> str|None:
|
|
37
|
+
response = Prompt.ask("Please enter your Gemini API key (use '$--' to access env vars)")
|
|
38
|
+
|
|
39
|
+
if response.startswith("$"):
|
|
40
|
+
response = os.getenv(response[1:])
|
|
41
|
+
if not response:
|
|
42
|
+
return
|
|
43
|
+
print(f"Storing: [green]{response}[/green]")
|
|
44
|
+
return response
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _select_gemini_model():
|
|
48
|
+
response = Prompt.ask(
|
|
49
|
+
"Please select which Gemini model you would like:",
|
|
50
|
+
choices=[
|
|
51
|
+
"gemini-3.5-flash"
|
|
52
|
+
]
|
|
53
|
+
)
|
|
54
|
+
return response
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
import os
|
|
59
|
+
db = JsonDirectoryDatabase(
|
|
60
|
+
".taskai/task_db", user=os.getenv("USER")
|
|
61
|
+
)
|
|
62
|
+
db.connect()
|
|
63
|
+
user_setup_service(db)
|
taskai/views.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# local
|
|
2
|
+
from taskai.json_dir_database import JsonDirectoryDatabase
|
|
3
|
+
from taskai.models import TodoList, TodoItem, Comment
|
|
4
|
+
|
|
5
|
+
# external
|
|
6
|
+
from rich import print
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
import rich
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def view_lists(db: JsonDirectoryDatabase, ids: list[str], show_done=False):
|
|
12
|
+
"""Shows all the lists"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
for id_ in ids:
|
|
16
|
+
|
|
17
|
+
if id_ in db.lists:
|
|
18
|
+
list: TodoList = db.read(id_)
|
|
19
|
+
elif isinstance(id_, str):
|
|
20
|
+
for lid in db.lists:
|
|
21
|
+
list = db.read(lid)
|
|
22
|
+
if list.name.lower() == id_.lower():
|
|
23
|
+
break
|
|
24
|
+
else:
|
|
25
|
+
raise RuntimeError("cannot match list")
|
|
26
|
+
|
|
27
|
+
print(list.id, list.name)
|
|
28
|
+
for item_id in list.item_ids:
|
|
29
|
+
|
|
30
|
+
# silently repair lists with
|
|
31
|
+
if (item_id not in db.items):
|
|
32
|
+
list.item_ids.remove(item_id)
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
item: TodoItem = db.read(item_id)
|
|
36
|
+
if not item.completed:
|
|
37
|
+
print("\t",item.id, item.title)
|
|
38
|
+
elif show_done:
|
|
39
|
+
print(f"\t [strike]{item.id} {item.title}[/strike]")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def view_item(db: JsonDirectoryDatabase, item: str|TodoItem):
|
|
43
|
+
"""Show details for item"""
|
|
44
|
+
|
|
45
|
+
if isinstance(item, (str,int)):
|
|
46
|
+
item: TodoItem = db.read(item)
|
|
47
|
+
console = Console()
|
|
48
|
+
console.print(f"[bold green]Title:[/bold green] {item.title}")
|
|
49
|
+
if item.due_by:
|
|
50
|
+
console.print(f"[bold green]Due By:[/bold green] {item.due_by or ""}")
|
|
51
|
+
|
|
52
|
+
if item.description:
|
|
53
|
+
console.print(f"\n[bold green]Description:[/bold green]\n{item.description}")
|
|
54
|
+
|
|
55
|
+
if item.dependency_ids:
|
|
56
|
+
console.print(f"\n[bold green]Depends on:[/bold green]{''.join([
|
|
57
|
+
f'\n{depend_id} - {db.read(depend_id).title}' for depend_id in item.dependency_ids
|
|
58
|
+
])}")
|
|
59
|
+
|
|
60
|
+
if item.comment_ids:
|
|
61
|
+
console.print("\n[bold green]\nComments:[/bold green]")
|
|
62
|
+
for comment_id in item.comment_ids:
|
|
63
|
+
comment: Comment = db.read(comment_id)
|
|
64
|
+
console.print(f"{comment.created_on.strftime("%Y-%m-%d %H:%M:%S")} - {comment.content}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def view_items(
|
|
68
|
+
db: JsonDirectoryDatabase,
|
|
69
|
+
ids: list[str],
|
|
70
|
+
sort_by: str = "due_by",
|
|
71
|
+
ascending: bool = False
|
|
72
|
+
):
|
|
73
|
+
|
|
74
|
+
items = {}
|
|
75
|
+
for id_ in ids:
|
|
76
|
+
if id_ in db.items:
|
|
77
|
+
item = db.read(id_)
|
|
78
|
+
items[id_] = (item)
|
|
79
|
+
elif id_ in db.lists:
|
|
80
|
+
list: TodoList = db.read(id_)
|
|
81
|
+
for item_id in list.item_ids:
|
|
82
|
+
item = db.read(item_id)
|
|
83
|
+
items[item_id] = item
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# sort
|
|
87
|
+
items = sorted(items.values(), key=lambda item: str(getattr(item, sort_by)), reverse=ascending)
|
|
88
|
+
|
|
89
|
+
# view
|
|
90
|
+
for item in items:
|
|
91
|
+
rich.console.Console().rule(style="bold white")
|
|
92
|
+
view_item(db, item)
|
|
93
|
+
print()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: taskai-cli
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Author-email: Alex Paskal <alexcpaskal@gmail.com>
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: google-genai>=2.8
|
|
7
|
+
Requires-Dist: orjson>=3.11
|
|
8
|
+
Requires-Dist: pydantic>=2.13
|
|
9
|
+
Requires-Dist: rich>=15
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# Task AI
|
|
13
|
+
|
|
14
|
+
Welcome to Task AI - a command-line todo list with some extra ai features.
|
|
15
|
+
|
|
16
|
+
Here's what you can do:
|
|
17
|
+
|
|
18
|
+
- `task show all` --> show all of your lists and item titles, with their respective IDs prepended
|
|
19
|
+
- `task show {id or substring}` --> find the list or item matching your identifier and show it using its respective type's show command
|
|
20
|
+
- `task show list {id or substring}` --> show the list and all of its item titles
|
|
21
|
+
- `task show item {id}` --> show the associated item and all of its specified information
|
|
22
|
+
- `task show items {id1},{id2},...,{idx}` --> show the associated items and all of their specified information
|
|
23
|
+
- `task create item {list id or substring} {title} {**kwargs}` --> Create a new item for the associated list. Can specify kwargs as --optional cli arguments.
|
|
24
|
+
- `task create list {name}` --> create a new list by that name
|
|
25
|
+
- `task delete {id}` --> deletes the list or item associated with that id
|
|
26
|
+
- `task delete item {id}` --> deletes the item associated with that id
|
|
27
|
+
- `task delete list {id}` --> deletes the list associated with that id
|
|
28
|
+
- `task delete completed` --> deletes all items that have been completed
|
|
29
|
+
- `task remove ...` --> aliases directly to 'task delete ...
|
|
30
|
+
- `task update {item id} {**kwargs}` --> updates the associated attributes on the item
|
|
31
|
+
- `task comment {item id} {content}` --> adds a comment to that items comment thread
|
|
32
|
+
- `task ai {prompt}` --> Feeds a prompt directly to an LLM, which constructs and executes a series of task commands according to its interpretation of the prompt
|
|
33
|
+
- `task ai headstart {item id}` --> Feeds the item context to an LLM, which responds with a concise description of the next step to perform. The response is added as a comment in the items comment thread
|
|
34
|
+
- `task nuke` --> deletes all of your task data, letting you have a fresh start
|
|
35
|
+
- `task add ...` -> aliases directly to `task create item ...`
|
|
36
|
+
- `task complete {item id}` --> sets the associated item's .completed attribute to true
|
|
37
|
+
|
|
38
|
+
Run `task show examples` to print a comprehensive set of examples, and grep for the ones that you're interested in!
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Installation
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Configuration
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
taskai/cli.py,sha256=hXC8Ksp9VsGoCji-yihd0e_cl48K374JN1h7cQ1X4BM,10392
|
|
2
|
+
taskai/config.py,sha256=TJWcIUCvACwSrEQNPm4yIzIu3MfGL4DFlj0LjyO_6As,808
|
|
3
|
+
taskai/help_menu.py,sha256=H91sgHc9pkfe21CPWoijBMLJ93UBpJ3ZGW6wVTupW0Y,2053
|
|
4
|
+
taskai/json_dir_database.py,sha256=5jwzytRimoceXYJ8CEhFSWW9EQnbmAXohyPAqhB0NY4,5292
|
|
5
|
+
taskai/models.py,sha256=PKy2FNNU0r5azO0GBw1RoiW2H9-KY1scaGHvYLrbFlw,1509
|
|
6
|
+
taskai/views.py,sha256=gvd0mt8xTyNJYpn6uzg6r9trZNnOgCrfZA_8cIkluh8,2834
|
|
7
|
+
taskai/services/ai.py,sha256=ZQKii_fU-mUWfSLkiC6y7EIk1pTdNS-Av3o5Fd6uBNc,4950
|
|
8
|
+
taskai/services/user_setup.py,sha256=F68b6aITPI3Ez4yGE6yu3fGRJ7k1tIroLux84Uw7-m8,1627
|
|
9
|
+
taskai_cli-0.1.3.dist-info/METADATA,sha256=dR_ILfRS3vnbfxIct8spMavL_p7VXQJfL8poxiPMlb4,2410
|
|
10
|
+
taskai_cli-0.1.3.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
11
|
+
taskai_cli-0.1.3.dist-info/entry_points.txt,sha256=BWVOsXn4B7dsx_uNYjxEmX6YSW_qjCXqwNQVyIqctO4,48
|
|
12
|
+
taskai_cli-0.1.3.dist-info/RECORD,,
|