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.
Files changed (88) hide show
  1. agent_knowledge/__init__.py +3 -0
  2. agent_knowledge/__main__.py +3 -0
  3. agent_knowledge/assets/__init__.py +0 -0
  4. agent_knowledge/assets/claude/global.md +44 -0
  5. agent_knowledge/assets/claude/project-template.md +46 -0
  6. agent_knowledge/assets/claude/scripts/install.sh +85 -0
  7. agent_knowledge/assets/commands/doctor.md +21 -0
  8. agent_knowledge/assets/commands/global-knowledge-sync.md +27 -0
  9. agent_knowledge/assets/commands/graphify-sync.md +26 -0
  10. agent_knowledge/assets/commands/knowledge-sync.md +26 -0
  11. agent_knowledge/assets/commands/ship.md +29 -0
  12. agent_knowledge/assets/rules/generate-architecture-doc.mdc +87 -0
  13. agent_knowledge/assets/rules/history-backfill.mdc +67 -0
  14. agent_knowledge/assets/rules/memory-bootstrap.mdc +53 -0
  15. agent_knowledge/assets/rules/memory-writeback.mdc +90 -0
  16. agent_knowledge/assets/rules/shared-memory.mdc +102 -0
  17. agent_knowledge/assets/rules/workflow-orchestration.mdc +93 -0
  18. agent_knowledge/assets/rules-global/action-first.mdc +26 -0
  19. agent_knowledge/assets/rules-global/no-icons-emojis.mdc +16 -0
  20. agent_knowledge/assets/rules-global/no-unsolicited-docs.mdc +20 -0
  21. agent_knowledge/assets/scripts/bootstrap-memory-tree.sh +389 -0
  22. agent_knowledge/assets/scripts/compact-memory.sh +191 -0
  23. agent_knowledge/assets/scripts/doctor.sh +137 -0
  24. agent_knowledge/assets/scripts/global-knowledge-sync.sh +372 -0
  25. agent_knowledge/assets/scripts/graphify-sync.sh +397 -0
  26. agent_knowledge/assets/scripts/import-agent-history.sh +706 -0
  27. agent_knowledge/assets/scripts/install-project-links.sh +258 -0
  28. agent_knowledge/assets/scripts/lib/knowledge-common.sh +875 -0
  29. agent_knowledge/assets/scripts/measure-token-savings.py +540 -0
  30. agent_knowledge/assets/scripts/ship.sh +256 -0
  31. agent_knowledge/assets/scripts/update-knowledge.sh +341 -0
  32. agent_knowledge/assets/scripts/validate-knowledge.sh +265 -0
  33. agent_knowledge/assets/skills/decision-recording/SKILL.md +124 -0
  34. agent_knowledge/assets/skills/history-backfill/SKILL.md +115 -0
  35. agent_knowledge/assets/skills/memory-compaction/SKILL.md +115 -0
  36. agent_knowledge/assets/skills/memory-management/SKILL.md +134 -0
  37. agent_knowledge/assets/skills/project-ontology-bootstrap/SKILL.md +173 -0
  38. agent_knowledge/assets/skills/session-management/SKILL.md +116 -0
  39. agent_knowledge/assets/skills-cursor/create-rule/SKILL.md +164 -0
  40. agent_knowledge/assets/skills-cursor/create-skill/SKILL.md +498 -0
  41. agent_knowledge/assets/skills-cursor/create-subagent/SKILL.md +225 -0
  42. agent_knowledge/assets/skills-cursor/migrate-to-skills/SKILL.md +134 -0
  43. agent_knowledge/assets/skills-cursor/shell/SKILL.md +24 -0
  44. agent_knowledge/assets/skills-cursor/update-cursor-settings/SKILL.md +122 -0
  45. agent_knowledge/assets/templates/dashboards/project-overview.template.md +24 -0
  46. agent_knowledge/assets/templates/dashboards/session-rollup.template.md +23 -0
  47. agent_knowledge/assets/templates/hooks/hooks.json.template +11 -0
  48. agent_knowledge/assets/templates/integrations/claude/CLAUDE.md +7 -0
  49. agent_knowledge/assets/templates/integrations/codex/AGENTS.md +7 -0
  50. agent_knowledge/assets/templates/integrations/cursor/agent-knowledge.mdc +11 -0
  51. agent_knowledge/assets/templates/integrations/cursor/hooks.json +11 -0
  52. agent_knowledge/assets/templates/memory/MEMORY.root.template.md +36 -0
  53. agent_knowledge/assets/templates/memory/branch.template.md +33 -0
  54. agent_knowledge/assets/templates/memory/decision.template.md +33 -0
  55. agent_knowledge/assets/templates/memory/profile.hybrid.yaml +16 -0
  56. agent_knowledge/assets/templates/memory/profile.ml-platform.yaml +18 -0
  57. agent_knowledge/assets/templates/memory/profile.robotics.yaml +19 -0
  58. agent_knowledge/assets/templates/memory/profile.web-app.yaml +16 -0
  59. agent_knowledge/assets/templates/portfolio/.obsidian/README.md +21 -0
  60. agent_knowledge/assets/templates/portfolio/.obsidian/app.json +5 -0
  61. agent_knowledge/assets/templates/portfolio/.obsidian/core-plugins.json +7 -0
  62. agent_knowledge/assets/templates/project/.agent-project.yaml +36 -0
  63. agent_knowledge/assets/templates/project/.agentknowledgeignore +10 -0
  64. agent_knowledge/assets/templates/project/AGENTS.md +87 -0
  65. agent_knowledge/assets/templates/project/agent-knowledge/.obsidian/README.md +23 -0
  66. agent_knowledge/assets/templates/project/agent-knowledge/.obsidian/app.json +5 -0
  67. agent_knowledge/assets/templates/project/agent-knowledge/.obsidian/core-plugins.json +7 -0
  68. agent_knowledge/assets/templates/project/agent-knowledge/Evidence/README.md +34 -0
  69. agent_knowledge/assets/templates/project/agent-knowledge/Evidence/imports/README.md +29 -0
  70. agent_knowledge/assets/templates/project/agent-knowledge/Evidence/raw/README.md +25 -0
  71. agent_knowledge/assets/templates/project/agent-knowledge/Memory/MEMORY.md +37 -0
  72. agent_knowledge/assets/templates/project/agent-knowledge/Memory/decisions/decisions.md +31 -0
  73. agent_knowledge/assets/templates/project/agent-knowledge/Outputs/README.md +24 -0
  74. agent_knowledge/assets/templates/project/agent-knowledge/STATUS.md +43 -0
  75. agent_knowledge/assets/templates/project/agent-knowledge/Sessions/README.md +21 -0
  76. agent_knowledge/assets/templates/project/agent-knowledge/Templates/README.md +19 -0
  77. agent_knowledge/assets/templates/project/gitignore.agent-knowledge +13 -0
  78. agent_knowledge/cli.py +457 -0
  79. agent_knowledge/runtime/__init__.py +0 -0
  80. agent_knowledge/runtime/integrations.py +154 -0
  81. agent_knowledge/runtime/paths.py +46 -0
  82. agent_knowledge/runtime/shell.py +22 -0
  83. agent_knowledge/runtime/sync.py +255 -0
  84. agent_knowledge_cli-0.1.2.dist-info/METADATA +155 -0
  85. agent_knowledge_cli-0.1.2.dist-info/RECORD +88 -0
  86. agent_knowledge_cli-0.1.2.dist-info/WHEEL +4 -0
  87. agent_knowledge_cli-0.1.2.dist-info/entry_points.txt +2 -0
  88. 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