agencode 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.
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from agencli.agents.factory import AgentSpec, SubAgentSpec
4
+
5
+
6
+ PERFORMANCE_PROMPT_SUFFIX = (
7
+ " Optimize for fewer, larger steps. Inspect enough context once, avoid redundant delegation or repeated "
8
+ "tool chatter, and prefer grouped actions when they are safe."
9
+ )
10
+
11
+
12
+ def build_supervisor_agent(default_model: str) -> AgentSpec:
13
+ return AgentSpec(
14
+ name="supervisor-agent",
15
+ model=default_model,
16
+ description="Orchestrates specialized subagents for multi-step workspace tasks.",
17
+ system_prompt=(
18
+ "You are an orchestration specialist. Break complex work into focused subagent tasks, "
19
+ "delegate proactively when specialization or parallelism helps, and synthesize the results "
20
+ "into one concise final answer. Use the coding specialist for implementation and verification, "
21
+ "the shell specialist for command-heavy inspection or automation, and the filesystem specialist "
22
+ "for focused file navigation or content retrieval. Prefer the task tool for independent workstreams. "
23
+ "You also have structured management tools for saved subagents and skills. When the user wants to "
24
+ "create or edit a subagent, first ask the minimum clarifying questions needed, then inspect saved "
25
+ "subagents and currently attachable skills before proposing changes. If relevant skills are missing, "
26
+ "search the marketplace skills tool automatically, suggest the strongest matches, and ask for user "
27
+ "confirmation before installing or activating anything. Use the structured subagent tools to save or "
28
+ "update subagents instead of relying on raw shell edits whenever possible. After changes, summarize "
29
+ "the final subagent name, refined purpose, attached skills, and activation state."
30
+ "When a task is mostly filesystem organization, push the subagent toward one inspected plan with "
31
+ "batched create/move steps instead of repetitive one-file-at-a-time operations."
32
+ + PERFORMANCE_PROMPT_SUFFIX
33
+ ),
34
+ subagents=[
35
+ SubAgentSpec(
36
+ name="coding-specialist",
37
+ description="Use for code changes, debugging, test execution, and implementation work.",
38
+ system_prompt=(
39
+ "You are a coding specialist. Inspect relevant files, make precise changes, "
40
+ "verify results, and return a concise implementation summary with any risks. Prefer one "
41
+ "cohesive patch and one focused verification pass over fragmented micro-steps."
42
+ + PERFORMANCE_PROMPT_SUFFIX
43
+ ),
44
+ ),
45
+ SubAgentSpec(
46
+ name="shell-specialist",
47
+ description="Use for command-driven inspection, environment checks, and shell automation.",
48
+ system_prompt=(
49
+ "You are a shell specialist. Prefer safe read-first commands, summarize outputs clearly, "
50
+ "and avoid destructive actions unless they are explicitly required. When safe, use one strong "
51
+ "command to handle bulk work instead of many repetitive commands."
52
+ + PERFORMANCE_PROMPT_SUFFIX
53
+ ),
54
+ ),
55
+ SubAgentSpec(
56
+ name="filesystem-specialist",
57
+ description="Use for file discovery, content lookup, and precise workspace navigation.",
58
+ system_prompt=(
59
+ "You are a filesystem specialist. Traverse carefully, read thoroughly, and return only the "
60
+ "most relevant file paths and excerpts needed by the parent agent. For organization tasks, "
61
+ "inspect first, infer the full layout, and prefer batched directory creation and grouped "
62
+ "file moves over repetitive one-by-one steps whenever the available tools allow it."
63
+ + PERFORMANCE_PROMPT_SUFFIX
64
+ ),
65
+ ),
66
+ ],
67
+ )
agencli/cli.py ADDED
@@ -0,0 +1,561 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import replace
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from agencli.agents.factory import build_agent
11
+ from agencli.agents.prebuilt.catalog import get_prebuilt_agents
12
+ from agencli.agents.registry import AgentRegistry
13
+ from agencli.agents.runtime import (
14
+ DEFAULT_HISTORY_REPLAY_LIMIT,
15
+ build_prompt_payload,
16
+ extract_text_response,
17
+ parse_stream_chunk,
18
+ )
19
+ from agencli.core.config import (
20
+ AgenCLIConfig,
21
+ ensure_config,
22
+ load_config,
23
+ save_config,
24
+ set_openai_compatible_provider,
25
+ )
26
+ from agencli.core.logger import configure_logging
27
+ from agencli.core.session import (
28
+ append_chat_turn,
29
+ build_thread_config,
30
+ describe_langgraph_checkpointer,
31
+ ensure_session_db,
32
+ get_thread_id,
33
+ has_thread_checkpoint,
34
+ list_chat_threads,
35
+ load_chat_history,
36
+ resolve_thread_id,
37
+ )
38
+ from agencli.mcp.config import (
39
+ MCPServerConfig,
40
+ bootstrap_default_mcp_servers,
41
+ load_mcp_servers,
42
+ upsert_mcp_server,
43
+ )
44
+ from agencli.providers.model import describe_api_key_source
45
+ from agencli.skills.manager import install_skill, list_installed_skills
46
+ from agencli.tui.app import AgenCLIApp
47
+
48
+ app = typer.Typer(help="AgenCLI: a multi-agent terminal workspace.")
49
+ console = Console()
50
+
51
+
52
+ def _get_registry(config: AgenCLIConfig) -> AgentRegistry:
53
+ return AgentRegistry(config.agents_dir)
54
+
55
+
56
+ def _resolve_agent_spec(config: AgenCLIConfig, name: str):
57
+ prebuilt = get_prebuilt_agents(config.default_model)
58
+ if name in prebuilt:
59
+ return prebuilt[name]
60
+ return _get_registry(config).load(name)
61
+
62
+
63
+ def _resolve_project_config_path(config_path: Path | None, workspace: Path) -> Path:
64
+ if config_path is not None:
65
+ return config_path
66
+ return ensure_config(root=workspace / ".agencode")
67
+
68
+
69
+ @app.callback()
70
+ def main() -> None:
71
+ """Initialize CLI state shared by all commands."""
72
+ configure_logging()
73
+
74
+
75
+ @app.command()
76
+ def tui(
77
+ config_path: Path | None = typer.Option(None, "--config", help="Path to config.toml"),
78
+ workspace: Path | None = typer.Option(
79
+ None,
80
+ "--workspace",
81
+ help="Workspace directory to open. Defaults to the current directory.",
82
+ ),
83
+ inline: bool = typer.Option(
84
+ True,
85
+ "--inline/--fullscreen",
86
+ help="Run inline in the terminal scrollback instead of using Textual's fullscreen alternate screen.",
87
+ ),
88
+ ) -> None:
89
+ """Launch the Textual UI."""
90
+ launch_workspace = (workspace or Path.cwd()).expanduser().resolve()
91
+ resolved_config_path = _resolve_project_config_path(config_path, launch_workspace)
92
+ config = load_config(resolved_config_path)
93
+ config = replace(config, workspace_dir=str(launch_workspace))
94
+ save_config(config, resolved_config_path)
95
+ AgenCLIApp(config=config, config_path=resolved_config_path).run(inline=inline)
96
+
97
+
98
+ @app.command("init-config")
99
+ def init_config(
100
+ force: bool = typer.Option(False, "--force", help="Overwrite an existing config file."),
101
+ base_url: str | None = typer.Option(None, "--base-url", help="OpenAI-compatible API base URL."),
102
+ model: str | None = typer.Option(None, "--model", help="Default OpenAI-compatible model name."),
103
+ model_kind: str | None = typer.Option(None, "--model-kind", help="Model kind, for example chat or responses."),
104
+ api_key: str | None = typer.Option(None, "--api-key", help="OpenAI-compatible API key to store in keyring."),
105
+ ) -> None:
106
+ """Create the default AgenCLI config in the user directory."""
107
+ path = ensure_config(overwrite=force)
108
+ if any(value is not None for value in (base_url, model, model_kind, api_key)):
109
+ set_openai_compatible_provider(
110
+ config_path=path,
111
+ base_url=base_url,
112
+ model=model,
113
+ model_kind=model_kind,
114
+ api_key=api_key,
115
+ )
116
+ console.print(f"Config ready at {path}")
117
+
118
+
119
+ @app.command()
120
+ def doctor(config_path: Path | None = typer.Option(None, "--config", help="Path to config.toml")) -> None:
121
+ """Print basic environment details for debugging."""
122
+ config: AgenCLIConfig = load_config(config_path)
123
+ console.print(f"workspace: {config.workspace_dir}")
124
+ console.print(f"sessions: {config.sessions_dir}")
125
+ console.print(f"agents: {config.agents_dir}")
126
+ console.print(f"default model: {config.default_model}")
127
+ console.print(f"openai-compatible base_url: {config.openai_compatible.base_url or '-'}")
128
+ console.print(f"openai-compatible model: {config.openai_compatible.model or '-'}")
129
+ console.print(f"openai-compatible model_kind: {config.openai_compatible.model_kind}")
130
+ console.print(f"openai-compatible api key: {describe_api_key_source(config)}")
131
+ console.print(f"langgraph checkpointer: {describe_langgraph_checkpointer(config.sessions_dir)}")
132
+ console.print(f"session db: {ensure_session_db(config.sessions_dir)}")
133
+
134
+
135
+ @app.command("config-openai")
136
+ def config_openai(
137
+ config_path: Path | None = typer.Option(None, "--config", help="Path to config.toml"),
138
+ provider_name: str = typer.Option("openai-compatible", "--provider-name", help="Logical provider label."),
139
+ base_url: str = typer.Option(..., "--base-url", help="OpenAI-compatible API base URL."),
140
+ model: str = typer.Option(..., "--model", help="Model name exposed by the provider."),
141
+ model_kind: str = typer.Option("chat", "--model-kind", help="Model kind, for example chat or responses."),
142
+ api_key: str | None = typer.Option(None, "--api-key", help="API key to store in keyring."),
143
+ api_key_env: str = typer.Option("OPENAI_API_KEY", "--api-key-env", help="Env var name used by this provider."),
144
+ set_default: bool = typer.Option(True, "--set-default/--no-set-default", help="Update default_model to this model."),
145
+ ) -> None:
146
+ """Configure an OpenAI-compatible provider."""
147
+ config = set_openai_compatible_provider(
148
+ config_path=config_path,
149
+ provider_name=provider_name,
150
+ base_url=base_url,
151
+ model=model,
152
+ model_kind=model_kind,
153
+ api_key=api_key,
154
+ api_key_env=api_key_env,
155
+ set_as_default=set_default,
156
+ )
157
+ console.print(f"Saved OpenAI-compatible provider to {config_path or ensure_config()}")
158
+ console.print(f"default model: {config.default_model}")
159
+
160
+
161
+ @app.command("config-show")
162
+ def config_show(config_path: Path | None = typer.Option(None, "--config", help="Path to config.toml")) -> None:
163
+ """Show the current resolved configuration."""
164
+ config = load_config(config_path)
165
+ table = Table(title="AgenCLI Config")
166
+ table.add_column("Key")
167
+ table.add_column("Value")
168
+ table.add_row("workspace_dir", config.workspace_dir)
169
+ table.add_row("sessions_dir", config.sessions_dir)
170
+ table.add_row("agents_dir", config.agents_dir)
171
+ table.add_row("skills_dir", config.skills_dir)
172
+ table.add_row("default_model", config.default_model)
173
+ table.add_row("openai.provider_name", config.openai_compatible.provider_name)
174
+ table.add_row("openai.base_url", config.openai_compatible.base_url or "-")
175
+ table.add_row("openai.model", config.openai_compatible.model or "-")
176
+ table.add_row("openai.model_kind", config.openai_compatible.model_kind)
177
+ table.add_row("openai.api_key_env", config.openai_compatible.api_key_env)
178
+ table.add_row("openai.api_key", describe_api_key_source(config))
179
+ console.print(table)
180
+
181
+
182
+ @app.command("mcp-list")
183
+ def mcp_list(config_path: Path | None = typer.Option(None, "--config", help="Path to config.toml")) -> None:
184
+ """List configured MCP servers."""
185
+ config = load_config(config_path)
186
+ servers = load_mcp_servers(config.mcp_config_path)
187
+ table = Table(title="MCP Servers")
188
+ table.add_column("Name")
189
+ table.add_column("Transport")
190
+ table.add_column("Command / URL")
191
+ if not servers:
192
+ table.add_row("-", "-", "No MCP servers configured")
193
+ else:
194
+ for name, server in sorted(servers.items()):
195
+ target = server.url or " ".join([server.command or "", *server.args]).strip()
196
+ table.add_row(name, server.transport, target or "-")
197
+ console.print(table)
198
+
199
+
200
+ @app.command("mcp-add-stdio")
201
+ def mcp_add_stdio(
202
+ name: str = typer.Argument(..., help="MCP server name."),
203
+ command: str = typer.Option(..., "--command", help="Command used to start the server."),
204
+ args: list[str] | None = typer.Option(None, "--arg", help="Repeatable command arguments."),
205
+ config_path: Path | None = typer.Option(None, "--config", help="Path to config.toml"),
206
+ ) -> None:
207
+ """Add or update a stdio MCP server."""
208
+ config = load_config(config_path)
209
+ path = upsert_mcp_server(
210
+ config.mcp_config_path,
211
+ name,
212
+ MCPServerConfig(transport="stdio", command=command, args=args or []),
213
+ )
214
+ console.print(f"Saved MCP server `{name}` to {path}")
215
+
216
+
217
+ @app.command("mcp-add-http")
218
+ def mcp_add_http(
219
+ name: str = typer.Argument(..., help="MCP server name."),
220
+ url: str = typer.Option(..., "--url", help="HTTP MCP endpoint URL."),
221
+ config_path: Path | None = typer.Option(None, "--config", help="Path to config.toml"),
222
+ ) -> None:
223
+ """Add or update an HTTP MCP server."""
224
+ config = load_config(config_path)
225
+ path = upsert_mcp_server(
226
+ config.mcp_config_path,
227
+ name,
228
+ MCPServerConfig(transport="http", url=url),
229
+ )
230
+ console.print(f"Saved MCP server `{name}` to {path}")
231
+
232
+
233
+ @app.command("mcp-bootstrap-defaults")
234
+ def mcp_bootstrap_defaults(
235
+ config_path: Path | None = typer.Option(None, "--config", help="Path to config.toml"),
236
+ ) -> None:
237
+ """Seed the default remote MCP servers."""
238
+ config = load_config(config_path)
239
+ path = bootstrap_default_mcp_servers(config.mcp_config_path, config.workspace_dir)
240
+ console.print(f"Bootstrapped default MCP servers into {path}")
241
+
242
+
243
+ @app.command("agent-list")
244
+ def agent_list(
245
+ config_path: Path | None = typer.Option(None, "--config", help="Path to config.toml"),
246
+ include_saved: bool = typer.Option(True, "--include-saved/--no-include-saved", help="Show saved agents."),
247
+ ) -> None:
248
+ """List prebuilt and saved agent specs."""
249
+ config = load_config(config_path)
250
+ prebuilt = get_prebuilt_agents(config.default_model)
251
+ registry = _get_registry(config)
252
+
253
+ table = Table(title="Agents")
254
+ table.add_column("Name")
255
+ table.add_column("Source")
256
+ table.add_column("MCP Servers")
257
+ table.add_column("Description")
258
+ for spec in prebuilt.values():
259
+ table.add_row(spec.name, "prebuilt", ", ".join(spec.mcp_servers) or "-", spec.description)
260
+ if include_saved:
261
+ for name in registry.list_agents():
262
+ spec = registry.load(name)
263
+ table.add_row(spec.name, "saved", ", ".join(spec.mcp_servers) or "-", spec.description or "-")
264
+ console.print(table)
265
+
266
+
267
+ @app.command("agent-show")
268
+ def agent_show(
269
+ name: str = typer.Argument(..., help="Agent name."),
270
+ config_path: Path | None = typer.Option(None, "--config", help="Path to config.toml"),
271
+ ) -> None:
272
+ """Show a resolved agent spec."""
273
+ config = load_config(config_path)
274
+ spec = _resolve_agent_spec(config, name)
275
+ table = Table(title=f"Agent: {spec.name}")
276
+ table.add_column("Field")
277
+ table.add_column("Value")
278
+ table.add_row("model", spec.model)
279
+ table.add_row("description", spec.description or "-")
280
+ table.add_row("mcp_servers", ", ".join(spec.mcp_servers) or "-")
281
+ table.add_row("skills", ", ".join(spec.skills) or "-")
282
+ table.add_row("workspace_dir", spec.workspace_dir or config.workspace_dir)
283
+ table.add_row("system_prompt", spec.system_prompt)
284
+ console.print(table)
285
+
286
+
287
+ @app.command("agent-save-prebuilt")
288
+ def agent_save_prebuilt(
289
+ name: str = typer.Argument(..., help="Prebuilt agent name."),
290
+ config_path: Path | None = typer.Option(None, "--config", help="Path to config.toml"),
291
+ ) -> None:
292
+ """Save a prebuilt agent spec into the user registry."""
293
+ config = load_config(config_path)
294
+ prebuilt = get_prebuilt_agents(config.default_model)
295
+ if name not in prebuilt:
296
+ raise typer.BadParameter(f"Unknown prebuilt agent: {name}")
297
+ path = _get_registry(config).save(prebuilt[name])
298
+ console.print(f"Saved agent `{name}` to {path}")
299
+
300
+
301
+ @app.command("agent-build-check")
302
+ def agent_build_check(
303
+ name: str = typer.Argument(..., help="Agent name."),
304
+ skill: list[str] | None = typer.Option(
305
+ None,
306
+ "--skill",
307
+ help="Installed skill name, `installed`, or a skill source path to enable for this build.",
308
+ ),
309
+ config_path: Path | None = typer.Option(None, "--config", help="Path to config.toml"),
310
+ ) -> None:
311
+ """Construct an agent graph to verify runtime wiring."""
312
+ config = load_config(config_path)
313
+ spec = _resolve_agent_spec(config, name)
314
+ if skill:
315
+ spec = replace(spec, skills=[*spec.skills, *skill])
316
+ diagnostics: list[str] = []
317
+ agent = build_agent(spec, config=config, diagnostics=diagnostics)
318
+ for message in diagnostics:
319
+ console.print(f"[yellow]{message}[/yellow]")
320
+ console.print(f"Built agent `{spec.name}` as {type(agent).__name__}")
321
+
322
+
323
+ @app.command("agent-run-prompt")
324
+ def agent_run_prompt(
325
+ name: str = typer.Argument(..., help="Agent name."),
326
+ prompt: str = typer.Option(..., "--prompt", help="Prompt text to send to the agent."),
327
+ skill: list[str] | None = typer.Option(
328
+ None,
329
+ "--skill",
330
+ help="Installed skill name, `installed`, or a skill source path to enable for this run.",
331
+ ),
332
+ stream: bool = typer.Option(False, "--stream", help="Print streamed updates instead of only the final response."),
333
+ history_limit: int = typer.Option(
334
+ DEFAULT_HISTORY_REPLAY_LIMIT,
335
+ "--history-limit",
336
+ min=0,
337
+ help="Number of recent persisted turns to replay into the prompt.",
338
+ ),
339
+ thread_id: str | None = typer.Option(None, "--thread-id", help="Explicit persisted thread ID to continue."),
340
+ new_thread: bool = typer.Option(False, "--new-thread", help="Create a fresh thread ID for this run."),
341
+ config_path: Path | None = typer.Option(None, "--config", help="Path to config.toml"),
342
+ ) -> None:
343
+ """Build an agent and run a single prompt."""
344
+ config = load_config(config_path)
345
+ spec = _resolve_agent_spec(config, name)
346
+ if skill:
347
+ spec = replace(spec, skills=[*spec.skills, *skill])
348
+ diagnostics: list[str] = []
349
+ agent = build_agent(spec, config=config, diagnostics=diagnostics)
350
+ for message in diagnostics:
351
+ console.print(f"[yellow]{message}[/yellow]")
352
+ if thread_id and new_thread:
353
+ raise typer.BadParameter("Use either --thread-id or --new-thread, not both.")
354
+ resolved_thread_id = resolve_thread_id(spec.name, thread_id=thread_id, new_thread=new_thread)
355
+ thread_config = build_thread_config(spec.name, thread_id=resolved_thread_id)
356
+ should_bootstrap_history = history_limit > 0 and not has_thread_checkpoint(
357
+ config.sessions_dir,
358
+ spec.name,
359
+ thread_id=resolved_thread_id,
360
+ )
361
+ history = (
362
+ load_chat_history(
363
+ config.sessions_dir,
364
+ limit=history_limit,
365
+ agent_name=spec.name,
366
+ thread_id=resolved_thread_id,
367
+ )
368
+ if should_bootstrap_history
369
+ else []
370
+ )
371
+ payload = build_prompt_payload(prompt, history=history)
372
+ console.print(f"thread: {resolved_thread_id}")
373
+ append_chat_turn(
374
+ config.sessions_dir,
375
+ "user",
376
+ prompt,
377
+ agent_name=spec.name,
378
+ thread_id=resolved_thread_id,
379
+ )
380
+ if stream:
381
+ last_chunk = None
382
+ final_response = ""
383
+ for chunk in agent.stream(payload, config=thread_config, stream_mode="updates"):
384
+ last_chunk = chunk
385
+ for event in parse_stream_chunk(chunk):
386
+ if event.kind == "assistant":
387
+ final_response = event.text
388
+ console.print(event.text)
389
+ else:
390
+ console.print(f"[{event.kind}:{event.label}] {event.text or '(ok)'}")
391
+ if last_chunk is not None:
392
+ final_text = extract_text_response(last_chunk)
393
+ if final_text and not str(final_text).startswith("{'"):
394
+ final_response = final_text
395
+ console.print(f"[final] {final_text}")
396
+ if final_response:
397
+ append_chat_turn(
398
+ config.sessions_dir,
399
+ "assistant",
400
+ final_response,
401
+ agent_name=spec.name,
402
+ thread_id=resolved_thread_id,
403
+ )
404
+ return
405
+
406
+ result = agent.invoke(payload, config=thread_config)
407
+ response = extract_text_response(result)
408
+ console.print(response)
409
+ if response:
410
+ append_chat_turn(
411
+ config.sessions_dir,
412
+ "assistant",
413
+ response,
414
+ agent_name=spec.name,
415
+ thread_id=resolved_thread_id,
416
+ )
417
+
418
+
419
+ @app.command("session-show")
420
+ def session_show(
421
+ limit: int = typer.Option(20, "--limit", help="Number of recent chat turns to show."),
422
+ agent_name: str | None = typer.Option(None, "--agent", help="Optional agent name to filter by."),
423
+ thread_id: str | None = typer.Option(None, "--thread-id", help="Optional explicit thread ID to filter by."),
424
+ all_threads: bool = typer.Option(
425
+ False,
426
+ "--all-threads",
427
+ help="When used with --agent, include every persisted thread for that agent.",
428
+ ),
429
+ config_path: Path | None = typer.Option(None, "--config", help="Path to config.toml"),
430
+ ) -> None:
431
+ """Show recent persisted chat turns."""
432
+ config = load_config(config_path)
433
+ if thread_id and all_threads:
434
+ raise typer.BadParameter("Use either --thread-id or --all-threads, not both.")
435
+ resolved_thread_id = thread_id
436
+ if resolved_thread_id is None and agent_name and not all_threads:
437
+ resolved_thread_id = get_thread_id(agent_name)
438
+ turns = load_chat_history(
439
+ config.sessions_dir,
440
+ limit=limit,
441
+ agent_name=agent_name,
442
+ thread_id=resolved_thread_id,
443
+ )
444
+ table = Table(title="Recent Session History")
445
+ table.add_column("Role")
446
+ table.add_column("Content")
447
+ table.add_column("Agent")
448
+ table.add_column("Thread")
449
+ table.add_column("Created At")
450
+ if not turns:
451
+ table.add_row("-", "No chat history", agent_name or "-", resolved_thread_id or "-", "-")
452
+ else:
453
+ for turn in turns:
454
+ table.add_row(
455
+ turn.role,
456
+ turn.content,
457
+ turn.agent_name or "-",
458
+ turn.thread_id or "-",
459
+ turn.created_at,
460
+ )
461
+ console.print(table)
462
+
463
+
464
+ @app.command("session-list")
465
+ def session_list(
466
+ limit: int = typer.Option(20, "--limit", help="Number of persisted threads to show."),
467
+ agent_name: str | None = typer.Option(None, "--agent", help="Optional agent name to filter by."),
468
+ thread_id: str | None = typer.Option(None, "--thread-id", help="Optional exact thread ID to filter by."),
469
+ query: str | None = typer.Option(None, "--query", help="Search agent name, thread ID, or latest content."),
470
+ config_path: Path | None = typer.Option(None, "--config", help="Path to config.toml"),
471
+ ) -> None:
472
+ """List persisted chat threads with latest activity."""
473
+ config = load_config(config_path)
474
+ threads = list_chat_threads(
475
+ config.sessions_dir,
476
+ limit=limit,
477
+ agent_name=agent_name,
478
+ thread_id=thread_id,
479
+ query=query,
480
+ )
481
+ table = Table(title="Session Threads")
482
+ table.add_column("Agent")
483
+ table.add_column("Thread")
484
+ table.add_column("Turns")
485
+ table.add_column("Last Role")
486
+ table.add_column("Last Content")
487
+ table.add_column("Last Updated")
488
+ if not threads:
489
+ table.add_row(agent_name or "-", "-", "0", "-", "No persisted threads", "-")
490
+ else:
491
+ for thread in threads:
492
+ preview = thread.last_content if len(thread.last_content) <= 80 else f"{thread.last_content[:77]}..."
493
+ table.add_row(
494
+ thread.agent_name or "-",
495
+ thread.thread_id or "-",
496
+ str(thread.turn_count),
497
+ thread.last_role,
498
+ preview,
499
+ thread.last_created_at,
500
+ )
501
+ console.print(table)
502
+
503
+
504
+ @app.command("skill-list")
505
+ def skill_list(config_path: Path | None = typer.Option(None, "--config", help="Path to config.toml")) -> None:
506
+ """List locally installed skills."""
507
+ config = load_config(config_path)
508
+ skills = list_installed_skills(config.skills_dir)
509
+ table = Table(title="Installed Skills")
510
+ table.add_column("Name")
511
+ table.add_column("Tools")
512
+ table.add_column("Description")
513
+ table.add_column("Path")
514
+ if not skills:
515
+ table.add_row("-", "-", "No local skills installed", config.skills_dir)
516
+ else:
517
+ for skill in skills:
518
+ table.add_row(
519
+ skill.name,
520
+ ", ".join(skill.allowed_tools) or "-",
521
+ skill.description,
522
+ str(skill.path),
523
+ )
524
+ console.print(table)
525
+
526
+
527
+ @app.command("skill-show")
528
+ def skill_show(
529
+ name: str = typer.Argument(..., help="Installed skill name."),
530
+ config_path: Path | None = typer.Option(None, "--config", help="Path to config.toml"),
531
+ ) -> None:
532
+ """Show metadata for one locally installed skill."""
533
+ config = load_config(config_path)
534
+ skills = {skill.name: skill for skill in list_installed_skills(config.skills_dir)}
535
+ if name not in skills:
536
+ raise typer.BadParameter(f"Unknown installed skill: {name}")
537
+ skill = skills[name]
538
+ table = Table(title=f"Skill: {skill.name}")
539
+ table.add_column("Field")
540
+ table.add_column("Value")
541
+ table.add_row("path", str(skill.path))
542
+ table.add_row("description", skill.description)
543
+ table.add_row("allowed_tools", ", ".join(skill.allowed_tools) or "-")
544
+ table.add_row("license", skill.license or "-")
545
+ table.add_row("compatibility", skill.compatibility or "-")
546
+ table.add_row("agent spec token", skill.name)
547
+ table.add_row("all installed token", "installed")
548
+ console.print(table)
549
+
550
+
551
+ @app.command("skill-install")
552
+ def skill_install(
553
+ source: Path = typer.Argument(..., help="Path to a local skill directory or SKILL.md file."),
554
+ overwrite: bool = typer.Option(False, "--overwrite", help="Replace an existing installed skill."),
555
+ config_path: Path | None = typer.Option(None, "--config", help="Path to config.toml"),
556
+ ) -> None:
557
+ """Install a local skill into the AgenCLI skill library."""
558
+ config = load_config(config_path)
559
+ installed = install_skill(source, config.skills_dir, overwrite=overwrite)
560
+ console.print(f"Installed skill `{installed.name}` into {installed.path}")
561
+ console.print("Use it with `--skill installed` or `--skill <skill-name>` on agent commands.")
@@ -0,0 +1 @@
1
+ """Core runtime primitives for AgenCLI."""