dotai-cli 0.1.1__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.
- dotai/__init__.py +3 -0
- dotai/__main__.py +5 -0
- dotai/cli/__init__.py +148 -0
- dotai/cli/__main__.py +5 -0
- dotai/cli/roles_cmd.py +62 -0
- dotai/cli/rules_cmd.py +202 -0
- dotai/cli/skills_cmd.py +570 -0
- dotai/cli/sync_cmd.py +190 -0
- dotai/cli/watch_cmd.py +106 -0
- dotai/converter.py +738 -0
- dotai/indexer.py +201 -0
- dotai/models.py +375 -0
- dotai/roles.py +174 -0
- dotai/rules.py +328 -0
- dotai/seed/roles/architect.md +26 -0
- dotai/seed/roles/debugger.md +26 -0
- dotai/seed/roles/founder.md +29 -0
- dotai/seed/roles/mentor.md +26 -0
- dotai/seed/roles/product-manager.md +26 -0
- dotai/seed/roles/qa.md +26 -0
- dotai/seed/roles/reviewer.md +26 -0
- dotai/seed/roles/security.md +26 -0
- dotai/seed/roles/ship.md +26 -0
- dotai/seed/roles/writer.md +26 -0
- dotai/seed/skills/careful.md +31 -0
- dotai/seed/skills/commit-helper.md +48 -0
- dotai/seed/skills/context-dump.md +47 -0
- dotai/seed/skills/investigate.md +38 -0
- dotai/seed/skills/learn.md +37 -0
- dotai/seed/skills/parallel-work.md +88 -0
- dotai/seed/skills/plan.md +38 -0
- dotai/seed/skills/review.md +43 -0
- dotai/seed/skills/scaffold.md +35 -0
- dotai/seed/skills/ship.md +29 -0
- dotai/seed/skills/techdebt.md +44 -0
- dotai/seed/skills/verify.md +35 -0
- dotai/skills.py +452 -0
- dotai/store.py +68 -0
- dotai/sync.py +422 -0
- dotai/tools/__init__.py +9 -0
- dotai/tools/base.py +155 -0
- dotai/tools/registry.py +80 -0
- dotai/utils.py +8 -0
- dotai_cli-0.1.1.dist-info/METADATA +594 -0
- dotai_cli-0.1.1.dist-info/RECORD +48 -0
- dotai_cli-0.1.1.dist-info/WHEEL +4 -0
- dotai_cli-0.1.1.dist-info/entry_points.txt +2 -0
- dotai_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
dotai/__init__.py
ADDED
dotai/__main__.py
ADDED
dotai/cli/__init__.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""dotai CLI — manage ~/.ai/ knowledge, roles, skills, and agent sync."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from .. import __version__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _version_callback(value: bool):
|
|
14
|
+
if value:
|
|
15
|
+
print(f"dotai {__version__}")
|
|
16
|
+
raise typer.Exit()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(help="Universal AI context for any coding agent.", no_args_is_help=True)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@app.callback()
|
|
23
|
+
def main(
|
|
24
|
+
version: bool = typer.Option(False, "--version", "-V", callback=_version_callback, is_eager=True, help="Show version and exit"),
|
|
25
|
+
):
|
|
26
|
+
pass
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
SEED_DIR = Path(__file__).parent.parent / "seed"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@app.command()
|
|
33
|
+
def init(
|
|
34
|
+
project: Optional[str] = typer.Argument(None, help="Project path to initialize (omit for global ~/.ai/)"),
|
|
35
|
+
force: bool = typer.Option(False, "--force", "-f", help="Re-seed missing/updated roles and skills (overwrites seeds, keeps custom files)"),
|
|
36
|
+
):
|
|
37
|
+
"""Initialize ~/.ai/ with seed roles and skills, or add .ai/ to a project.
|
|
38
|
+
|
|
39
|
+
Use --force to re-seed after upgrading dotai (overwrites seed files, keeps your custom files).
|
|
40
|
+
"""
|
|
41
|
+
from ..store import load_config, save_config
|
|
42
|
+
from ..models import ProjectConfig
|
|
43
|
+
|
|
44
|
+
if project:
|
|
45
|
+
# Project init
|
|
46
|
+
project_path = Path(project).expanduser().resolve()
|
|
47
|
+
if not project_path.exists():
|
|
48
|
+
console.print(f"[red]Path does not exist: {project_path}[/red]")
|
|
49
|
+
raise typer.Exit(1)
|
|
50
|
+
|
|
51
|
+
ai_dir = project_path / ".ai"
|
|
52
|
+
ai_dir.mkdir(exist_ok=True)
|
|
53
|
+
(ai_dir / "rules").mkdir(exist_ok=True)
|
|
54
|
+
(ai_dir / "roles").mkdir(exist_ok=True)
|
|
55
|
+
(ai_dir / "skills").mkdir(exist_ok=True)
|
|
56
|
+
(ai_dir / "tools").mkdir(exist_ok=True)
|
|
57
|
+
|
|
58
|
+
# Create starter files
|
|
59
|
+
if not (ai_dir / "rules.md").exists():
|
|
60
|
+
(ai_dir / "rules.md").write_text(
|
|
61
|
+
"# Project Rules\n\nProject-specific conventions and guidelines.\n"
|
|
62
|
+
)
|
|
63
|
+
# Register in config
|
|
64
|
+
config = load_config()
|
|
65
|
+
config.add_project(ProjectConfig(
|
|
66
|
+
name=project_path.name,
|
|
67
|
+
path=project_path,
|
|
68
|
+
))
|
|
69
|
+
save_config(config)
|
|
70
|
+
|
|
71
|
+
console.print(f"[green]Initialized .ai/ in {project_path}[/green]")
|
|
72
|
+
console.print(f" Created: rules.md, roles/, skills/, tools/")
|
|
73
|
+
|
|
74
|
+
# Auto-sync agent config files into the project
|
|
75
|
+
from ..sync import sync_project
|
|
76
|
+
written = sync_project(project_path, config, project_path.name)
|
|
77
|
+
for f in written:
|
|
78
|
+
console.print(f" [green]Synced[/green] {f}")
|
|
79
|
+
else:
|
|
80
|
+
# Global init
|
|
81
|
+
ai_dir = Path.home() / ".ai"
|
|
82
|
+
ai_dir.mkdir(exist_ok=True)
|
|
83
|
+
|
|
84
|
+
# Copy seed roles
|
|
85
|
+
roles_dir = ai_dir / "roles"
|
|
86
|
+
roles_dir.mkdir(exist_ok=True)
|
|
87
|
+
seed_roles = SEED_DIR / "roles"
|
|
88
|
+
if seed_roles.exists():
|
|
89
|
+
copied = 0
|
|
90
|
+
for role_file in seed_roles.glob("*.md"):
|
|
91
|
+
dest = roles_dir / role_file.name
|
|
92
|
+
if force or not dest.exists():
|
|
93
|
+
shutil.copy2(role_file, dest)
|
|
94
|
+
copied += 1
|
|
95
|
+
if copied:
|
|
96
|
+
verb = "Updated" if force else "Seeded"
|
|
97
|
+
console.print(f" {verb} {copied} roles")
|
|
98
|
+
|
|
99
|
+
# Copy seed skills (files and folders)
|
|
100
|
+
skills_dir = ai_dir / "skills"
|
|
101
|
+
skills_dir.mkdir(exist_ok=True)
|
|
102
|
+
seed_skills = SEED_DIR / "skills"
|
|
103
|
+
if seed_skills.exists():
|
|
104
|
+
copied = 0
|
|
105
|
+
# Single-file skills
|
|
106
|
+
for skill_file in seed_skills.glob("*.md"):
|
|
107
|
+
dest = skills_dir / skill_file.name
|
|
108
|
+
if force or not dest.exists():
|
|
109
|
+
shutil.copy2(skill_file, dest)
|
|
110
|
+
copied += 1
|
|
111
|
+
# Folder-based skills
|
|
112
|
+
for item in seed_skills.iterdir():
|
|
113
|
+
if item.is_dir() and (item / "main.md").exists():
|
|
114
|
+
dest = skills_dir / item.name
|
|
115
|
+
if force or not dest.exists():
|
|
116
|
+
if dest.exists():
|
|
117
|
+
shutil.rmtree(dest)
|
|
118
|
+
shutil.copytree(item, dest)
|
|
119
|
+
copied += 1
|
|
120
|
+
if copied:
|
|
121
|
+
verb = "Updated" if force else "Seeded"
|
|
122
|
+
console.print(f" {verb} {copied} skills")
|
|
123
|
+
|
|
124
|
+
# Create rules and tools dirs
|
|
125
|
+
(ai_dir / "rules").mkdir(exist_ok=True)
|
|
126
|
+
(ai_dir / "tools").mkdir(exist_ok=True)
|
|
127
|
+
|
|
128
|
+
# Create starter files
|
|
129
|
+
if not (ai_dir / "rules.md").exists():
|
|
130
|
+
(ai_dir / "rules.md").write_text(
|
|
131
|
+
"# Global Rules\n\nUniversal AI coding rules across all projects.\n\n"
|
|
132
|
+
"## Code Quality\n\n- Write clear, readable code\n- Handle errors explicitly\n"
|
|
133
|
+
"- Prefer simple solutions over clever ones\n"
|
|
134
|
+
)
|
|
135
|
+
console.print(f"[green]Initialized ~/.ai/[/green]")
|
|
136
|
+
console.print(f" {ai_dir}/rules.md")
|
|
137
|
+
console.print(f" {ai_dir}/roles/ ({len(list(roles_dir.glob('*.md')))} roles)")
|
|
138
|
+
console.print(f" {ai_dir}/skills/ ({len(list(skills_dir.glob('*.md')))} skills)")
|
|
139
|
+
console.print(f" {ai_dir}/tools/")
|
|
140
|
+
console.print(f"\n[dim]Next: cd into a project and run `dotai sync` to generate agent configs.[/dim]")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# Import submodules to register their commands with the app
|
|
144
|
+
from . import roles_cmd # noqa: E402, F401
|
|
145
|
+
from . import skills_cmd # noqa: E402, F401
|
|
146
|
+
from . import rules_cmd # noqa: E402, F401
|
|
147
|
+
from . import sync_cmd # noqa: E402, F401
|
|
148
|
+
from . import watch_cmd # noqa: E402, F401
|
dotai/cli/__main__.py
ADDED
dotai/cli/roles_cmd.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""CLI commands for roles."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
from . import app, console
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@app.command()
|
|
12
|
+
def roles():
|
|
13
|
+
"""List all available roles."""
|
|
14
|
+
from ..store import load_config
|
|
15
|
+
from ..roles import load_all_roles
|
|
16
|
+
|
|
17
|
+
config = load_config()
|
|
18
|
+
all_roles = load_all_roles(config)
|
|
19
|
+
|
|
20
|
+
if not all_roles:
|
|
21
|
+
console.print("[dim]No roles found. Run `dotai init` to seed default roles.[/dim]")
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
table = Table(title="Available Roles")
|
|
25
|
+
table.add_column("Name", style="bold")
|
|
26
|
+
table.add_column("Scope", style="dim")
|
|
27
|
+
table.add_column("Description")
|
|
28
|
+
table.add_column("Tags", style="dim")
|
|
29
|
+
|
|
30
|
+
for role in all_roles:
|
|
31
|
+
table.add_row(role.id, role.scope, role.description[:60], ", ".join(role.tags))
|
|
32
|
+
|
|
33
|
+
console.print(table)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.command()
|
|
37
|
+
def role(
|
|
38
|
+
name: str = typer.Argument(..., help="Role ID to output (e.g. qa, paranoid-reviewer, debugger)"),
|
|
39
|
+
):
|
|
40
|
+
"""Output a role's full prompt to stdout.
|
|
41
|
+
|
|
42
|
+
Use this to inject a persona into any agent:
|
|
43
|
+
dotai role qa | pbcopy
|
|
44
|
+
dotai role paranoid-reviewer > /tmp/role.md
|
|
45
|
+
"""
|
|
46
|
+
from ..store import load_config
|
|
47
|
+
from ..roles import load_all_roles
|
|
48
|
+
|
|
49
|
+
config = load_config()
|
|
50
|
+
all_roles = load_all_roles(config)
|
|
51
|
+
|
|
52
|
+
matched = next((r for r in all_roles if r.id == name), None)
|
|
53
|
+
|
|
54
|
+
if not matched:
|
|
55
|
+
console.print(f"[red]Role '{name}' not found.[/red]")
|
|
56
|
+
console.print("Available roles:")
|
|
57
|
+
for r in all_roles:
|
|
58
|
+
console.print(f" {r.id}: {r.description[:60]}")
|
|
59
|
+
raise typer.Exit(1)
|
|
60
|
+
|
|
61
|
+
# print() intentional — stdout for piping, no Rich markup
|
|
62
|
+
print(matched.to_prompt())
|
dotai/cli/rules_cmd.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""CLI commands for rules."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from . import app, console
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command()
|
|
14
|
+
def rules(
|
|
15
|
+
project: Optional[str] = typer.Option(None, "--project", "-p", help="Show rules resolved for a project"),
|
|
16
|
+
all: bool = typer.Option(False, "--all", "-a", help="Show all rules including disabled"),
|
|
17
|
+
):
|
|
18
|
+
"""List rules. Shows resolved active rules by default."""
|
|
19
|
+
from ..store import load_config
|
|
20
|
+
from ..rules import load_all_rules, resolve_rules_for_project
|
|
21
|
+
|
|
22
|
+
config = load_config()
|
|
23
|
+
|
|
24
|
+
if all:
|
|
25
|
+
rule_list = load_all_rules(config)
|
|
26
|
+
elif project:
|
|
27
|
+
rule_list = resolve_rules_for_project(config, project)
|
|
28
|
+
else:
|
|
29
|
+
rule_list = load_all_rules(config)
|
|
30
|
+
|
|
31
|
+
if not rule_list:
|
|
32
|
+
console.print("[dim]No rules found. Use `dotai learn` to add rules.[/dim]")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
# Check project disabled list for display
|
|
36
|
+
disabled_ids: set[str] = set()
|
|
37
|
+
if project:
|
|
38
|
+
proj = config.get_project(project)
|
|
39
|
+
if proj:
|
|
40
|
+
disabled_ids = set(proj.disabled_rules)
|
|
41
|
+
|
|
42
|
+
title = f"Rules (project: {project})" if project else "Rules"
|
|
43
|
+
table = Table(title=title)
|
|
44
|
+
table.add_column("Name", style="bold")
|
|
45
|
+
table.add_column("Status", style="green")
|
|
46
|
+
table.add_column("Scope", style="dim")
|
|
47
|
+
table.add_column("Globs", style="cyan")
|
|
48
|
+
table.add_column("Tags", style="dim")
|
|
49
|
+
table.add_column("Description")
|
|
50
|
+
|
|
51
|
+
for rule in rule_list:
|
|
52
|
+
if rule.id in disabled_ids:
|
|
53
|
+
status = "[red]disabled (project)[/red]"
|
|
54
|
+
elif not rule.enabled:
|
|
55
|
+
status = "[red]disabled[/red]"
|
|
56
|
+
else:
|
|
57
|
+
status = "[green]on[/green]"
|
|
58
|
+
table.add_row(
|
|
59
|
+
rule.id,
|
|
60
|
+
status,
|
|
61
|
+
rule.scope,
|
|
62
|
+
", ".join(rule.globs) if rule.globs else "",
|
|
63
|
+
", ".join(rule.tags),
|
|
64
|
+
rule.description[:50],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
console.print(table)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.command()
|
|
71
|
+
def toggle(
|
|
72
|
+
rule_id: str = typer.Argument(..., help="Rule ID to toggle (e.g. no-useeffect)"),
|
|
73
|
+
project: Optional[str] = typer.Option(None, "--project", "-p", help="Toggle for a specific project only"),
|
|
74
|
+
on: bool = typer.Option(False, "--on", help="Enable the rule"),
|
|
75
|
+
off: bool = typer.Option(False, "--off", help="Disable the rule"),
|
|
76
|
+
):
|
|
77
|
+
"""Enable or disable a rule globally or for a specific project.
|
|
78
|
+
|
|
79
|
+
Globally: dotai toggle no-useeffect --off
|
|
80
|
+
Per project: dotai toggle no-useeffect --off -p my-legacy-app
|
|
81
|
+
"""
|
|
82
|
+
from ..store import load_config
|
|
83
|
+
from ..rules import toggle_rule_global, toggle_rule_for_project
|
|
84
|
+
|
|
85
|
+
if not on and not off:
|
|
86
|
+
console.print("[red]Specify --on or --off[/red]")
|
|
87
|
+
raise typer.Exit(1)
|
|
88
|
+
|
|
89
|
+
config = load_config()
|
|
90
|
+
enabled = on # --on → True, --off → False
|
|
91
|
+
|
|
92
|
+
if project:
|
|
93
|
+
# Disable/enable a global rule for this project only
|
|
94
|
+
ok = toggle_rule_for_project(config, project, rule_id, disabled=not enabled)
|
|
95
|
+
if ok:
|
|
96
|
+
state = "enabled" if enabled else "disabled"
|
|
97
|
+
console.print(f"[green]Rule '{rule_id}' {state} for project '{project}'[/green]")
|
|
98
|
+
else:
|
|
99
|
+
console.print(f"[red]Project '{project}' not found or rule already in that state[/red]")
|
|
100
|
+
raise typer.Exit(1)
|
|
101
|
+
else:
|
|
102
|
+
# Toggle globally in the rule's frontmatter
|
|
103
|
+
rules_dir = config.global_rules_path
|
|
104
|
+
ok = toggle_rule_global(rule_id, rules_dir, enabled)
|
|
105
|
+
if ok:
|
|
106
|
+
state = "enabled" if enabled else "disabled"
|
|
107
|
+
console.print(f"[green]Rule '{rule_id}' {state} globally[/green]")
|
|
108
|
+
else:
|
|
109
|
+
console.print(f"[red]Rule '{rule_id}' not found in {rules_dir}[/red]")
|
|
110
|
+
raise typer.Exit(1)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@app.command()
|
|
114
|
+
def learn(
|
|
115
|
+
title: str = typer.Argument(..., help="Short title for the rule or learning"),
|
|
116
|
+
from_file: Optional[str] = typer.Option(None, "--from-file", "-f", help="Import rule from a file"),
|
|
117
|
+
issue: Optional[str] = typer.Option(None, "--issue", "-i", help="What went wrong (inline mode)"),
|
|
118
|
+
correction: Optional[str] = typer.Option(None, "--correction", "-c", help="What to do instead (inline mode)"),
|
|
119
|
+
description: Optional[str] = typer.Option(None, "--description", "-d", help="One-line description"),
|
|
120
|
+
globs: Optional[str] = typer.Option(None, "--globs", "-g", help="File patterns this rule applies to (e.g. '*.tsx,*.ts')"),
|
|
121
|
+
tags: Optional[str] = typer.Option(None, "--tags", "-t", help="Comma-separated tags"),
|
|
122
|
+
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project name (writes to project rules/)"),
|
|
123
|
+
):
|
|
124
|
+
"""Add a rule from a file or record an inline learning.
|
|
125
|
+
|
|
126
|
+
From file (creates structured rule in ~/.ai/rules/):
|
|
127
|
+
dotai learn "no-useEffect" -f react-rule.md -g "*.tsx,*.ts"
|
|
128
|
+
|
|
129
|
+
Inline learning (appends to rules.md):
|
|
130
|
+
dotai learn "title" -i "what went wrong" -c "what to do instead"
|
|
131
|
+
"""
|
|
132
|
+
from ..store import load_config
|
|
133
|
+
|
|
134
|
+
if not from_file and not (issue and correction):
|
|
135
|
+
console.print("[red]Provide --from-file, or --issue and --correction[/red]")
|
|
136
|
+
raise typer.Exit(1)
|
|
137
|
+
|
|
138
|
+
config = load_config()
|
|
139
|
+
|
|
140
|
+
if from_file:
|
|
141
|
+
from ..rules import create_rule_from_file
|
|
142
|
+
|
|
143
|
+
source = Path(from_file).expanduser().resolve()
|
|
144
|
+
if not source.exists():
|
|
145
|
+
console.print(f"[red]File not found: {source}[/red]")
|
|
146
|
+
raise typer.Exit(1)
|
|
147
|
+
|
|
148
|
+
# Determine target rules directory
|
|
149
|
+
if project:
|
|
150
|
+
proj = config.get_project(project)
|
|
151
|
+
if not proj:
|
|
152
|
+
console.print(f"[red]Project '{project}' not found[/red]")
|
|
153
|
+
raise typer.Exit(1)
|
|
154
|
+
rules_dir = proj.rules_path
|
|
155
|
+
else:
|
|
156
|
+
rules_dir = config.global_rules_path
|
|
157
|
+
|
|
158
|
+
glob_list = [g.strip() for g in globs.split(",")] if globs else None
|
|
159
|
+
tag_list = [t.strip() for t in tags.split(",")] if tags else None
|
|
160
|
+
|
|
161
|
+
dest = create_rule_from_file(
|
|
162
|
+
source_path=source,
|
|
163
|
+
name=title,
|
|
164
|
+
dest_dir=rules_dir,
|
|
165
|
+
description=description,
|
|
166
|
+
globs=glob_list,
|
|
167
|
+
tags=tag_list,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
console.print(f"[green]Rule created:[/green] {title}")
|
|
171
|
+
console.print(f" [dim]{dest}[/dim]")
|
|
172
|
+
|
|
173
|
+
# Show what was detected
|
|
174
|
+
from ..rules import parse_rule_file
|
|
175
|
+
rule = parse_rule_file(dest)
|
|
176
|
+
if rule:
|
|
177
|
+
if rule.tags:
|
|
178
|
+
console.print(f" Tags: {', '.join(rule.tags)}")
|
|
179
|
+
if rule.globs:
|
|
180
|
+
console.print(f" Globs: {', '.join(rule.globs)}")
|
|
181
|
+
console.print(f" Description: {rule.description}")
|
|
182
|
+
else:
|
|
183
|
+
# Inline learning — append to rules.md
|
|
184
|
+
if project:
|
|
185
|
+
proj = config.get_project(project)
|
|
186
|
+
if not proj:
|
|
187
|
+
console.print(f"[red]Project '{project}' not found[/red]")
|
|
188
|
+
raise typer.Exit(1)
|
|
189
|
+
rules_path = proj.full_ai_path / "rules.md"
|
|
190
|
+
else:
|
|
191
|
+
rules_path = config.global_ai_dir / "rules.md"
|
|
192
|
+
|
|
193
|
+
rules_path.parent.mkdir(parents=True, exist_ok=True)
|
|
194
|
+
|
|
195
|
+
entry = f"\n### {datetime.now().strftime('%Y-%m-%d')}: {title}\n"
|
|
196
|
+
entry += f"**Issue:** {issue}\n"
|
|
197
|
+
entry += f"**Correction:** {correction}\n"
|
|
198
|
+
|
|
199
|
+
with open(rules_path, "a") as f:
|
|
200
|
+
f.write(entry)
|
|
201
|
+
|
|
202
|
+
console.print(f"[green]Recorded learning:[/green] {title}")
|