dostuff 0.1.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.
- dostuff/__init__.py +2 -0
- dostuff/agent/call_agent.py +44 -0
- dostuff/agent/loop.py +161 -0
- dostuff/agent/run_tool.py +52 -0
- dostuff/cli.py +105 -0
- dostuff/cli_tui.py +647 -0
- dostuff/config.py +93 -0
- dostuff/helpers/agent/append_step.py +11 -0
- dostuff/helpers/agent/constants.py +187 -0
- dostuff/helpers/agent/extract_text.py +13 -0
- dostuff/helpers/agent/get_model_token_limit.py +11 -0
- dostuff/helpers/agent/learn_from_session.py +52 -0
- dostuff/helpers/agent/load_identity.py +17 -0
- dostuff/helpers/agent/load_project_instructions.py +36 -0
- dostuff/helpers/agent/manage_context.py +94 -0
- dostuff/helpers/agent/save_memories_and_exit.py +49 -0
- dostuff/helpers/mcp/load_mcp_config.py +32 -0
- dostuff/helpers/mcp/mcp_oauth.py +229 -0
- dostuff/helpers/memory/extract_episodic_memory.py +97 -0
- dostuff/helpers/memory/extract_semantic_memories.py +66 -0
- dostuff/helpers/memory/format_transcript.py +92 -0
- dostuff/helpers/memory/resolve_memory_operation.py +49 -0
- dostuff/helpers/skills/discover_skills.py +35 -0
- dostuff/helpers/tools/generate_tool_schema.py +62 -0
- dostuff/helpers/tools/resolve_safe_path.py +15 -0
- dostuff/helpers/ui/__init__.py +0 -0
- dostuff/helpers/ui/emit.py +24 -0
- dostuff/lib/exceptions.py +11 -0
- dostuff/lib/mcp/mcp_client.py +186 -0
- dostuff/lib/mcp/mcp_client_registration_store.py +48 -0
- dostuff/lib/mcp/mcp_tool_registry_store.py +78 -0
- dostuff/lib/memory/episodic_memory_store.py +112 -0
- dostuff/lib/memory/semantic_memory_store.py +51 -0
- dostuff/lib/memory/session_store.py +198 -0
- dostuff/lib/memory/types.py +36 -0
- dostuff/lib/model.py +22 -0
- dostuff/lib/tracing.py +78 -0
- dostuff/memory/__init__.py +6 -0
- dostuff/skills.py +3 -0
- dostuff/tools/__init__.py +1 -0
- dostuff/tools/bash/bash_command.py +63 -0
- dostuff/tools/definitions.py +35 -0
- dostuff/tools/delegate_to_subagent.py +74 -0
- dostuff/tools/files/delete_file.py +33 -0
- dostuff/tools/files/list_files.py +33 -0
- dostuff/tools/files/read_file.py +21 -0
- dostuff/tools/files/write_file.py +31 -0
- dostuff/tools/get_current_datetime.py +17 -0
- dostuff/tools/mcp/call_mcp_tool.py +83 -0
- dostuff/tools/mcp/get_mcp_tool_details.py +19 -0
- dostuff/tools/mcp/search_mcp_tools.py +55 -0
- dostuff-0.1.0.dist-info/METADATA +458 -0
- dostuff-0.1.0.dist-info/RECORD +57 -0
- dostuff-0.1.0.dist-info/WHEEL +5 -0
- dostuff-0.1.0.dist-info/entry_points.txt +2 -0
- dostuff-0.1.0.dist-info/licenses/LICENSE +21 -0
- dostuff-0.1.0.dist-info/top_level.txt +1 -0
dostuff/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from dostuff.lib.tracing import traced
|
|
2
|
+
from opentelemetry import trace as otel_trace
|
|
3
|
+
from dostuff.tools.definitions import TOOL_SCHEMAS
|
|
4
|
+
from litellm import acompletion
|
|
5
|
+
from dostuff.lib.model import MODEL
|
|
6
|
+
|
|
7
|
+
@traced("model_call")
|
|
8
|
+
async def call_agent(steps_history: list, system_instruction: str, tool_names: list[str] | None = None):
|
|
9
|
+
if tool_names is None:
|
|
10
|
+
tool_names = list(TOOL_SCHEMAS.keys())
|
|
11
|
+
|
|
12
|
+
active_schemas = [
|
|
13
|
+
TOOL_SCHEMAS[name]
|
|
14
|
+
for name in tool_names
|
|
15
|
+
if name in TOOL_SCHEMAS
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
messages = [{"role": "system", "content": system_instruction}] + steps_history
|
|
19
|
+
|
|
20
|
+
async def _make_request():
|
|
21
|
+
return await acompletion(
|
|
22
|
+
model=MODEL,
|
|
23
|
+
messages=messages,
|
|
24
|
+
tools=active_schemas,
|
|
25
|
+
drop_invalid_params=True
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
interaction = await _make_request()
|
|
30
|
+
except Exception as e:
|
|
31
|
+
if "malformed_tool_call" not in str(e):
|
|
32
|
+
raise
|
|
33
|
+
from dostuff.helpers.ui.emit import emit
|
|
34
|
+
emit("Model returned malformed JSON — retrying once...", msg_type="system")
|
|
35
|
+
interaction = await _make_request()
|
|
36
|
+
|
|
37
|
+
usage = getattr(interaction, "usage", None)
|
|
38
|
+
if usage is not None:
|
|
39
|
+
span = otel_trace.get_current_span()
|
|
40
|
+
span.set_attribute("usage.total_tokens", getattr(usage, "total_tokens", 0))
|
|
41
|
+
span.set_attribute("usage.input_tokens", getattr(usage, "prompt_tokens", 0))
|
|
42
|
+
span.set_attribute("usage.output_tokens", getattr(usage, "completion_tokens", 0))
|
|
43
|
+
|
|
44
|
+
return interaction
|
dostuff/agent/loop.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from dostuff.helpers.agent.get_model_token_limit import get_model_token_limit
|
|
2
|
+
from dostuff.lib.exceptions import ConfirmationRequired
|
|
3
|
+
from dostuff.lib.mcp.mcp_client import MCPClient
|
|
4
|
+
from dostuff.lib.memory.session_store import SessionStore
|
|
5
|
+
from dostuff.lib.tracing import tracer
|
|
6
|
+
from dostuff.helpers.agent.constants import MAX_ITERATIONS
|
|
7
|
+
from dostuff.helpers.agent.manage_context import compact_context
|
|
8
|
+
from typing import Literal, Optional, Any
|
|
9
|
+
from dostuff.agent.call_agent import call_agent
|
|
10
|
+
from opentelemetry.trace import Status, StatusCode
|
|
11
|
+
from dostuff.helpers.agent.append_step import append_step
|
|
12
|
+
import asyncio
|
|
13
|
+
from dostuff.agent.run_tool import run_tool
|
|
14
|
+
import json
|
|
15
|
+
|
|
16
|
+
async def loop(session_id: str, turn_id: str, user_text: str, dynamic_system_instruction: str, mcp_client: MCPClient, working_history: list, turn_type: Literal['interactive_loop', 'learning_loop', 'subagent_loop'], current_session_history: list = [], steps_history: list = [], store: SessionStore | None = None, adapter: Optional[Any] = None) -> str:
|
|
17
|
+
|
|
18
|
+
async def _emit(et, data):
|
|
19
|
+
if adapter and hasattr(adapter, "emit"):
|
|
20
|
+
try:
|
|
21
|
+
adapter.emit(et, data)
|
|
22
|
+
except Exception:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
iteration = 0
|
|
26
|
+
last_input_tokens = 0
|
|
27
|
+
token_limit = get_model_token_limit()
|
|
28
|
+
context_token_threshold = int(token_limit * 0.5)
|
|
29
|
+
keep_recent_token_budget = int(context_token_threshold * 0.15)
|
|
30
|
+
compaction_notes = ""
|
|
31
|
+
|
|
32
|
+
with tracer.start_as_current_span("turn") as turn_span:
|
|
33
|
+
turn_span.set_attribute("session_id", session_id)
|
|
34
|
+
turn_span.set_attribute("turn_type", turn_type)
|
|
35
|
+
turn_span.set_attribute("turn_id", turn_id)
|
|
36
|
+
turn_span.set_attribute("user_input", user_text)
|
|
37
|
+
while iteration < MAX_ITERATIONS:
|
|
38
|
+
iteration += 1
|
|
39
|
+
with tracer.start_as_current_span("iteration") as iter_span:
|
|
40
|
+
iter_span.set_attribute("iteration_number", iteration)
|
|
41
|
+
|
|
42
|
+
# context compaction
|
|
43
|
+
if last_input_tokens > context_token_threshold:
|
|
44
|
+
working_history, new_summary = await compact_context(working_history, keep_recent_token_budget)
|
|
45
|
+
compaction_notes = f"{compaction_notes}\n{new_summary}".strip()
|
|
46
|
+
with tracer.start_as_current_span("context_compaction") as compaction_span:
|
|
47
|
+
compaction_span.set_attribute("steps_after", len(working_history))
|
|
48
|
+
|
|
49
|
+
# agent call
|
|
50
|
+
interaction = await call_agent(steps_history=working_history, system_instruction=dynamic_system_instruction + (f"\n\n[Summary of earlier conversation]: {compaction_notes}" if compaction_notes else ""))
|
|
51
|
+
|
|
52
|
+
# token tracking
|
|
53
|
+
usage = getattr(interaction, "usage", None)
|
|
54
|
+
if usage:
|
|
55
|
+
last_input_tokens = getattr(usage, "total_tokens", last_input_tokens)
|
|
56
|
+
|
|
57
|
+
# CustomStreamWrapper does not expose ``choices`` in its static type,
|
|
58
|
+
# although the completed interaction provides it at runtime.
|
|
59
|
+
choices = getattr(interaction, "choices", None)
|
|
60
|
+
if not choices:
|
|
61
|
+
iter_span.set_status(Status(StatusCode.OK))
|
|
62
|
+
continue
|
|
63
|
+
message = choices[0].message
|
|
64
|
+
tool_calls = getattr(message, "tool_calls", None)
|
|
65
|
+
content = getattr(message, "content", None)
|
|
66
|
+
|
|
67
|
+
if content and not tool_calls:
|
|
68
|
+
model_step = {
|
|
69
|
+
"role": "assistant",
|
|
70
|
+
"content": content,
|
|
71
|
+
}
|
|
72
|
+
await append_step(model_step, steps_history, working_history, current_session_history, session_id, store, turn_type)
|
|
73
|
+
|
|
74
|
+
turn_span.set_attribute("outcome", "success")
|
|
75
|
+
turn_span.set_status(Status(StatusCode.OK))
|
|
76
|
+
iter_span.set_status(Status(StatusCode.OK))
|
|
77
|
+
# print(f"\n\nAgent: {content}")
|
|
78
|
+
if usage:
|
|
79
|
+
usage_dict = {
|
|
80
|
+
"prompt_tokens": getattr(usage, "prompt_tokens", 0) or 0,
|
|
81
|
+
"completion_tokens": getattr(usage, "completion_tokens", 0) or 0,
|
|
82
|
+
"total_tokens": getattr(usage, "total_tokens", 0) or 0,
|
|
83
|
+
}
|
|
84
|
+
await _emit("usage", usage_dict)
|
|
85
|
+
return content
|
|
86
|
+
|
|
87
|
+
if not tool_calls:
|
|
88
|
+
iter_span.set_status(Status(StatusCode.OK))
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
function_calls = []
|
|
92
|
+
for tool_call in tool_calls:
|
|
93
|
+
if tool_call.type == "function":
|
|
94
|
+
fn_name = tool_call.function.name
|
|
95
|
+
fn_args = json.loads(tool_call.function.arguments) if isinstance(tool_call.function.arguments, str) else tool_call.function.arguments
|
|
96
|
+
fn_id = tool_call.id
|
|
97
|
+
function_calls.append((fn_name, fn_args, fn_id))
|
|
98
|
+
|
|
99
|
+
assistant_tool_step = {
|
|
100
|
+
"role": "assistant",
|
|
101
|
+
"content": content,
|
|
102
|
+
"tool_calls": [t.model_dump() for t in tool_calls]
|
|
103
|
+
}
|
|
104
|
+
await append_step(assistant_tool_step, steps_history, working_history, current_session_history, session_id, store, turn_type)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
for fn_name, fn_args, _ in function_calls:
|
|
108
|
+
await _emit("tool_call", f"{fn_name}({fn_args})")
|
|
109
|
+
|
|
110
|
+
results = await asyncio.gather(
|
|
111
|
+
*(asyncio.wait_for(
|
|
112
|
+
run_tool(fn_name=fn_name, fn_args=dict(fn_args), mcp_client=mcp_client, session_id=session_id, turn_id=turn_id),
|
|
113
|
+
timeout=120.0 # 2 min per tool call; prevents hung MCP servers from blocking the turn
|
|
114
|
+
) for fn_name, fn_args, _ in function_calls),
|
|
115
|
+
return_exceptions=True,
|
|
116
|
+
)
|
|
117
|
+
final_results = []
|
|
118
|
+
for (fn_name, fn_args, fn_id), result in zip(function_calls, results):
|
|
119
|
+
if isinstance(result, ConfirmationRequired):
|
|
120
|
+
if adapter and hasattr(adapter, "ask"):
|
|
121
|
+
confirm = await adapter.ask(result.message, result.resume_args)
|
|
122
|
+
else:
|
|
123
|
+
if adapter and hasattr(adapter, "emit"):
|
|
124
|
+
adapter.emit("confirm", result.message)
|
|
125
|
+
else:
|
|
126
|
+
print(f"\nConfirmation needed: {result.message}")
|
|
127
|
+
confirm = await asyncio.to_thread(input, "Allow this? [y/n]: ")
|
|
128
|
+
confirm = confirm.strip().lower() == "y"
|
|
129
|
+
|
|
130
|
+
if confirm:
|
|
131
|
+
resumed_args = {**fn_args, **result.resume_args}
|
|
132
|
+
result = await run_tool(fn_name=fn_name, fn_args=resumed_args, mcp_client=mcp_client, session_id=session_id, turn_id=turn_id)
|
|
133
|
+
else:
|
|
134
|
+
result = "Error: User declined to allow this action."
|
|
135
|
+
|
|
136
|
+
elif isinstance(result, Exception):
|
|
137
|
+
result = f"Error: {result}"
|
|
138
|
+
|
|
139
|
+
final_results.append((fn_name, fn_id, result))
|
|
140
|
+
await _emit("tool_result", f"{fn_name}: {str(result)[:200]}")
|
|
141
|
+
|
|
142
|
+
for fn_name, fn_id, result in final_results:
|
|
143
|
+
result_step = {
|
|
144
|
+
"role": "tool",
|
|
145
|
+
"name": fn_name,
|
|
146
|
+
"tool_call_id": fn_id,
|
|
147
|
+
"content": str(result)
|
|
148
|
+
}
|
|
149
|
+
await append_step(result_step, steps_history, working_history, current_session_history, session_id, store, turn_type)
|
|
150
|
+
iter_span.set_status(Status(StatusCode.OK))
|
|
151
|
+
|
|
152
|
+
else:
|
|
153
|
+
msg = f"Reached maximum iterations ({MAX_ITERATIONS}) without receiving a model output. Ending the agent loop."
|
|
154
|
+
if adapter and hasattr(adapter, "emit"):
|
|
155
|
+
adapter.emit("system", msg)
|
|
156
|
+
else:
|
|
157
|
+
print(msg)
|
|
158
|
+
turn_span.set_attribute("outcome", "max_iterations_exceeded")
|
|
159
|
+
turn_span.set_status(Status(StatusCode.ERROR, "max_iterations_exceeded"))
|
|
160
|
+
return msg
|
|
161
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from dostuff.tools.definitions import TOOL_MAP
|
|
2
|
+
from typing import Any
|
|
3
|
+
from dostuff.lib.tracing import traced
|
|
4
|
+
import inspect
|
|
5
|
+
import asyncio
|
|
6
|
+
from dostuff.tools.mcp.call_mcp_tool import _impl_call_mcp_tool
|
|
7
|
+
from dostuff.tools.mcp.get_mcp_tool_details import _impl_get_mcp_tool_details
|
|
8
|
+
from dostuff.tools.mcp.search_mcp_tools import _impl_search_mcp_tools
|
|
9
|
+
from dostuff.lib.mcp.mcp_client import MCPClient
|
|
10
|
+
from dostuff.tools.delegate_to_subagent import _impl_delegate_to_subagent
|
|
11
|
+
|
|
12
|
+
MCP_META_TOOLS = {
|
|
13
|
+
"search_mcp_tools": _impl_search_mcp_tools,
|
|
14
|
+
"get_mcp_tool_details": _impl_get_mcp_tool_details,
|
|
15
|
+
"call_mcp_tool": _impl_call_mcp_tool,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
SUBAGENT_META_TOOLS = {
|
|
19
|
+
"delegate_to_subagent": _impl_delegate_to_subagent
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@traced("tool_call")
|
|
23
|
+
async def run_tool(fn_name: str | None, fn_args: dict[str, Any], mcp_client: MCPClient | None = None, session_id: str | None = None, turn_id: str | None = None) -> Any:
|
|
24
|
+
if not isinstance(fn_name, str):
|
|
25
|
+
raise ValueError("Tool name must be a string.")
|
|
26
|
+
|
|
27
|
+
if fn_name in SUBAGENT_META_TOOLS:
|
|
28
|
+
if not session_id or not turn_id or not mcp_client:
|
|
29
|
+
raise ValueError(f"Cannot spawn subagent without all required arguments.")
|
|
30
|
+
fn = SUBAGENT_META_TOOLS[fn_name]
|
|
31
|
+
if inspect.iscoroutinefunction(fn):
|
|
32
|
+
return await fn(**fn_args, mcp_client=mcp_client, session_id = session_id, turn_id=turn_id)
|
|
33
|
+
else:
|
|
34
|
+
return await asyncio.to_thread(fn, **fn_args, mcp_client=mcp_client, session_id = session_id, turn_id=turn_id)
|
|
35
|
+
|
|
36
|
+
if fn_name in MCP_META_TOOLS:
|
|
37
|
+
if not mcp_client:
|
|
38
|
+
raise ValueError(f"Cannot execute MCP tool '{fn_name}' without an active mcp_client.")
|
|
39
|
+
fn = MCP_META_TOOLS[fn_name]
|
|
40
|
+
if inspect.iscoroutinefunction(fn):
|
|
41
|
+
return await fn(**fn_args, mcp_client=mcp_client)
|
|
42
|
+
else:
|
|
43
|
+
return await asyncio.to_thread(fn, **fn_args, mcp_client=mcp_client)
|
|
44
|
+
|
|
45
|
+
if fn_name not in TOOL_MAP:
|
|
46
|
+
raise KeyError(f"Tool '{fn_name}' not found.")
|
|
47
|
+
|
|
48
|
+
fn = TOOL_MAP[fn_name]
|
|
49
|
+
if inspect.iscoroutinefunction(fn):
|
|
50
|
+
return await fn(**fn_args)
|
|
51
|
+
else:
|
|
52
|
+
return await asyncio.to_thread(fn, **fn_args)
|
dostuff/cli.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
app = typer.Typer()
|
|
5
|
+
|
|
6
|
+
@app.callback(invoke_without_command=True)
|
|
7
|
+
def main(
|
|
8
|
+
ctx: typer.Context = typer.Option(None),
|
|
9
|
+
session: str = typer.Option(None, "--session", help="Session ID (default: new UUID)"),
|
|
10
|
+
user: str = typer.Option(None, "--user", help="User ID (default: from ~/.dostuff/user_id)"),
|
|
11
|
+
):
|
|
12
|
+
if ctx.invoked_subcommand is not None:
|
|
13
|
+
return # subcommand (init/config/doctor/session_list/resume) — don't start agent
|
|
14
|
+
from dostuff.config import Config
|
|
15
|
+
|
|
16
|
+
config = Config()
|
|
17
|
+
user_id = user or config.get_user_id()
|
|
18
|
+
session_id = session or (sys.argv[0] + "_" + __import__("uuid").uuid4().hex[:8])
|
|
19
|
+
if session is None and session_id.startswith(sys.argv[0]):
|
|
20
|
+
session_id = __import__("uuid").uuid4().hex[:16]
|
|
21
|
+
typer.echo(f"USER: {user_id} SESSION: {session_id}")
|
|
22
|
+
typer.echo(f"DATA_DIR: {config.get_data_dir()}")
|
|
23
|
+
typer.echo("Starting TUI...")
|
|
24
|
+
typer.echo("STARTING DoStuff...")
|
|
25
|
+
# Real TUI — default experience (not skeleton)
|
|
26
|
+
from dostuff.cli_tui import DostuffTUI
|
|
27
|
+
tui_app = DostuffTUI(session_id=session_id, user_id=user_id)
|
|
28
|
+
tui_app.run()
|
|
29
|
+
|
|
30
|
+
@app.command()
|
|
31
|
+
def init():
|
|
32
|
+
import pathlib
|
|
33
|
+
cwd = pathlib.Path.cwd()
|
|
34
|
+
proj_dir = cwd / ".dostuff"
|
|
35
|
+
proj_dir.mkdir(exist_ok=True)
|
|
36
|
+
|
|
37
|
+
# Create project config.yaml with template
|
|
38
|
+
config_template = """# Project-specific config (overrides ~/.dostuff/config.yaml)
|
|
39
|
+
# Uncomment any section below to override the global config.
|
|
40
|
+
# Full reference: https://github.com/<you>/dostuff/blob/main/config.example.yaml
|
|
41
|
+
|
|
42
|
+
# data:
|
|
43
|
+
# mode: "project" # use project data dir instead of global
|
|
44
|
+
|
|
45
|
+
# mcp:
|
|
46
|
+
# config_path: "mcp_config.json" # project-specific MCP servers
|
|
47
|
+
|
|
48
|
+
# model:
|
|
49
|
+
# name: "openai/gpt-4o-mini" # litellm format
|
|
50
|
+
# api_key_env: "OPENAI_API_KEY" # env var with key (never put key in YAML)
|
|
51
|
+
|
|
52
|
+
# tracing:
|
|
53
|
+
# enabled: false
|
|
54
|
+
# exporter: "otlp"
|
|
55
|
+
"""
|
|
56
|
+
(proj_dir / "config.yaml").write_text(config_template)
|
|
57
|
+
|
|
58
|
+
# Create directories
|
|
59
|
+
(proj_dir / "skills").mkdir(exist_ok=True)
|
|
60
|
+
|
|
61
|
+
typer.echo(f"Initialized project at {cwd}")
|
|
62
|
+
|
|
63
|
+
@app.command()
|
|
64
|
+
def config():
|
|
65
|
+
from dostuff.config import Config
|
|
66
|
+
|
|
67
|
+
cfg = Config()
|
|
68
|
+
typer.echo(f"Global config: {cfg.global_path}")
|
|
69
|
+
# typer.echo(f"Data mode: {cfg.raw.get('data',{}).get('mode','global')}")
|
|
70
|
+
typer.echo(f"Data dir: {cfg.get_data_dir()}")
|
|
71
|
+
typer.echo(f"User ID: {cfg.get_user_id()}")
|
|
72
|
+
|
|
73
|
+
@app.command()
|
|
74
|
+
def doctor():
|
|
75
|
+
typer.echo("Health check: dostuff package installed.")
|
|
76
|
+
from dostuff.config import Config
|
|
77
|
+
|
|
78
|
+
cfg = Config()
|
|
79
|
+
typer.echo(f" Global config: {cfg.global_path} (exists={cfg.global_path.exists()})")
|
|
80
|
+
cwd = __import__('pathlib').Path.cwd()
|
|
81
|
+
proj = cwd / ".dostuff" / "config.yaml"
|
|
82
|
+
typer.echo(f" Project config: {proj} (exists={proj.exists()})")
|
|
83
|
+
typer.echo(f" User ID file: {cfg.user_id_path} (exists={cfg.user_id_path.exists()})")
|
|
84
|
+
typer.echo(f" Data dir: {cfg.get_data_dir()} (exists={cfg.get_data_dir().exists()})")
|
|
85
|
+
|
|
86
|
+
@app.command()
|
|
87
|
+
def session_list():
|
|
88
|
+
import asyncio
|
|
89
|
+
from dostuff.memory import SQLiteSessionStore
|
|
90
|
+
from dostuff.config import Config
|
|
91
|
+
|
|
92
|
+
async def _run():
|
|
93
|
+
cfg = Config()
|
|
94
|
+
store = SQLiteSessionStore(db_path=str(cfg.get_data_dir() / "sessions.db"))
|
|
95
|
+
rows = await store.list()
|
|
96
|
+
if not rows:
|
|
97
|
+
typer.echo("No past sessions.")
|
|
98
|
+
return
|
|
99
|
+
typer.echo(f"{'SESSION':<36} | {'WORKING_DIR':<60}")
|
|
100
|
+
typer.echo("-" * 100)
|
|
101
|
+
for sid, wd in rows:
|
|
102
|
+
typer.echo(f"{sid:<36} | {str(wd):<60}")
|
|
103
|
+
asyncio.run(_run())
|
|
104
|
+
|
|
105
|
+
|