aisquare-cli 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.
Files changed (50) hide show
  1. aisquare/__init__.py +10 -0
  2. aisquare/__main__.py +6 -0
  3. aisquare/cli/__init__.py +1 -0
  4. aisquare/cli/agents.py +60 -0
  5. aisquare/cli/app.py +101 -0
  6. aisquare/cli/auth.py +27 -0
  7. aisquare/cli/capture.py +39 -0
  8. aisquare/cli/common.py +350 -0
  9. aisquare/cli/config.py +54 -0
  10. aisquare/cli/connectors.py +37 -0
  11. aisquare/cli/context.py +139 -0
  12. aisquare/cli/enforce.py +27 -0
  13. aisquare/cli/hook.py +61 -0
  14. aisquare/cli/policy.py +15 -0
  15. aisquare/cli/project.py +66 -0
  16. aisquare/cli/root.py +158 -0
  17. aisquare/core/__init__.py +1 -0
  18. aisquare/core/agents.py +196 -0
  19. aisquare/core/config.py +63 -0
  20. aisquare/core/console.py +17 -0
  21. aisquare/core/editor.py +39 -0
  22. aisquare/core/entries.py +33 -0
  23. aisquare/core/ids.py +51 -0
  24. aisquare/core/injection.py +60 -0
  25. aisquare/core/paths.py +82 -0
  26. aisquare/core/snapshot.py +221 -0
  27. aisquare/core/state.py +35 -0
  28. aisquare/core/store.py +430 -0
  29. aisquare/core/stubs.py +34 -0
  30. aisquare/core/workspace.py +84 -0
  31. aisquare/models.py +167 -0
  32. aisquare/py.typed +0 -0
  33. aisquare/services/__init__.py +6 -0
  34. aisquare/services/agents.py +87 -0
  35. aisquare/services/auth.py +35 -0
  36. aisquare/services/capture.py +30 -0
  37. aisquare/services/connectors.py +25 -0
  38. aisquare/services/context.py +213 -0
  39. aisquare/services/diagnostics.py +193 -0
  40. aisquare/services/hooks.py +62 -0
  41. aisquare/services/lifecycle.py +93 -0
  42. aisquare/services/policy.py +25 -0
  43. aisquare/services/project.py +106 -0
  44. aisquare/services/settings.py +72 -0
  45. aisquare/services/sync.py +10 -0
  46. aisquare_cli-0.1.0.dist-info/METADATA +307 -0
  47. aisquare_cli-0.1.0.dist-info/RECORD +50 -0
  48. aisquare_cli-0.1.0.dist-info/WHEEL +4 -0
  49. aisquare_cli-0.1.0.dist-info/entry_points.txt +3 -0
  50. aisquare_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
aisquare/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """aisquare — a portable memory layer for coding agents."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("aisquare-cli")
7
+ except PackageNotFoundError: # pragma: no cover - only when running from a raw checkout
8
+ __version__ = "0.0.0+uninstalled"
9
+
10
+ __all__ = ["__version__"]
aisquare/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Support ``python -m aisquare``."""
2
+
3
+ from aisquare.cli.app import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1 @@
1
+ """Thin Typer layer — parses arguments and delegates to the service layer."""
aisquare/cli/agents.py ADDED
@@ -0,0 +1,60 @@
1
+ """``aisquare agents`` — detect and connect coding agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from aisquare.cli.common import emit_agents, emit_connected, emit_disconnected, fail
10
+ from aisquare.services import agents as agents_service
11
+
12
+ app = typer.Typer(help="Detect and connect coding agents.", no_args_is_help=True)
13
+
14
+ AgentName = Annotated[str, typer.Argument(help="Agent name, e.g. 'claude-code'.")]
15
+
16
+
17
+ @app.command("list")
18
+ def list_() -> None:
19
+ """List supported agents and whether they are connected."""
20
+ emit_agents(agents_service.list_agents())
21
+
22
+
23
+ @app.command("scan")
24
+ def scan() -> None:
25
+ """Scan this machine for installed agents."""
26
+ emit_agents(agents_service.scan())
27
+
28
+
29
+ @app.command("status")
30
+ def status(
31
+ name: Annotated[str | None, typer.Argument(help="Agent to inspect (default: all).")] = None,
32
+ ) -> None:
33
+ """Show integration health for one agent, or all of them."""
34
+ try:
35
+ agents = agents_service.status(name)
36
+ except KeyError:
37
+ fail(f"unknown agent: {name}", error="unknown_agent", ref=name)
38
+ emit_agents(agents)
39
+
40
+
41
+ @app.command("connect")
42
+ def connect(name: AgentName) -> None:
43
+ """Connect an agent: install aisquare's hooks and ingest its existing context."""
44
+ try:
45
+ connection = agents_service.connect(name)
46
+ except KeyError:
47
+ fail(f"unknown agent: {name}", error="unknown_agent", ref=name)
48
+ except ValueError as exc:
49
+ fail(str(exc), error="not_installed", ref=name)
50
+ emit_connected(connection)
51
+
52
+
53
+ @app.command("disconnect")
54
+ def disconnect(name: AgentName) -> None:
55
+ """Disconnect an agent (its already-imported context is kept)."""
56
+ try:
57
+ agents_service.disconnect(name)
58
+ except KeyError:
59
+ fail(f"unknown agent: {name}", error="unknown_agent", ref=name)
60
+ emit_disconnected(name)
aisquare/cli/app.py ADDED
@@ -0,0 +1,101 @@
1
+ """Root Typer application: global flags, version, and command registration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from aisquare import __version__
10
+ from aisquare.cli import (
11
+ agents,
12
+ auth,
13
+ capture,
14
+ connectors,
15
+ context,
16
+ enforce,
17
+ hook,
18
+ policy,
19
+ project,
20
+ root,
21
+ )
22
+ from aisquare.cli import config as config_cli
23
+ from aisquare.core.state import RuntimeState, set_state
24
+
25
+ app = typer.Typer(
26
+ help="Portable memory layer for coding agents.",
27
+ no_args_is_help=True,
28
+ context_settings={"help_option_names": ["-h", "--help"]},
29
+ pretty_exceptions_show_locals=False,
30
+ )
31
+
32
+
33
+ def _print_version(value: bool) -> None:
34
+ """Eager callback for ``--version``."""
35
+ if value:
36
+ typer.echo(f"aisquare {__version__}")
37
+ raise typer.Exit()
38
+
39
+
40
+ @app.callback()
41
+ def main_callback(
42
+ ctx: typer.Context,
43
+ version: Annotated[
44
+ bool,
45
+ typer.Option(
46
+ "--version",
47
+ "-V",
48
+ help="Show the version and exit.",
49
+ callback=_print_version,
50
+ is_eager=True,
51
+ ),
52
+ ] = False,
53
+ verbose: Annotated[
54
+ bool, typer.Option("--verbose", "-v", help="Enable verbose output.")
55
+ ] = False,
56
+ quiet: Annotated[
57
+ bool, typer.Option("--quiet", "-q", help="Suppress non-essential output.")
58
+ ] = False,
59
+ json_output: Annotated[
60
+ bool, typer.Option("--json", help="Emit machine-readable JSON on stdout.")
61
+ ] = False,
62
+ profile: Annotated[
63
+ str, typer.Option("--profile", help="Configuration profile to use.", metavar="NAME")
64
+ ] = "default",
65
+ no_color: Annotated[bool, typer.Option("--no-color", help="Disable coloured output.")] = False,
66
+ ) -> None:
67
+ """Portable memory layer for coding agents.
68
+
69
+ Keeps user preferences and per-project conventions persistent across
70
+ agent sessions.
71
+ """
72
+ state = RuntimeState(
73
+ verbose=verbose,
74
+ quiet=quiet,
75
+ json_output=json_output,
76
+ profile=profile,
77
+ no_color=no_color,
78
+ )
79
+ set_state(state)
80
+ ctx.obj = state
81
+
82
+
83
+ root.register(app)
84
+
85
+ app.add_typer(auth.app, name="auth")
86
+ app.add_typer(agents.app, name="agents")
87
+ app.add_typer(connectors.app, name="connectors")
88
+ app.add_typer(context.app, name="context")
89
+ app.add_typer(context.app, name="ctx", hidden=True, help="Alias of 'context'.")
90
+ app.add_typer(project.app, name="project")
91
+ app.add_typer(project.app, name="workspace", hidden=True, help="Alias of 'project'.")
92
+ app.add_typer(capture.app, name="capture")
93
+ app.add_typer(config_cli.app, name="config")
94
+ app.add_typer(policy.app, name="policy")
95
+ app.add_typer(enforce.app, name="enforce")
96
+ app.add_typer(hook.app, name="hook", hidden=True)
97
+
98
+
99
+ def main() -> None:
100
+ """Console-script entry point for both ``aisquare`` and ``asq``."""
101
+ app()
aisquare/cli/auth.py ADDED
@@ -0,0 +1,27 @@
1
+ """``aisquare auth`` — inspect and manage credentials."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from aisquare.services import auth as auth_service
8
+
9
+ app = typer.Typer(help="Inspect and manage credentials.", no_args_is_help=True)
10
+
11
+
12
+ @app.command("status")
13
+ def status() -> None:
14
+ """Show whether this machine is authenticated, and as whom."""
15
+ auth_service.status()
16
+
17
+
18
+ @app.command("rotate")
19
+ def rotate() -> None:
20
+ """Rotate the stored API token."""
21
+ auth_service.rotate()
22
+
23
+
24
+ @app.command("token")
25
+ def token() -> None:
26
+ """Print the active API token (for scripting)."""
27
+ auth_service.token()
@@ -0,0 +1,39 @@
1
+ """``aisquare capture`` — the background capture pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from aisquare.services import capture as capture_service
8
+
9
+ app = typer.Typer(help="Control background capture of agent activity.", no_args_is_help=True)
10
+
11
+
12
+ @app.command("status")
13
+ def status() -> None:
14
+ """Show whether capture is running."""
15
+ capture_service.status()
16
+
17
+
18
+ @app.command("pause")
19
+ def pause() -> None:
20
+ """Pause capture without removing hooks."""
21
+ capture_service.pause()
22
+
23
+
24
+ @app.command("resume")
25
+ def resume() -> None:
26
+ """Resume paused capture."""
27
+ capture_service.resume()
28
+
29
+
30
+ @app.command("start")
31
+ def start() -> None:
32
+ """Start the capture pipeline."""
33
+ capture_service.start()
34
+
35
+
36
+ @app.command("stop")
37
+ def stop() -> None:
38
+ """Stop the capture pipeline."""
39
+ capture_service.stop()
aisquare/cli/common.py ADDED
@@ -0,0 +1,350 @@
1
+ """Shared CLI parsing and rendering helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import NoReturn
8
+
9
+ import tomli_w
10
+ import typer
11
+ from rich.table import Table
12
+
13
+ from aisquare.core.config import AppConfig
14
+ from aisquare.core.console import stderr_console, stdout_console
15
+ from aisquare.core.state import get_state
16
+ from aisquare.models import (
17
+ AgentConnection,
18
+ AgentInfo,
19
+ CheckStatus,
20
+ ContextEntry,
21
+ DoctorCheck,
22
+ InjectionRecord,
23
+ OnboardReport,
24
+ Pool,
25
+ ProjectInfo,
26
+ PromptRecord,
27
+ SetupReport,
28
+ StatusReport,
29
+ )
30
+
31
+ _DEFAULT_EMPTY = 'No context entries yet. Add one with: aisquare remember "…"'
32
+
33
+
34
+ def resolve_pool(user: bool, project: bool) -> Pool | None:
35
+ """Map the ``--user``/``--project`` flag pair onto a pool name.
36
+
37
+ Returns ``None`` when neither flag is given, letting the service apply
38
+ the configured default pool.
39
+ """
40
+ if user and project:
41
+ raise typer.BadParameter("--user and --project are mutually exclusive.")
42
+ if user:
43
+ return "user"
44
+ if project:
45
+ return "project"
46
+ return None
47
+
48
+
49
+ def emit_entry(entry: ContextEntry, *, verb: str) -> None:
50
+ """Render a single entry — JSON under ``--json``, a confirmation otherwise."""
51
+ if get_state().json_output:
52
+ typer.echo(entry.model_dump_json())
53
+ else:
54
+ stdout_console().print(f"✓ {verb} ({entry.pool}): {entry.text}")
55
+
56
+
57
+ def emit_entries(entries: list[ContextEntry], *, empty_message: str = _DEFAULT_EMPTY) -> None:
58
+ """Render a list of entries — a JSON array under ``--json``, a table otherwise."""
59
+ if get_state().json_output:
60
+ typer.echo(json.dumps([entry.model_dump(mode="json") for entry in entries]))
61
+ return
62
+ if not entries:
63
+ stdout_console().print(empty_message)
64
+ return
65
+ table = Table(show_header=True, header_style="bold")
66
+ table.add_column("ID", no_wrap=True)
67
+ table.add_column("POOL", no_wrap=True)
68
+ table.add_column("TAGS")
69
+ table.add_column("TEXT")
70
+ for entry in entries:
71
+ table.add_row(entry.id, entry.pool, ", ".join(entry.tags), entry.text)
72
+ stdout_console().print(table)
73
+
74
+
75
+ def emit_removed(ref: str) -> None:
76
+ """Confirm a deletion — JSON under ``--json``, a confirmation otherwise."""
77
+ if get_state().json_output:
78
+ typer.echo(json.dumps({"removed": ref}, separators=(",", ":")))
79
+ else:
80
+ stdout_console().print(f"✓ removed {ref}")
81
+
82
+
83
+ def emit_imported(count: int) -> None:
84
+ """Confirm an import — JSON under ``--json``, a confirmation otherwise."""
85
+ if get_state().json_output:
86
+ typer.echo(json.dumps({"imported": count}, separators=(",", ":")))
87
+ else:
88
+ noun = "entry" if count == 1 else "entries"
89
+ stdout_console().print(f"✓ imported {count} {noun}")
90
+
91
+
92
+ def emit_exported(file: Path) -> None:
93
+ """Confirm an export to a file — JSON under ``--json``, a confirmation otherwise."""
94
+ if get_state().json_output:
95
+ typer.echo(json.dumps({"exported": str(file)}, separators=(",", ":")))
96
+ else:
97
+ stdout_console().print(f"✓ exported to {file}")
98
+
99
+
100
+ def emit_entry_detail(entry: ContextEntry) -> None:
101
+ """Render one entry in full — JSON under ``--json``, a key/value view otherwise."""
102
+ if get_state().json_output:
103
+ typer.echo(entry.model_dump_json())
104
+ return
105
+ grid = Table.grid(padding=(0, 2))
106
+ grid.add_column(style="bold")
107
+ grid.add_column()
108
+ grid.add_row("id", entry.id)
109
+ grid.add_row("pool", entry.pool)
110
+ if entry.project_id:
111
+ grid.add_row("project", entry.project_id)
112
+ if entry.tags:
113
+ grid.add_row("tags", ", ".join(entry.tags))
114
+ grid.add_row("created", entry.created_at.isoformat())
115
+ grid.add_row("updated", entry.updated_at.isoformat())
116
+ console = stdout_console()
117
+ console.print(grid)
118
+ console.print()
119
+ console.print(entry.text)
120
+
121
+
122
+ def emit_block(block: str) -> None:
123
+ """Emit an assembled context block — wrapped under ``--json``, verbatim otherwise."""
124
+ if get_state().json_output:
125
+ typer.echo(json.dumps({"block": block}))
126
+ else:
127
+ typer.echo(block, nl=False)
128
+
129
+
130
+ def emit_injection_record(record: InjectionRecord | None) -> None:
131
+ """Render the last-injection record for ``why``."""
132
+ if get_state().json_output:
133
+ typer.echo("null" if record is None else record.model_dump_json())
134
+ return
135
+ console = stdout_console()
136
+ if record is None:
137
+ console.print("No context has been injected yet. Run: aisquare inject")
138
+ return
139
+ total = record.user_count + record.project_count
140
+ console.print(f"Last injection: {record.injected_at.isoformat()}")
141
+ console.print(
142
+ f" {total} entries — {record.user_count} from your user pool, "
143
+ f"{record.project_count} from this project"
144
+ )
145
+
146
+
147
+ def emit_project_detail(project: ProjectInfo) -> None:
148
+ """Render one project — JSON under ``--json``, a key/value view otherwise."""
149
+ if get_state().json_output:
150
+ typer.echo(project.model_dump_json())
151
+ return
152
+ grid = Table.grid(padding=(0, 2))
153
+ grid.add_column(style="bold")
154
+ grid.add_column()
155
+ grid.add_row("name", project.root.name or project.id)
156
+ grid.add_row("id", project.id)
157
+ grid.add_row("root", str(project.root))
158
+ if project.linked_repos:
159
+ grid.add_row("repos", ", ".join(project.linked_repos))
160
+ stdout_console().print(grid)
161
+
162
+
163
+ def emit_projects(projects: list[ProjectInfo], *, active_id: str | None) -> None:
164
+ """Render the project list — a JSON array under ``--json``, a table otherwise."""
165
+ if get_state().json_output:
166
+ typer.echo(json.dumps([project.model_dump(mode="json") for project in projects]))
167
+ return
168
+ if not projects:
169
+ stdout_console().print("No projects registered yet. Run: aisquare init")
170
+ return
171
+ table = Table(show_header=True, header_style="bold")
172
+ table.add_column("", no_wrap=True)
173
+ table.add_column("NAME")
174
+ table.add_column("ID", no_wrap=True)
175
+ table.add_column("ROOT")
176
+ for project in projects:
177
+ marker = "*" if project.id == active_id else ""
178
+ table.add_row(marker, project.root.name or "—", project.id, str(project.root))
179
+ stdout_console().print(table)
180
+
181
+
182
+ def emit_project_action(message: str, project: ProjectInfo) -> None:
183
+ """Confirm a project action — the project as JSON under ``--json``, a message otherwise."""
184
+ if get_state().json_output:
185
+ typer.echo(project.model_dump_json())
186
+ else:
187
+ stdout_console().print(message)
188
+
189
+
190
+ def emit_setup(report: SetupReport) -> None:
191
+ """Render the outcome of ``init``."""
192
+ if get_state().json_output:
193
+ typer.echo(report.model_dump_json())
194
+ return
195
+ console = stdout_console()
196
+ verb = "already initialized" if report.already_initialized else "initialized"
197
+ console.print(f"✓ aisquare {verb} at {report.home}")
198
+ console.print(
199
+ f" project: {report.project.root.name or report.project.id} ({report.project.id})"
200
+ )
201
+ if report.onboarded:
202
+ console.print(f" onboarded {report.onboarded} context entries")
203
+ for note in report.notes:
204
+ console.print(f" note: {note}")
205
+
206
+
207
+ def emit_config(config: AppConfig) -> None:
208
+ """Render the full config — JSON under ``--json``, TOML otherwise."""
209
+ if get_state().json_output:
210
+ typer.echo(config.model_dump_json())
211
+ else:
212
+ typer.echo(tomli_w.dumps(config.model_dump(mode="json")), nl=False)
213
+
214
+
215
+ def emit_config_value(key: str, value: str) -> None:
216
+ """Render one config value — ``{key: value}`` under ``--json``, ``key = value`` otherwise."""
217
+ if get_state().json_output:
218
+ typer.echo(json.dumps({key: value}))
219
+ else:
220
+ stdout_console().print(f"{key} = {value}")
221
+
222
+
223
+ def emit_agents(agents: list[AgentInfo]) -> None:
224
+ """Render detected agents — a JSON array under ``--json``, a table otherwise."""
225
+ if get_state().json_output:
226
+ typer.echo(json.dumps([agent.model_dump(mode="json") for agent in agents]))
227
+ return
228
+ table = Table(show_header=True, header_style="bold")
229
+ table.add_column("AGENT")
230
+ table.add_column("DETECTED")
231
+ table.add_column("CONNECTED")
232
+ table.add_column("CONTEXT")
233
+ for agent in agents:
234
+ context = ", ".join(str(path) for path in agent.config_paths) or "—"
235
+ table.add_row(
236
+ agent.name,
237
+ "yes" if agent.detected else "no",
238
+ "yes" if agent.connected else "no",
239
+ context,
240
+ )
241
+ stdout_console().print(table)
242
+
243
+
244
+ def emit_connected(connection: AgentConnection) -> None:
245
+ """Confirm an agent connection: hook install + context ingested."""
246
+ if get_state().json_output:
247
+ typer.echo(connection.model_dump_json())
248
+ return
249
+ hooks = "hooks installed" if connection.hooks_installed else "no hooks for this agent"
250
+ noun = "entry" if connection.imported == 1 else "entries"
251
+ stdout_console().print(
252
+ f"✓ connected {connection.name} — {hooks}; imported {connection.imported} {noun}"
253
+ )
254
+
255
+
256
+ def emit_onboard(report: OnboardReport) -> None:
257
+ """Render the outcome of ``project onboard`` — snapshot summary + seeded facts."""
258
+ if get_state().json_output:
259
+ typer.echo(report.model_dump_json())
260
+ return
261
+ console = stdout_console()
262
+ snapshot = report.snapshot
263
+ if snapshot is not None and snapshot.status == "ready":
264
+ line = f"✓ snapshot: {snapshot.file_count} files, {snapshot.token_count} tokens"
265
+ if snapshot.skeleton_token_count:
266
+ line += f" (skeleton {snapshot.skeleton_token_count} tokens)"
267
+ console.print(line)
268
+ elif snapshot is not None and snapshot.status == "too_large":
269
+ console.print("snapshot: codebase too large to pack within the token budget")
270
+ else:
271
+ console.print("snapshot: skipped (repomix/Node not available)")
272
+ if report.seeded:
273
+ console.print(f"seeded {len(report.seeded)} project fact(s):")
274
+ for entry in report.seeded:
275
+ console.print(f" - {entry.text}")
276
+
277
+
278
+ def emit_prompts(prompts: list[PromptRecord]) -> None:
279
+ """Render captured prompt history — a JSON array under ``--json``, a table otherwise."""
280
+ if get_state().json_output:
281
+ typer.echo(json.dumps([prompt.model_dump(mode="json") for prompt in prompts]))
282
+ return
283
+ if not prompts:
284
+ stdout_console().print(
285
+ "No prompts captured yet. Connect Claude Code: aisquare agents connect claude-code"
286
+ )
287
+ return
288
+ table = Table(show_header=True, header_style="bold")
289
+ table.add_column("WHEN", no_wrap=True)
290
+ table.add_column("PROMPT")
291
+ for prompt in prompts:
292
+ table.add_row(prompt.created_at.strftime("%Y-%m-%d %H:%M"), prompt.text)
293
+ stdout_console().print(table)
294
+
295
+
296
+ def emit_disconnected(name: str) -> None:
297
+ """Confirm an agent disconnection."""
298
+ if get_state().json_output:
299
+ typer.echo(json.dumps({"disconnected": name}, separators=(",", ":")))
300
+ else:
301
+ stdout_console().print(f"✓ disconnected {name}")
302
+
303
+
304
+ def emit_status(report: StatusReport) -> None:
305
+ """Render the status summary."""
306
+ if get_state().json_output:
307
+ typer.echo(report.model_dump_json())
308
+ return
309
+ console = stdout_console()
310
+ project = report.active_project
311
+ console.print(f"aisquare: {'initialized' if report.initialized else 'not initialized'}")
312
+ console.print(f"home: {report.home}")
313
+ console.print(f"project: {project.root.name or project.id} ({project.id})")
314
+ console.print(
315
+ f"context: {report.user_entries} user, {report.project_entries} in this project; "
316
+ f"{report.project_count} project(s) registered"
317
+ )
318
+ console.print(f"detected: {', '.join(report.agents_detected) or 'none'}")
319
+ console.print(f"connected: {', '.join(report.agents_connected) or 'none'}")
320
+
321
+
322
+ _CHECK_SYMBOL = {CheckStatus.ok: "✓", CheckStatus.warn: "⚠", CheckStatus.fail: "✗"}
323
+
324
+
325
+ def emit_doctor(checks: list[DoctorCheck]) -> None:
326
+ """Render diagnostic check results, with a fix hint for anything not OK."""
327
+ if get_state().json_output:
328
+ typer.echo(json.dumps([check.model_dump(mode="json") for check in checks]))
329
+ return
330
+ console = stdout_console()
331
+ for check in checks:
332
+ console.print(f"{_CHECK_SYMBOL[check.status]} {check.name}: {check.detail}")
333
+ if check.fix and check.status is not CheckStatus.ok:
334
+ console.print(f" → {check.fix}")
335
+
336
+
337
+ def fail(message: str, *, error: str, ref: str | None = None, exit_code: int = 1) -> NoReturn:
338
+ """Report a runtime error and exit.
339
+
340
+ Mirrors the stub contract: a machine-readable object on stdout under
341
+ ``--json``, a human message on stderr otherwise.
342
+ """
343
+ if get_state().json_output:
344
+ payload = {"error": error}
345
+ if ref is not None:
346
+ payload["ref"] = ref
347
+ typer.echo(json.dumps(payload, separators=(",", ":")))
348
+ else:
349
+ stderr_console().print(f"✗ {message}")
350
+ raise typer.Exit(exit_code)
aisquare/cli/config.py ADDED
@@ -0,0 +1,54 @@
1
+ """``aisquare config`` — read and write configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from aisquare.cli.common import emit_config, emit_config_value, fail
10
+ from aisquare.models import RedactionLevel
11
+ from aisquare.services import settings as settings_service
12
+
13
+ app = typer.Typer(help="Read and write aisquare configuration.", no_args_is_help=True)
14
+
15
+
16
+ @app.command("list")
17
+ def list_() -> None:
18
+ """Print the fully-resolved configuration."""
19
+ emit_config(settings_service.list_values())
20
+
21
+
22
+ @app.command("get")
23
+ def get(
24
+ key: Annotated[str, typer.Argument(help="Dotted key, e.g. 'redaction.level'.")],
25
+ ) -> None:
26
+ """Print a single configuration value."""
27
+ try:
28
+ value = settings_service.get_value(key)
29
+ except KeyError:
30
+ fail(f"unknown config key: {key}", error="unknown_key", ref=key)
31
+ emit_config_value(key, value)
32
+
33
+
34
+ @app.command("set")
35
+ def set_(
36
+ key: Annotated[str, typer.Argument(help="Dotted key, e.g. 'capture.enabled'.")],
37
+ value: Annotated[str, typer.Argument(help="New value.")],
38
+ ) -> None:
39
+ """Set a configuration value and save it."""
40
+ try:
41
+ stored = settings_service.set_value(key, value)
42
+ except KeyError:
43
+ fail(f"unknown config key: {key}", error="unknown_key", ref=key)
44
+ except ValueError as exc:
45
+ fail(str(exc), error="invalid_value", ref=key)
46
+ emit_config_value(key, stored)
47
+
48
+
49
+ @app.command("redaction")
50
+ def redaction(
51
+ level: Annotated[RedactionLevel, typer.Argument(help="Redaction level.")],
52
+ ) -> None:
53
+ """Set how aggressively captured data is scrubbed."""
54
+ emit_config_value("redaction.level", settings_service.set_redaction(level))
@@ -0,0 +1,37 @@
1
+ """``aisquare connectors`` — external sources feeding the context pools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from aisquare.services import connectors as connectors_service
10
+
11
+ app = typer.Typer(help="Manage connectors to external sources.", no_args_is_help=True)
12
+
13
+ ConnectorName = Annotated[str, typer.Argument(help="Connector name, e.g. 'notion'.")]
14
+
15
+
16
+ @app.command("list")
17
+ def list_() -> None:
18
+ """List available and configured connectors."""
19
+ connectors_service.list_connectors()
20
+
21
+
22
+ @app.command("add")
23
+ def add(name: ConnectorName) -> None:
24
+ """Add and configure a connector."""
25
+ connectors_service.add(name)
26
+
27
+
28
+ @app.command("remove")
29
+ def remove(name: ConnectorName) -> None:
30
+ """Remove a configured connector."""
31
+ connectors_service.remove(name)
32
+
33
+
34
+ @app.command("status")
35
+ def status() -> None:
36
+ """Show connector health and last sync times."""
37
+ connectors_service.status()