agent-knowledge-cli 0.1.2__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.
- agent_knowledge/__init__.py +3 -0
- agent_knowledge/__main__.py +3 -0
- agent_knowledge/assets/__init__.py +0 -0
- agent_knowledge/assets/claude/global.md +44 -0
- agent_knowledge/assets/claude/project-template.md +46 -0
- agent_knowledge/assets/claude/scripts/install.sh +85 -0
- agent_knowledge/assets/commands/doctor.md +21 -0
- agent_knowledge/assets/commands/global-knowledge-sync.md +27 -0
- agent_knowledge/assets/commands/graphify-sync.md +26 -0
- agent_knowledge/assets/commands/knowledge-sync.md +26 -0
- agent_knowledge/assets/commands/ship.md +29 -0
- agent_knowledge/assets/rules/generate-architecture-doc.mdc +87 -0
- agent_knowledge/assets/rules/history-backfill.mdc +67 -0
- agent_knowledge/assets/rules/memory-bootstrap.mdc +53 -0
- agent_knowledge/assets/rules/memory-writeback.mdc +90 -0
- agent_knowledge/assets/rules/shared-memory.mdc +102 -0
- agent_knowledge/assets/rules/workflow-orchestration.mdc +93 -0
- agent_knowledge/assets/rules-global/action-first.mdc +26 -0
- agent_knowledge/assets/rules-global/no-icons-emojis.mdc +16 -0
- agent_knowledge/assets/rules-global/no-unsolicited-docs.mdc +20 -0
- agent_knowledge/assets/scripts/bootstrap-memory-tree.sh +389 -0
- agent_knowledge/assets/scripts/compact-memory.sh +191 -0
- agent_knowledge/assets/scripts/doctor.sh +137 -0
- agent_knowledge/assets/scripts/global-knowledge-sync.sh +372 -0
- agent_knowledge/assets/scripts/graphify-sync.sh +397 -0
- agent_knowledge/assets/scripts/import-agent-history.sh +706 -0
- agent_knowledge/assets/scripts/install-project-links.sh +258 -0
- agent_knowledge/assets/scripts/lib/knowledge-common.sh +875 -0
- agent_knowledge/assets/scripts/measure-token-savings.py +540 -0
- agent_knowledge/assets/scripts/ship.sh +256 -0
- agent_knowledge/assets/scripts/update-knowledge.sh +341 -0
- agent_knowledge/assets/scripts/validate-knowledge.sh +265 -0
- agent_knowledge/assets/skills/decision-recording/SKILL.md +124 -0
- agent_knowledge/assets/skills/history-backfill/SKILL.md +115 -0
- agent_knowledge/assets/skills/memory-compaction/SKILL.md +115 -0
- agent_knowledge/assets/skills/memory-management/SKILL.md +134 -0
- agent_knowledge/assets/skills/project-ontology-bootstrap/SKILL.md +173 -0
- agent_knowledge/assets/skills/session-management/SKILL.md +116 -0
- agent_knowledge/assets/skills-cursor/create-rule/SKILL.md +164 -0
- agent_knowledge/assets/skills-cursor/create-skill/SKILL.md +498 -0
- agent_knowledge/assets/skills-cursor/create-subagent/SKILL.md +225 -0
- agent_knowledge/assets/skills-cursor/migrate-to-skills/SKILL.md +134 -0
- agent_knowledge/assets/skills-cursor/shell/SKILL.md +24 -0
- agent_knowledge/assets/skills-cursor/update-cursor-settings/SKILL.md +122 -0
- agent_knowledge/assets/templates/dashboards/project-overview.template.md +24 -0
- agent_knowledge/assets/templates/dashboards/session-rollup.template.md +23 -0
- agent_knowledge/assets/templates/hooks/hooks.json.template +11 -0
- agent_knowledge/assets/templates/integrations/claude/CLAUDE.md +7 -0
- agent_knowledge/assets/templates/integrations/codex/AGENTS.md +7 -0
- agent_knowledge/assets/templates/integrations/cursor/agent-knowledge.mdc +11 -0
- agent_knowledge/assets/templates/integrations/cursor/hooks.json +11 -0
- agent_knowledge/assets/templates/memory/MEMORY.root.template.md +36 -0
- agent_knowledge/assets/templates/memory/branch.template.md +33 -0
- agent_knowledge/assets/templates/memory/decision.template.md +33 -0
- agent_knowledge/assets/templates/memory/profile.hybrid.yaml +16 -0
- agent_knowledge/assets/templates/memory/profile.ml-platform.yaml +18 -0
- agent_knowledge/assets/templates/memory/profile.robotics.yaml +19 -0
- agent_knowledge/assets/templates/memory/profile.web-app.yaml +16 -0
- agent_knowledge/assets/templates/portfolio/.obsidian/README.md +21 -0
- agent_knowledge/assets/templates/portfolio/.obsidian/app.json +5 -0
- agent_knowledge/assets/templates/portfolio/.obsidian/core-plugins.json +7 -0
- agent_knowledge/assets/templates/project/.agent-project.yaml +36 -0
- agent_knowledge/assets/templates/project/.agentknowledgeignore +10 -0
- agent_knowledge/assets/templates/project/AGENTS.md +87 -0
- agent_knowledge/assets/templates/project/agent-knowledge/.obsidian/README.md +23 -0
- agent_knowledge/assets/templates/project/agent-knowledge/.obsidian/app.json +5 -0
- agent_knowledge/assets/templates/project/agent-knowledge/.obsidian/core-plugins.json +7 -0
- agent_knowledge/assets/templates/project/agent-knowledge/Evidence/README.md +34 -0
- agent_knowledge/assets/templates/project/agent-knowledge/Evidence/imports/README.md +29 -0
- agent_knowledge/assets/templates/project/agent-knowledge/Evidence/raw/README.md +25 -0
- agent_knowledge/assets/templates/project/agent-knowledge/Memory/MEMORY.md +37 -0
- agent_knowledge/assets/templates/project/agent-knowledge/Memory/decisions/decisions.md +31 -0
- agent_knowledge/assets/templates/project/agent-knowledge/Outputs/README.md +24 -0
- agent_knowledge/assets/templates/project/agent-knowledge/STATUS.md +43 -0
- agent_knowledge/assets/templates/project/agent-knowledge/Sessions/README.md +21 -0
- agent_knowledge/assets/templates/project/agent-knowledge/Templates/README.md +19 -0
- agent_knowledge/assets/templates/project/gitignore.agent-knowledge +13 -0
- agent_knowledge/cli.py +457 -0
- agent_knowledge/runtime/__init__.py +0 -0
- agent_knowledge/runtime/integrations.py +154 -0
- agent_knowledge/runtime/paths.py +46 -0
- agent_knowledge/runtime/shell.py +22 -0
- agent_knowledge/runtime/sync.py +255 -0
- agent_knowledge_cli-0.1.2.dist-info/METADATA +155 -0
- agent_knowledge_cli-0.1.2.dist-info/RECORD +88 -0
- agent_knowledge_cli-0.1.2.dist-info/WHEEL +4 -0
- agent_knowledge_cli-0.1.2.dist-info/entry_points.txt +2 -0
- agent_knowledge_cli-0.1.2.dist-info/licenses/LICENSE +21 -0
agent_knowledge/cli.py
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
"""CLI entry point for agent-knowledge."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from agent_knowledge import __version__
|
|
13
|
+
from agent_knowledge.runtime.paths import get_assets_dir
|
|
14
|
+
from agent_knowledge.runtime.shell import run_bash_script, run_python_script
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _add_common_flags(
|
|
18
|
+
args: list[str],
|
|
19
|
+
*,
|
|
20
|
+
dry_run: bool = False,
|
|
21
|
+
json_mode: bool = False,
|
|
22
|
+
force: bool = False,
|
|
23
|
+
) -> list[str]:
|
|
24
|
+
if dry_run:
|
|
25
|
+
args.append("--dry-run")
|
|
26
|
+
if json_mode:
|
|
27
|
+
args.append("--json")
|
|
28
|
+
if force:
|
|
29
|
+
args.append("--force")
|
|
30
|
+
return args
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@click.group()
|
|
34
|
+
@click.version_option(version=__version__, prog_name="agent-knowledge")
|
|
35
|
+
def main() -> None:
|
|
36
|
+
"""Adaptive, file-based project knowledge for AI coding agents."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# -- init ------------------------------------------------------------------ #
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@main.command()
|
|
43
|
+
@click.option("--slug", default=None, help="Project slug (default: repo directory name).")
|
|
44
|
+
@click.option("--repo", default=".", type=click.Path(exists=True), help="Project repo path (default: cwd).")
|
|
45
|
+
@click.option("--knowledge-home", default=None, help="Knowledge root (default: $AGENT_KNOWLEDGE_HOME or ~/agent-os/projects).")
|
|
46
|
+
@click.option("--real-path", default=None, help="Explicit external knowledge folder path.")
|
|
47
|
+
@click.option("--no-integrations", is_flag=True, help="Skip auto-detection and installation of tool integrations.")
|
|
48
|
+
@click.option("--dry-run", is_flag=True, help="Preview changes without writing.")
|
|
49
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output JSON only.")
|
|
50
|
+
@click.option("--force", is_flag=True, help="Overwrite existing files.")
|
|
51
|
+
def init(
|
|
52
|
+
slug: str | None,
|
|
53
|
+
repo: str,
|
|
54
|
+
knowledge_home: str | None,
|
|
55
|
+
real_path: str | None,
|
|
56
|
+
no_integrations: bool,
|
|
57
|
+
dry_run: bool,
|
|
58
|
+
json_mode: bool,
|
|
59
|
+
force: bool,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Initialize a project: create knowledge folder, pointer, and metadata.
|
|
62
|
+
|
|
63
|
+
When run with no arguments inside a repo, infers slug from the directory
|
|
64
|
+
name, auto-detects tool integrations, and installs everything needed.
|
|
65
|
+
"""
|
|
66
|
+
from agent_knowledge.runtime.integrations import detect, install_all
|
|
67
|
+
|
|
68
|
+
repo_path = Path(repo).resolve()
|
|
69
|
+
if slug is None:
|
|
70
|
+
slug = _sanitize_slug(repo_path.name)
|
|
71
|
+
|
|
72
|
+
if knowledge_home is None:
|
|
73
|
+
knowledge_home = os.environ.get("AGENT_KNOWLEDGE_HOME")
|
|
74
|
+
|
|
75
|
+
# Core setup: symlink, .agent-project.yaml, AGENTS.md, bootstrap
|
|
76
|
+
args = ["--slug", slug, "--repo", str(repo_path)]
|
|
77
|
+
if knowledge_home:
|
|
78
|
+
args.extend(["--knowledge-home", knowledge_home])
|
|
79
|
+
if real_path:
|
|
80
|
+
args.extend(["--real-path", real_path])
|
|
81
|
+
args.append("--install-hooks")
|
|
82
|
+
_add_common_flags(args, dry_run=dry_run, json_mode=json_mode, force=force)
|
|
83
|
+
rc = run_bash_script("install-project-links.sh", args)
|
|
84
|
+
if rc != 0:
|
|
85
|
+
sys.exit(rc)
|
|
86
|
+
|
|
87
|
+
# Auto-detect and install tool integrations
|
|
88
|
+
if not no_integrations:
|
|
89
|
+
detected = detect(repo_path)
|
|
90
|
+
if not json_mode:
|
|
91
|
+
tools_found = [t for t, v in detected.items() if v]
|
|
92
|
+
click.echo("", err=True)
|
|
93
|
+
click.echo(f"Detected integrations: {', '.join(tools_found) if tools_found else 'none'}", err=True)
|
|
94
|
+
|
|
95
|
+
results = install_all(repo_path, detected, dry_run=dry_run, force=force)
|
|
96
|
+
if not json_mode:
|
|
97
|
+
for tool, actions in results.items():
|
|
98
|
+
click.echo(f" [{tool}]", err=True)
|
|
99
|
+
for action in actions:
|
|
100
|
+
click.echo(action, err=True)
|
|
101
|
+
|
|
102
|
+
if not json_mode:
|
|
103
|
+
prompt = "Read AGENTS.md and ./agent-knowledge/STATUS.md, then onboard this project."
|
|
104
|
+
border = "+" + "-" * (len(prompt) + 2) + "+"
|
|
105
|
+
click.echo("", err=True)
|
|
106
|
+
click.secho("Ready. Open your agent and send:", bold=True, err=True)
|
|
107
|
+
click.echo("", err=True)
|
|
108
|
+
click.secho(border, fg="cyan", err=True)
|
|
109
|
+
click.secho(f"| {prompt} |", fg="cyan", bold=True, err=True)
|
|
110
|
+
click.secho(border, fg="cyan", err=True)
|
|
111
|
+
click.echo("", err=True)
|
|
112
|
+
|
|
113
|
+
_maybe_star()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
_REPO_URL = "https://github.com/robotaitai/agent-knowledge"
|
|
117
|
+
_STAR_MARKER = Path.home() / ".agent-knowledge-starred"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _maybe_star() -> None:
|
|
121
|
+
"""Prompt to star the repo once, then never again. Skips in non-interactive shells."""
|
|
122
|
+
if _STAR_MARKER.exists():
|
|
123
|
+
return
|
|
124
|
+
if not sys.stderr.isatty():
|
|
125
|
+
return
|
|
126
|
+
try:
|
|
127
|
+
click.echo("", err=True)
|
|
128
|
+
if click.confirm(
|
|
129
|
+
click.style("Like agent-knowledge? Star it on GitHub", fg="yellow"),
|
|
130
|
+
default=True,
|
|
131
|
+
err=True,
|
|
132
|
+
):
|
|
133
|
+
import webbrowser
|
|
134
|
+
|
|
135
|
+
webbrowser.open(_REPO_URL)
|
|
136
|
+
except (EOFError, KeyboardInterrupt):
|
|
137
|
+
click.echo("", err=True)
|
|
138
|
+
_STAR_MARKER.touch()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _sanitize_slug(name: str) -> str:
|
|
142
|
+
"""Normalize a directory name into a safe project slug."""
|
|
143
|
+
import re
|
|
144
|
+
slug = name.lower().strip()
|
|
145
|
+
slug = re.sub(r"[^a-z0-9]+", "-", slug)
|
|
146
|
+
return slug.strip("-")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# -- sync ------------------------------------------------------------------ #
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@main.command()
|
|
153
|
+
@click.option("--project", default=".", type=click.Path(exists=True), help="Project repo root.")
|
|
154
|
+
@click.option("--dry-run", is_flag=True, help="Preview changes without writing.")
|
|
155
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output JSON only.")
|
|
156
|
+
def sync(project: str, dry_run: bool, json_mode: bool) -> None:
|
|
157
|
+
"""Sync memory branches, roll up sessions, and extract git evidence.
|
|
158
|
+
|
|
159
|
+
\b
|
|
160
|
+
Steps:
|
|
161
|
+
1. Copy agent_docs/memory/*.md -> agent-knowledge/Memory/ (newer only)
|
|
162
|
+
2. Scan Sessions/ and rebuild Dashboards/session-rollup.md
|
|
163
|
+
3. Extract recent git log into Evidence/raw/git-recent.md
|
|
164
|
+
4. Update last_project_sync in STATUS.md
|
|
165
|
+
"""
|
|
166
|
+
import json as json_mod
|
|
167
|
+
|
|
168
|
+
from agent_knowledge.runtime.sync import run_sync
|
|
169
|
+
|
|
170
|
+
repo_path = Path(project).resolve()
|
|
171
|
+
results = run_sync(repo_path, dry_run=dry_run)
|
|
172
|
+
|
|
173
|
+
if json_mode:
|
|
174
|
+
click.echo(json_mod.dumps({"sync": results}, indent=2))
|
|
175
|
+
else:
|
|
176
|
+
for step, actions in results.items():
|
|
177
|
+
click.echo(f"[{step}]", err=True)
|
|
178
|
+
for action in actions:
|
|
179
|
+
click.echo(action, err=True)
|
|
180
|
+
click.echo("", err=True)
|
|
181
|
+
|
|
182
|
+
if dry_run:
|
|
183
|
+
click.echo("(dry-run -- no changes written)", err=True)
|
|
184
|
+
else:
|
|
185
|
+
click.secho("Sync complete.", bold=True, err=True)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# -- bootstrap ------------------------------------------------------------- #
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@main.command()
|
|
192
|
+
@click.option("--project", default=".", type=click.Path(exists=True), help="Project repo root.")
|
|
193
|
+
@click.option("--profile", default=None, help="Profile hint (web-app, robotics, ml-platform, hybrid). Advisory only.")
|
|
194
|
+
@click.option("--dry-run", is_flag=True, help="Preview changes without writing.")
|
|
195
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output JSON only.")
|
|
196
|
+
@click.option("--force", is_flag=True, help="Overwrite existing files.")
|
|
197
|
+
def bootstrap(
|
|
198
|
+
project: str,
|
|
199
|
+
profile: str | None,
|
|
200
|
+
dry_run: bool,
|
|
201
|
+
json_mode: bool,
|
|
202
|
+
force: bool,
|
|
203
|
+
) -> None:
|
|
204
|
+
"""Bootstrap or repair the project memory tree."""
|
|
205
|
+
args = ["--project", project]
|
|
206
|
+
if profile:
|
|
207
|
+
args.extend(["--profile", profile])
|
|
208
|
+
_add_common_flags(args, dry_run=dry_run, json_mode=json_mode, force=force)
|
|
209
|
+
sys.exit(run_bash_script("bootstrap-memory-tree.sh", args))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# -- import ---------------------------------------------------------------- #
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@main.command(name="import")
|
|
216
|
+
@click.option("--project", default=".", type=click.Path(exists=True), help="Project repo root.")
|
|
217
|
+
@click.option("--dry-run", is_flag=True, help="Preview changes without writing.")
|
|
218
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output JSON only.")
|
|
219
|
+
def import_cmd(project: str, dry_run: bool, json_mode: bool) -> None:
|
|
220
|
+
"""Import repo history and evidence into Evidence/."""
|
|
221
|
+
args = ["--project", project]
|
|
222
|
+
_add_common_flags(args, dry_run=dry_run, json_mode=json_mode)
|
|
223
|
+
sys.exit(run_bash_script("import-agent-history.sh", args))
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# -- update ---------------------------------------------------------------- #
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@main.command()
|
|
230
|
+
@click.option("--project", default=".", type=click.Path(exists=True), help="Project repo root.")
|
|
231
|
+
@click.option("--compact", is_flag=True, help="Run memory compaction after sync.")
|
|
232
|
+
@click.option("--decision-title", default=None, help="Record a decision note with this title.")
|
|
233
|
+
@click.option("--decision-why", default=None, help="Reason for the decision.")
|
|
234
|
+
@click.option("--decision-slug", default=None, help="Custom slug for the decision note.")
|
|
235
|
+
@click.option("--summary-file", default=None, hidden=True, help="Write JSON summary to file.")
|
|
236
|
+
@click.option("--dry-run", is_flag=True, help="Preview changes without writing.")
|
|
237
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output JSON only.")
|
|
238
|
+
def update(
|
|
239
|
+
project: str,
|
|
240
|
+
compact: bool,
|
|
241
|
+
decision_title: str | None,
|
|
242
|
+
decision_why: str | None,
|
|
243
|
+
decision_slug: str | None,
|
|
244
|
+
summary_file: str | None,
|
|
245
|
+
dry_run: bool,
|
|
246
|
+
json_mode: bool,
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Sync project changes into the knowledge tree."""
|
|
249
|
+
args = ["--project", project]
|
|
250
|
+
if compact:
|
|
251
|
+
args.append("--compact")
|
|
252
|
+
if decision_title:
|
|
253
|
+
args.extend(["--decision-title", decision_title])
|
|
254
|
+
if decision_why:
|
|
255
|
+
args.extend(["--decision-why", decision_why])
|
|
256
|
+
if decision_slug:
|
|
257
|
+
args.extend(["--decision-slug", decision_slug])
|
|
258
|
+
if summary_file:
|
|
259
|
+
args.extend(["--summary-file", summary_file])
|
|
260
|
+
_add_common_flags(args, dry_run=dry_run, json_mode=json_mode)
|
|
261
|
+
sys.exit(run_bash_script("update-knowledge.sh", args))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# -- doctor ---------------------------------------------------------------- #
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@main.command()
|
|
268
|
+
@click.option("--project", default=".", type=click.Path(exists=True), help="Project repo root.")
|
|
269
|
+
@click.option("--dry-run", is_flag=True, help="Preview changes without writing.")
|
|
270
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output JSON only.")
|
|
271
|
+
def doctor(project: str, dry_run: bool, json_mode: bool) -> None:
|
|
272
|
+
"""Validate setup, pointer resolution, and note structure."""
|
|
273
|
+
args = ["--project", project]
|
|
274
|
+
_add_common_flags(args, dry_run=dry_run, json_mode=json_mode)
|
|
275
|
+
sys.exit(run_bash_script("doctor.sh", args))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# -- validate -------------------------------------------------------------- #
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@main.command()
|
|
282
|
+
@click.option("--project", default=".", type=click.Path(exists=True), help="Project repo root.")
|
|
283
|
+
@click.option("--dry-run", is_flag=True, help="Preview changes without writing.")
|
|
284
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output JSON only.")
|
|
285
|
+
def validate(project: str, dry_run: bool, json_mode: bool) -> None:
|
|
286
|
+
"""Validate the knowledge layout and operational links."""
|
|
287
|
+
args = ["--project", project]
|
|
288
|
+
_add_common_flags(args, dry_run=dry_run, json_mode=json_mode)
|
|
289
|
+
sys.exit(run_bash_script("validate-knowledge.sh", args))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# -- ship ------------------------------------------------------------------ #
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@main.command()
|
|
296
|
+
@click.option("--project", default=".", type=click.Path(exists=True), help="Project repo root.")
|
|
297
|
+
@click.option("--message", default=None, help="Custom commit message.")
|
|
298
|
+
@click.option("--open-pr", is_flag=True, help="Create a pull request after pushing.")
|
|
299
|
+
@click.option("--dry-run", is_flag=True, help="Preview changes without writing.")
|
|
300
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output JSON only.")
|
|
301
|
+
def ship(
|
|
302
|
+
project: str,
|
|
303
|
+
message: str | None,
|
|
304
|
+
open_pr: bool,
|
|
305
|
+
dry_run: bool,
|
|
306
|
+
json_mode: bool,
|
|
307
|
+
) -> None:
|
|
308
|
+
"""Validate, sync, commit, push, and optionally create a PR."""
|
|
309
|
+
args = ["--project", project]
|
|
310
|
+
if message:
|
|
311
|
+
args.extend(["--message", message])
|
|
312
|
+
if open_pr:
|
|
313
|
+
args.append("--open-pr")
|
|
314
|
+
_add_common_flags(args, dry_run=dry_run, json_mode=json_mode)
|
|
315
|
+
sys.exit(run_bash_script("ship.sh", args))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# -- global-sync ----------------------------------------------------------- #
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@main.command("global-sync")
|
|
322
|
+
@click.option("--project", default=".", type=click.Path(exists=True), help="Project repo root.")
|
|
323
|
+
@click.option("--dry-run", is_flag=True, help="Preview changes without writing.")
|
|
324
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output JSON only.")
|
|
325
|
+
def global_sync(project: str, dry_run: bool, json_mode: bool) -> None:
|
|
326
|
+
"""Import safe local tooling config into the knowledge tree."""
|
|
327
|
+
args = ["--project", project]
|
|
328
|
+
_add_common_flags(args, dry_run=dry_run, json_mode=json_mode)
|
|
329
|
+
sys.exit(run_bash_script("global-knowledge-sync.sh", args))
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# -- graphify-sync --------------------------------------------------------- #
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@main.command("graphify-sync")
|
|
336
|
+
@click.option("--project", default=".", type=click.Path(exists=True), help="Project repo root.")
|
|
337
|
+
@click.option("--source", default=None, help="Override source path for graph artifacts.")
|
|
338
|
+
@click.option("--dry-run", is_flag=True, help="Preview changes without writing.")
|
|
339
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output JSON only.")
|
|
340
|
+
def graphify_sync(
|
|
341
|
+
project: str,
|
|
342
|
+
source: str | None,
|
|
343
|
+
dry_run: bool,
|
|
344
|
+
json_mode: bool,
|
|
345
|
+
) -> None:
|
|
346
|
+
"""Import optional graph/discovery artifacts into Evidence and Outputs."""
|
|
347
|
+
args = ["--project", project]
|
|
348
|
+
if source:
|
|
349
|
+
args.extend(["--source", source])
|
|
350
|
+
_add_common_flags(args, dry_run=dry_run, json_mode=json_mode)
|
|
351
|
+
sys.exit(run_bash_script("graphify-sync.sh", args))
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# -- compact --------------------------------------------------------------- #
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@main.command()
|
|
358
|
+
@click.option("--project", default=".", type=click.Path(exists=True), help="Project repo root.")
|
|
359
|
+
@click.option("--dry-run", is_flag=True, help="Preview changes without writing.")
|
|
360
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output JSON only.")
|
|
361
|
+
def compact(project: str, dry_run: bool, json_mode: bool) -> None:
|
|
362
|
+
"""Compact memory notes conservatively."""
|
|
363
|
+
args = ["--project", project]
|
|
364
|
+
_add_common_flags(args, dry_run=dry_run, json_mode=json_mode)
|
|
365
|
+
sys.exit(run_bash_script("compact-memory.sh", args))
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# -- measure-tokens -------------------------------------------------------- #
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@main.command(
|
|
372
|
+
"measure-tokens",
|
|
373
|
+
context_settings={
|
|
374
|
+
"ignore_unknown_options": True,
|
|
375
|
+
"allow_extra_args": True,
|
|
376
|
+
"allow_interspersed_args": False,
|
|
377
|
+
},
|
|
378
|
+
)
|
|
379
|
+
@click.argument("args", nargs=-1, type=click.UNPROCESSED)
|
|
380
|
+
def measure_tokens(args: tuple[str, ...]) -> None:
|
|
381
|
+
"""Estimate repo-controlled context token savings.
|
|
382
|
+
|
|
383
|
+
\b
|
|
384
|
+
Subcommands: compare, log-run, summarize-log.
|
|
385
|
+
Pass --help after the subcommand for its options:
|
|
386
|
+
agent-knowledge measure-tokens compare --help
|
|
387
|
+
"""
|
|
388
|
+
if not args:
|
|
389
|
+
sys.exit(run_python_script("measure-token-savings.py", ["--help"]))
|
|
390
|
+
sys.exit(run_python_script("measure-token-savings.py", list(args)))
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# -- setup ----------------------------------------------------------------- #
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _link(src: Path, dst: Path, label: str, dry_run: bool) -> None:
|
|
397
|
+
if dst.is_symlink() and dst.resolve() == src.resolve():
|
|
398
|
+
click.echo(f" up to date: {label}", err=True)
|
|
399
|
+
return
|
|
400
|
+
if dry_run:
|
|
401
|
+
click.echo(f" [dry-run] would link: {label}", err=True)
|
|
402
|
+
return
|
|
403
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
404
|
+
if dst.exists() or dst.is_symlink():
|
|
405
|
+
dst.unlink() if dst.is_file() or dst.is_symlink() else None
|
|
406
|
+
dst.symlink_to(src)
|
|
407
|
+
click.echo(f" linked: {label}", err=True)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@main.command()
|
|
411
|
+
@click.option("--dry-run", is_flag=True, help="Preview changes without writing.")
|
|
412
|
+
def setup(dry_run: bool) -> None:
|
|
413
|
+
"""Install global Cursor rules, skills, and Claude config into your home directory."""
|
|
414
|
+
assets = get_assets_dir()
|
|
415
|
+
home = Path.home()
|
|
416
|
+
|
|
417
|
+
click.echo("agent-knowledge: setting up global config", err=True)
|
|
418
|
+
if dry_run:
|
|
419
|
+
click.echo("(dry-run mode)", err=True)
|
|
420
|
+
click.echo("", err=True)
|
|
421
|
+
|
|
422
|
+
# Cursor rules
|
|
423
|
+
rules_dst = home / ".cursor" / "rules"
|
|
424
|
+
rules_dst.mkdir(parents=True, exist_ok=True)
|
|
425
|
+
click.echo("[cursor rules -> ~/.cursor/rules/]", err=True)
|
|
426
|
+
for src in sorted((assets / "rules-global").glob("*.mdc")):
|
|
427
|
+
_link(src, rules_dst / src.name, src.name, dry_run)
|
|
428
|
+
click.echo("", err=True)
|
|
429
|
+
|
|
430
|
+
# Skills
|
|
431
|
+
skills_dst = home / ".cursor" / "skills"
|
|
432
|
+
skills_dst.mkdir(parents=True, exist_ok=True)
|
|
433
|
+
click.echo("[skills -> ~/.cursor/skills/]", err=True)
|
|
434
|
+
for src in sorted((assets / "skills").iterdir()):
|
|
435
|
+
if src.is_dir():
|
|
436
|
+
_link(src, skills_dst / src.name, src.name, dry_run)
|
|
437
|
+
click.echo("", err=True)
|
|
438
|
+
|
|
439
|
+
# Cursor-specific skills
|
|
440
|
+
skills_cursor_dst = home / ".cursor" / "skills-cursor"
|
|
441
|
+
skills_cursor_dst.mkdir(parents=True, exist_ok=True)
|
|
442
|
+
click.echo("[skills-cursor -> ~/.cursor/skills-cursor/]", err=True)
|
|
443
|
+
for src in sorted((assets / "skills-cursor").iterdir()):
|
|
444
|
+
if src.is_dir():
|
|
445
|
+
_link(src, skills_cursor_dst / src.name, src.name, dry_run)
|
|
446
|
+
click.echo("", err=True)
|
|
447
|
+
|
|
448
|
+
# Claude Code
|
|
449
|
+
claude_install = assets / "claude" / "scripts" / "install.sh"
|
|
450
|
+
click.echo("[claude code -> ~/.claude/CLAUDE.md]", err=True)
|
|
451
|
+
if dry_run:
|
|
452
|
+
click.echo(" [dry-run] would run: claude/scripts/install.sh", err=True)
|
|
453
|
+
elif claude_install.is_file():
|
|
454
|
+
subprocess.run(["bash", str(claude_install)], check=False)
|
|
455
|
+
click.echo("", err=True)
|
|
456
|
+
|
|
457
|
+
click.echo("Done.", err=True)
|
|
File without changes
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Auto-detect and install tool integrations (Cursor, Claude, Codex)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .paths import get_assets_dir
|
|
9
|
+
|
|
10
|
+
TOOLS = ("cursor", "claude", "codex")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def detect(repo: Path) -> dict[str, bool]:
|
|
14
|
+
"""Return which tools are detected in the repo."""
|
|
15
|
+
return {
|
|
16
|
+
"cursor": (repo / ".cursor").is_dir(),
|
|
17
|
+
"claude": (repo / ".claude").is_dir() or (repo / "CLAUDE.md").is_file(),
|
|
18
|
+
"codex": (repo / ".codex").is_dir(),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _copy_template(src: Path, dst: Path, replacements: dict[str, str], *, force: bool = False) -> str:
|
|
23
|
+
"""Copy a template file with placeholder substitutions. Returns action taken."""
|
|
24
|
+
if dst.exists() and not force:
|
|
25
|
+
return "exists"
|
|
26
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
content = src.read_text()
|
|
28
|
+
for placeholder, value in replacements.items():
|
|
29
|
+
content = content.replace(placeholder, value)
|
|
30
|
+
dst.write_text(content)
|
|
31
|
+
return "created" if not dst.exists() else "updated"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_CURSOR_RULE = """\
|
|
35
|
+
---
|
|
36
|
+
description: Agent knowledge system -- read on every session
|
|
37
|
+
alwaysApply: true
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
This project uses agent-knowledge for persistent memory.
|
|
41
|
+
|
|
42
|
+
On session start:
|
|
43
|
+
1. Read `./agent-knowledge/STATUS.md`
|
|
44
|
+
2. If `onboarding: pending`, read `AGENTS.md` and follow the First-Time Onboarding instructions
|
|
45
|
+
3. If `onboarding: complete`, read `./agent-knowledge/Memory/MEMORY.md` for project context
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def install_cursor(repo: Path, *, dry_run: bool = False, force: bool = False) -> list[str]:
|
|
50
|
+
"""Install Cursor hooks and rules integration."""
|
|
51
|
+
assets = get_assets_dir()
|
|
52
|
+
actions = []
|
|
53
|
+
repo_abs = str(repo.resolve())
|
|
54
|
+
|
|
55
|
+
# Hooks
|
|
56
|
+
hooks_src = assets / "templates" / "integrations" / "cursor" / "hooks.json"
|
|
57
|
+
hooks_dst = repo / ".cursor" / "hooks.json"
|
|
58
|
+
if hooks_dst.exists() and not force:
|
|
59
|
+
actions.append(" exists: .cursor/hooks.json")
|
|
60
|
+
elif dry_run:
|
|
61
|
+
actions.append(" [dry-run] would create: .cursor/hooks.json")
|
|
62
|
+
else:
|
|
63
|
+
hooks_dst.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
content = hooks_src.read_text().replace("<repo-path>", repo_abs)
|
|
65
|
+
hooks_dst.write_text(content)
|
|
66
|
+
actions.append(" created: .cursor/hooks.json")
|
|
67
|
+
|
|
68
|
+
# Rule
|
|
69
|
+
rule_dst = repo / ".cursor" / "rules" / "agent-knowledge.mdc"
|
|
70
|
+
if rule_dst.exists() and not force:
|
|
71
|
+
actions.append(" exists: .cursor/rules/agent-knowledge.mdc")
|
|
72
|
+
elif dry_run:
|
|
73
|
+
actions.append(" [dry-run] would create: .cursor/rules/agent-knowledge.mdc")
|
|
74
|
+
else:
|
|
75
|
+
rule_dst.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
rule_dst.write_text(_CURSOR_RULE)
|
|
77
|
+
actions.append(" created: .cursor/rules/agent-knowledge.mdc")
|
|
78
|
+
|
|
79
|
+
return actions
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def install_claude(repo: Path, *, dry_run: bool = False, force: bool = False) -> list[str]:
|
|
83
|
+
"""Install Claude CLAUDE.md integration."""
|
|
84
|
+
assets = get_assets_dir()
|
|
85
|
+
actions = []
|
|
86
|
+
|
|
87
|
+
src = assets / "templates" / "integrations" / "claude" / "CLAUDE.md"
|
|
88
|
+
dst = repo / "CLAUDE.md"
|
|
89
|
+
|
|
90
|
+
if dst.exists() and not force:
|
|
91
|
+
actions.append(f" exists: CLAUDE.md")
|
|
92
|
+
elif dry_run:
|
|
93
|
+
actions.append(f" [dry-run] would create: CLAUDE.md")
|
|
94
|
+
else:
|
|
95
|
+
shutil.copy2(src, dst)
|
|
96
|
+
actions.append(f" created: CLAUDE.md")
|
|
97
|
+
|
|
98
|
+
return actions
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def install_codex(repo: Path, *, dry_run: bool = False, force: bool = False) -> list[str]:
|
|
102
|
+
"""Install Codex .codex/AGENTS.md integration."""
|
|
103
|
+
assets = get_assets_dir()
|
|
104
|
+
actions = []
|
|
105
|
+
|
|
106
|
+
src = assets / "templates" / "integrations" / "codex" / "AGENTS.md"
|
|
107
|
+
dst = repo / ".codex" / "AGENTS.md"
|
|
108
|
+
|
|
109
|
+
if dst.exists() and not force:
|
|
110
|
+
actions.append(f" exists: .codex/AGENTS.md")
|
|
111
|
+
elif dry_run:
|
|
112
|
+
actions.append(f" [dry-run] would create: .codex/AGENTS.md")
|
|
113
|
+
else:
|
|
114
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
shutil.copy2(src, dst)
|
|
116
|
+
actions.append(f" created: .codex/AGENTS.md")
|
|
117
|
+
|
|
118
|
+
return actions
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
_INSTALLERS = {
|
|
122
|
+
"cursor": install_cursor,
|
|
123
|
+
"claude": install_claude,
|
|
124
|
+
"codex": install_codex,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def install_all(
|
|
129
|
+
repo: Path,
|
|
130
|
+
detected: dict[str, bool],
|
|
131
|
+
*,
|
|
132
|
+
dry_run: bool = False,
|
|
133
|
+
force: bool = False,
|
|
134
|
+
) -> dict[str, list[str]]:
|
|
135
|
+
"""Install bridge files for detected integrations.
|
|
136
|
+
|
|
137
|
+
Cursor is always installed (hooks + rule) because it is the primary agent
|
|
138
|
+
IDE and the hooks/rules have no effect when Cursor is not in use.
|
|
139
|
+
|
|
140
|
+
Claude and Codex bridges are only installed when their marker directories
|
|
141
|
+
(.claude/ or .codex/) are detected, to avoid polluting repos that don't
|
|
142
|
+
use those tools.
|
|
143
|
+
"""
|
|
144
|
+
results: dict[str, list[str]] = {}
|
|
145
|
+
|
|
146
|
+
# Cursor: always install -- hooks/rules are inert outside Cursor
|
|
147
|
+
results["cursor"] = _INSTALLERS["cursor"](repo, dry_run=dry_run, force=force)
|
|
148
|
+
|
|
149
|
+
# Claude / Codex: install only when detected
|
|
150
|
+
for tool in ("claude", "codex"):
|
|
151
|
+
if detected.get(tool, False):
|
|
152
|
+
results[tool] = _INSTALLERS[tool](repo, dry_run=dry_run, force=force)
|
|
153
|
+
|
|
154
|
+
return results
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Asset and path resolution for the agent-knowledge package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_cached_assets_dir: Path | None = None
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_assets_dir() -> Path:
|
|
11
|
+
"""Return the root of the bundled assets directory.
|
|
12
|
+
|
|
13
|
+
When installed via pip, assets live under agent_knowledge/assets/.
|
|
14
|
+
When running from a repo checkout (editable install), falls back to the
|
|
15
|
+
repository root where scripts/, templates/, etc. live directly.
|
|
16
|
+
"""
|
|
17
|
+
global _cached_assets_dir
|
|
18
|
+
if _cached_assets_dir is not None:
|
|
19
|
+
return _cached_assets_dir
|
|
20
|
+
|
|
21
|
+
marker = Path("scripts", "lib", "knowledge-common.sh")
|
|
22
|
+
|
|
23
|
+
# Installed package: assets/ is a sibling of runtime/
|
|
24
|
+
package_assets = Path(__file__).resolve().parent.parent / "assets"
|
|
25
|
+
if (package_assets / marker).is_file():
|
|
26
|
+
_cached_assets_dir = package_assets
|
|
27
|
+
return _cached_assets_dir
|
|
28
|
+
|
|
29
|
+
# Dev fallback: repo_root/assets/ (src/agent_knowledge/runtime -> 4 levels up)
|
|
30
|
+
repo_assets = Path(__file__).resolve().parent.parent.parent.parent / "assets"
|
|
31
|
+
if (repo_assets / marker).is_file():
|
|
32
|
+
_cached_assets_dir = repo_assets
|
|
33
|
+
return _cached_assets_dir
|
|
34
|
+
|
|
35
|
+
raise FileNotFoundError(
|
|
36
|
+
"Cannot locate agent-knowledge assets. "
|
|
37
|
+
"Ensure the package is installed correctly or you are running from the repo checkout."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_script(name: str) -> Path:
|
|
42
|
+
"""Return the path to a bundled script (shell or Python)."""
|
|
43
|
+
path = get_assets_dir() / "scripts" / name
|
|
44
|
+
if not path.is_file():
|
|
45
|
+
raise FileNotFoundError(f"Script not found: {path}")
|
|
46
|
+
return path
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Subprocess wrappers for calling bundled scripts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from .paths import get_script
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_bash_script(name: str, args: list[str]) -> int:
|
|
12
|
+
"""Run a bundled bash script and return its exit code."""
|
|
13
|
+
script = get_script(name)
|
|
14
|
+
result = subprocess.run(["bash", str(script)] + args)
|
|
15
|
+
return result.returncode
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run_python_script(name: str, args: list[str]) -> int:
|
|
19
|
+
"""Run a bundled Python script and return its exit code."""
|
|
20
|
+
script = get_script(name)
|
|
21
|
+
result = subprocess.run([sys.executable, str(script)] + args)
|
|
22
|
+
return result.returncode
|