@event4u/agent-config 1.9.1 → 1.12.0
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.
- package/.agent-src/commands/agent-handoff.md +15 -0
- package/.agent-src/commands/chat-history-clear.md +98 -0
- package/.agent-src/commands/chat-history-resume.md +178 -0
- package/.agent-src/commands/chat-history.md +102 -0
- package/.agent-src/commands/compress.md +9 -9
- package/.agent-src/commands/copilot-agents-init.md +1 -1
- package/.agent-src/commands/fix-portability.md +2 -2
- package/.agent-src/commands/fix-pr-bot-comments.md +1 -1
- package/.agent-src/commands/fix-pr-developer-comments.md +1 -1
- package/.agent-src/commands/fix-references.md +2 -2
- package/.agent-src/commands/mode.md +5 -5
- package/.agent-src/commands/onboard.md +171 -0
- package/.agent-src/commands/roadmap-create.md +7 -2
- package/.agent-src/commands/roadmap-execute.md +2 -2
- package/.agent-src/commands/set-cost-profile.md +101 -0
- package/.agent-src/commands/sync-agent-settings.md +122 -0
- package/.agent-src/commands/sync-gitignore.md +104 -0
- package/.agent-src/commands/tests-execute.md +6 -6
- package/.agent-src/commands/upstream-contribute.md +5 -4
- package/.agent-src/contexts/augment-infrastructure.md +2 -2
- package/.agent-src/contexts/override-system.md +1 -1
- package/.agent-src/contexts/subagent-configuration.md +3 -3
- package/.agent-src/guidelines/agent-infra/layered-settings.md +48 -5
- package/.agent-src/rules/ask-when-uncertain.md +56 -3
- package/.agent-src/rules/augment-portability.md +52 -1
- package/.agent-src/rules/augment-source-of-truth.md +10 -10
- package/.agent-src/rules/chat-history.md +171 -0
- package/.agent-src/rules/docker-commands.md +5 -7
- package/.agent-src/rules/docs-sync.md +13 -9
- package/.agent-src/rules/improve-before-implement.md +2 -0
- package/.agent-src/rules/onboarding-gate.md +94 -0
- package/.agent-src/rules/package-ci-checks.md +6 -5
- package/.agent-src/rules/roadmap-progress-sync.md +24 -13
- package/.agent-src/rules/size-enforcement.md +1 -1
- package/.agent-src/rules/skill-quality.md +1 -1
- package/.agent-src/rules/think-before-action.md +1 -0
- package/.agent-src/scripts/update_roadmap_progress.py +26 -9
- package/.agent-src/skills/check-refs/SKILL.md +1 -1
- package/.agent-src/skills/command-routing/SKILL.md +1 -1
- package/.agent-src/skills/command-writing/SKILL.md +4 -3
- package/.agent-src/skills/file-editor/SKILL.md +2 -2
- package/.agent-src/skills/guideline-writing/SKILL.md +4 -3
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +2 -2
- package/.agent-src/skills/lint-skills/SKILL.md +1 -1
- package/.agent-src/skills/roadmap-management/SKILL.md +13 -10
- package/.agent-src/skills/rtk-output-filtering/SKILL.md +20 -30
- package/.agent-src/skills/rule-writing/SKILL.md +5 -5
- package/.agent-src/skills/terragrunt/SKILL.md +0 -8
- package/.agent-src/skills/upstream-contribute/SKILL.md +5 -4
- package/.agent-src/templates/agent-settings.md +86 -34
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +296 -0
- package/CONTRIBUTING.md +89 -40
- package/README.md +3 -3
- package/composer.json +2 -1
- package/config/agent-settings.template.yml +45 -6
- package/config/gitignore-block.txt +24 -0
- package/config/profiles/balanced.ini +5 -0
- package/config/profiles/full.ini +5 -0
- package/config/profiles/minimal.ini +5 -0
- package/docs/customization.md +30 -4
- package/docs/getting-started.md +52 -3
- package/docs/mcp.md +15 -4
- package/package.json +13 -2
- package/scripts/agent-config +155 -0
- package/scripts/chat_history.py +519 -0
- package/scripts/check_portability.py +151 -1
- package/scripts/install.py +55 -3
- package/scripts/install.sh +50 -21
- package/scripts/mcp_render.py +30 -16
- package/scripts/release.py +588 -0
- package/scripts/sync_agent_settings.py +211 -0
- package/scripts/sync_gitignore.py +226 -0
- package/templates/agent-config-wrapper.sh +47 -0
- package/.agent-src/commands/config-agent-settings.md +0 -126
- package/.agent-src/skills/eloquent/evals/last-run.json +0 -99
|
@@ -248,10 +248,137 @@ def check_file(filepath: Path, patterns: list, allowlist: list) -> List[Violatio
|
|
|
248
248
|
return violations
|
|
249
249
|
|
|
250
250
|
|
|
251
|
+
# ── Task-command detector ───────────────────────────────────────────────
|
|
252
|
+
# Artefact files shipped in the package must not reference `task <name>`
|
|
253
|
+
# invocations (per augment-portability rule). Consumer projects may not
|
|
254
|
+
# have Taskfile installed; agents must use direct script paths instead.
|
|
255
|
+
ARTIFACT_SUBDIRS = ["skills", "rules", "commands", "guidelines", "personas", "contexts"]
|
|
256
|
+
|
|
257
|
+
# Inline code: `task foo` or `task foo-bar` or `task foo:bar`
|
|
258
|
+
_TASK_INLINE_RE = re.compile(r"`task\s+([a-z][a-z0-9:_-]*)`")
|
|
259
|
+
# Code-fence line: "task foo …" (optional leading whitespace)
|
|
260
|
+
_TASK_FENCE_RE = re.compile(r"^\s*task\s+([a-z][a-z0-9:_-]*)\b")
|
|
261
|
+
|
|
262
|
+
# Files that legitimately document the forbidden pattern — they define
|
|
263
|
+
# the rule itself. Any path containing one of these suffixes is skipped
|
|
264
|
+
# by the task-invocation detector (but still scanned for layer 1 + 2).
|
|
265
|
+
_TASK_DETECTOR_SKIP = (
|
|
266
|
+
"rules/augment-portability.md",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def check_task_invocations(filepath: Path) -> List[Violation]:
|
|
271
|
+
"""Flag `task <cmd>` invocations in inline code or code fence lines."""
|
|
272
|
+
violations: List[Violation] = []
|
|
273
|
+
try:
|
|
274
|
+
lines = filepath.read_text(encoding="utf-8").splitlines()
|
|
275
|
+
except Exception:
|
|
276
|
+
return violations
|
|
277
|
+
|
|
278
|
+
in_code_block = False
|
|
279
|
+
for i, line in enumerate(lines, 1):
|
|
280
|
+
stripped = line.strip()
|
|
281
|
+
if stripped.startswith("```"):
|
|
282
|
+
in_code_block = not in_code_block
|
|
283
|
+
continue
|
|
284
|
+
if in_code_block:
|
|
285
|
+
m = _TASK_FENCE_RE.search(line)
|
|
286
|
+
if m:
|
|
287
|
+
violations.append(Violation(
|
|
288
|
+
file=str(filepath), line=i, match=m.group(0).strip(),
|
|
289
|
+
pattern_name="task-invocation", severity="error",
|
|
290
|
+
context=stripped,
|
|
291
|
+
))
|
|
292
|
+
else:
|
|
293
|
+
for m in _TASK_INLINE_RE.finditer(line):
|
|
294
|
+
violations.append(Violation(
|
|
295
|
+
file=str(filepath), line=i, match=m.group(0),
|
|
296
|
+
pattern_name="task-invocation", severity="error",
|
|
297
|
+
context=stripped,
|
|
298
|
+
))
|
|
299
|
+
|
|
300
|
+
return violations
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ── Direct script-invocation detector ───────────────────────────────────
|
|
304
|
+
# Artefacts shipped to consumers must use the `./agent-config` CLI for
|
|
305
|
+
# commands it already covers. Direct `python3 scripts/…` / `bash scripts/…`
|
|
306
|
+
# invocations only work inside the package repo, not in a consumer project
|
|
307
|
+
# where the scripts live under node_modules/ or vendor/.
|
|
308
|
+
#
|
|
309
|
+
# Each entry: (regex, suggested replacement). Patterns match inside inline
|
|
310
|
+
# backticks OR anywhere on a code-fence line.
|
|
311
|
+
_CLI_INVOCATION_MAP: list[tuple[re.Pattern, str]] = [
|
|
312
|
+
(
|
|
313
|
+
re.compile(r"python3\s+scripts/mcp_render\.py\s+--check\b"),
|
|
314
|
+
"./agent-config mcp:check",
|
|
315
|
+
),
|
|
316
|
+
(
|
|
317
|
+
re.compile(r"python3\s+scripts/mcp_render\.py\b"),
|
|
318
|
+
"./agent-config mcp:render",
|
|
319
|
+
),
|
|
320
|
+
(
|
|
321
|
+
re.compile(r"python3\s+\.(?:agent-src|augment)/scripts/update_roadmap_progress\.py\s+--check\b"),
|
|
322
|
+
"./agent-config roadmap:progress-check",
|
|
323
|
+
),
|
|
324
|
+
(
|
|
325
|
+
re.compile(r"python3\s+\.(?:agent-src|augment)/scripts/update_roadmap_progress\.py\b"),
|
|
326
|
+
"./agent-config roadmap:progress",
|
|
327
|
+
),
|
|
328
|
+
(
|
|
329
|
+
re.compile(r"bash\s+scripts/first-run\.sh\b"),
|
|
330
|
+
"./agent-config first-run",
|
|
331
|
+
),
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
# Paths that legitimately document the raw invocations (e.g. the CLI's
|
|
335
|
+
# own help, the portability rule that defines the mapping).
|
|
336
|
+
_CLI_DETECTOR_SKIP = (
|
|
337
|
+
"rules/augment-portability.md",
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def check_cli_invocations(filepath: Path) -> List[Violation]:
|
|
342
|
+
"""Flag direct script invocations that should go through `./agent-config`."""
|
|
343
|
+
violations: List[Violation] = []
|
|
344
|
+
try:
|
|
345
|
+
lines = filepath.read_text(encoding="utf-8").splitlines()
|
|
346
|
+
except Exception:
|
|
347
|
+
return violations
|
|
348
|
+
|
|
349
|
+
in_code_block = False
|
|
350
|
+
for i, line in enumerate(lines, 1):
|
|
351
|
+
stripped = line.strip()
|
|
352
|
+
if stripped.startswith("```"):
|
|
353
|
+
in_code_block = not in_code_block
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
# In prose lines, only check content inside inline `...` spans to
|
|
357
|
+
# avoid false positives in running text. In code fences, check the
|
|
358
|
+
# whole line.
|
|
359
|
+
if in_code_block:
|
|
360
|
+
segments = [line]
|
|
361
|
+
else:
|
|
362
|
+
segments = re.findall(r"`([^`]+)`", line)
|
|
363
|
+
|
|
364
|
+
for seg in segments:
|
|
365
|
+
for pattern, replacement in _CLI_INVOCATION_MAP:
|
|
366
|
+
m = pattern.search(seg)
|
|
367
|
+
if m:
|
|
368
|
+
violations.append(Violation(
|
|
369
|
+
file=str(filepath), line=i, match=m.group(0),
|
|
370
|
+
pattern_name=f"cli-bypass → use `{replacement}`",
|
|
371
|
+
severity="error", context=stripped,
|
|
372
|
+
))
|
|
373
|
+
break # one hit per segment is enough
|
|
374
|
+
|
|
375
|
+
return violations
|
|
376
|
+
|
|
377
|
+
|
|
251
378
|
def scan_all(root: Path) -> tuple[List[Violation], list[str]]:
|
|
252
379
|
"""Scan all package files for portability violations. Returns (violations, detected_identifiers).
|
|
253
380
|
|
|
254
|
-
Scanning has
|
|
381
|
+
Scanning has four layers:
|
|
255
382
|
1. Auto-detected identifiers — applied to `.agent-src/` and
|
|
256
383
|
`.agent-src.uncompressed/` only. The package's own root AGENTS.md and
|
|
257
384
|
copilot-instructions.md are meta docs ABOUT the package, so the
|
|
@@ -259,6 +386,13 @@ def scan_all(root: Path) -> tuple[List[Violation], list[str]]:
|
|
|
259
386
|
2. Optional FORBIDDEN_IDENTIFIERS from AGENT_CONFIG_BLOCKLIST —
|
|
260
387
|
applied to every scanned file, including the root files. Catches
|
|
261
388
|
leakage from renamed or adjacent projects in downstream forks.
|
|
389
|
+
3. `task <name>` invocations inside artefact subdirs — skills, rules,
|
|
390
|
+
commands, guidelines, personas, contexts. These shipped artefacts
|
|
391
|
+
run in consumer projects that may not have Taskfile installed.
|
|
392
|
+
4. Direct script invocations that bypass the `./agent-config` CLI
|
|
393
|
+
(e.g. `python3 scripts/mcp_render.py`). Same artefact-subdir scope
|
|
394
|
+
as layer 3; consumer projects only have the package under
|
|
395
|
+
`node_modules/` or `vendor/`, so the raw paths never resolve.
|
|
262
396
|
"""
|
|
263
397
|
patterns, detected = _compile_patterns(root)
|
|
264
398
|
forbidden = _compile_forbidden_patterns()
|
|
@@ -279,6 +413,22 @@ def scan_all(root: Path) -> tuple[List[Violation], list[str]]:
|
|
|
279
413
|
if f.is_file():
|
|
280
414
|
violations.extend(check_file(f, forbidden, allowlist))
|
|
281
415
|
|
|
416
|
+
# Layer 3 + 4: artefact-subdir-only scans (task invocations, CLI bypass)
|
|
417
|
+
for scan_dir in SCAN_DIRS:
|
|
418
|
+
base = root / scan_dir
|
|
419
|
+
if not base.exists():
|
|
420
|
+
continue
|
|
421
|
+
for sub in ARTIFACT_SUBDIRS:
|
|
422
|
+
d = base / sub
|
|
423
|
+
if not d.exists():
|
|
424
|
+
continue
|
|
425
|
+
for f in sorted(d.rglob("*.md")):
|
|
426
|
+
path_str = str(f)
|
|
427
|
+
if not any(path_str.endswith(skip) for skip in _TASK_DETECTOR_SKIP):
|
|
428
|
+
violations.extend(check_task_invocations(f))
|
|
429
|
+
if not any(path_str.endswith(skip) for skip in _CLI_DETECTOR_SKIP):
|
|
430
|
+
violations.extend(check_cli_invocations(f))
|
|
431
|
+
|
|
282
432
|
return violations, detected
|
|
283
433
|
|
|
284
434
|
|
package/scripts/install.py
CHANGED
|
@@ -223,11 +223,17 @@ def _parse_legacy_settings(text: str) -> "tuple[dict, list]":
|
|
|
223
223
|
return values, unknown
|
|
224
224
|
|
|
225
225
|
|
|
226
|
+
_BARE_ID_RE = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
227
|
+
|
|
228
|
+
|
|
226
229
|
def _yaml_scalar(value: str) -> str:
|
|
227
230
|
"""Format a string value as a YAML scalar with minimal quoting.
|
|
228
231
|
|
|
229
|
-
Booleans and non-negative integers are emitted unquoted
|
|
230
|
-
|
|
232
|
+
Booleans and non-negative integers are emitted unquoted. Bare
|
|
233
|
+
lowercase identifiers (``per_turn``, ``rotate``, ``getters_setters``
|
|
234
|
+
— the shape of profile values and enum-like strings) are emitted
|
|
235
|
+
unquoted so `sync_agent_settings.py` stays idempotent against its
|
|
236
|
+
own output. Everything else is double-quoted.
|
|
231
237
|
"""
|
|
232
238
|
if value == "":
|
|
233
239
|
return '""'
|
|
@@ -235,6 +241,8 @@ def _yaml_scalar(value: str) -> str:
|
|
|
235
241
|
return value
|
|
236
242
|
if value.isdigit():
|
|
237
243
|
return value
|
|
244
|
+
if _BARE_ID_RE.match(value):
|
|
245
|
+
return value
|
|
238
246
|
# Escape backslashes and double-quotes, then wrap
|
|
239
247
|
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
|
|
240
248
|
return f'"{escaped}"'
|
|
@@ -330,6 +338,44 @@ def _migrate_legacy_if_present(project_root: Path, template_body: str) -> "str |
|
|
|
330
338
|
|
|
331
339
|
# --- Bridge generators ---
|
|
332
340
|
|
|
341
|
+
def _parse_profile_ini(path: Path) -> "dict[str, str]":
|
|
342
|
+
"""Parse a simple key=value profile preset (comments start with ; or #)."""
|
|
343
|
+
values: "dict[str, str]" = {}
|
|
344
|
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
345
|
+
line = raw.strip()
|
|
346
|
+
if not line or line.startswith(";") or line.startswith("#"):
|
|
347
|
+
continue
|
|
348
|
+
if "=" not in line:
|
|
349
|
+
continue
|
|
350
|
+
key, _, val = line.partition("=")
|
|
351
|
+
values[key.strip()] = val.strip()
|
|
352
|
+
return values
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
_PLACEHOLDER_RE = re.compile(r"__[A-Z][A-Z0-9_]*__")
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _render_template(template: str, profile_values: "dict[str, str]") -> str:
|
|
359
|
+
"""Substitute __UPPER_KEY__ placeholders using ini values.
|
|
360
|
+
|
|
361
|
+
Each ini key `foo_bar` maps to the `__FOO_BAR__` placeholder. Fails
|
|
362
|
+
if any placeholder remains unfilled — catches typos and missing
|
|
363
|
+
profile entries early.
|
|
364
|
+
"""
|
|
365
|
+
body = template
|
|
366
|
+
for key, value in profile_values.items():
|
|
367
|
+
placeholder = f"__{key.upper()}__"
|
|
368
|
+
if placeholder in body:
|
|
369
|
+
body = body.replace(placeholder, value)
|
|
370
|
+
leftover = sorted(set(_PLACEHOLDER_RE.findall(body)))
|
|
371
|
+
if leftover:
|
|
372
|
+
fail(
|
|
373
|
+
"Template has unfilled placeholders after profile render: "
|
|
374
|
+
+ ", ".join(leftover)
|
|
375
|
+
)
|
|
376
|
+
return body
|
|
377
|
+
|
|
378
|
+
|
|
333
379
|
def ensure_agent_settings(project_root: Path, package_root: Path, profile: str, force: bool) -> None:
|
|
334
380
|
target = project_root / SETTINGS_FILE
|
|
335
381
|
profile_source = package_root / "config" / "profiles" / f"{profile}.ini"
|
|
@@ -343,7 +389,13 @@ def ensure_agent_settings(project_root: Path, package_root: Path, profile: str,
|
|
|
343
389
|
template = template_source.read_text(encoding="utf-8")
|
|
344
390
|
if COST_PROFILE_PLACEHOLDER not in template:
|
|
345
391
|
fail(f"Template is missing placeholder {COST_PROFILE_PLACEHOLDER}")
|
|
346
|
-
|
|
392
|
+
profile_values = _parse_profile_ini(profile_source)
|
|
393
|
+
if profile_values.get("cost_profile") != profile:
|
|
394
|
+
fail(
|
|
395
|
+
f"Profile preset {profile_source.name} has cost_profile="
|
|
396
|
+
f"{profile_values.get('cost_profile')!r} but --profile={profile}"
|
|
397
|
+
)
|
|
398
|
+
template_body = _render_template(template, profile_values)
|
|
347
399
|
|
|
348
400
|
legacy_target = project_root / LEGACY_SETTINGS_FILE
|
|
349
401
|
if legacy_target.is_file() and target.exists():
|
package/scripts/install.sh
CHANGED
|
@@ -19,7 +19,6 @@ set -euo pipefail
|
|
|
19
19
|
|
|
20
20
|
# --- Configuration ---
|
|
21
21
|
COPY_DIRS="rules" # Subdirectories where files must be real copies (space-separated)
|
|
22
|
-
GITIGNORE_MARKER="# event4u/agent-config"
|
|
23
22
|
|
|
24
23
|
# Rules that are internal to the agent-config package and should NOT be shipped to consumers.
|
|
25
24
|
# These are only relevant when developing the agent-config package itself.
|
|
@@ -35,6 +34,7 @@ TARGET_DIR=""
|
|
|
35
34
|
DRY_RUN=false
|
|
36
35
|
VERBOSE=false
|
|
37
36
|
QUIET=false
|
|
37
|
+
SKIP_GITIGNORE=false
|
|
38
38
|
|
|
39
39
|
# --- Logging ---
|
|
40
40
|
log_info() { $QUIET || echo " ✅ $*"; }
|
|
@@ -51,6 +51,7 @@ parse_args() {
|
|
|
51
51
|
--dry-run) DRY_RUN=true; shift ;;
|
|
52
52
|
--verbose) VERBOSE=true; shift ;;
|
|
53
53
|
--quiet) QUIET=true; shift ;;
|
|
54
|
+
--skip-gitignore) SKIP_GITIGNORE=true; shift ;;
|
|
54
55
|
--help|-h) show_help; exit 0 ;;
|
|
55
56
|
*) log_error "Unknown argument: $1"; show_help; exit 1 ;;
|
|
56
57
|
esac
|
|
@@ -103,6 +104,7 @@ Options:
|
|
|
103
104
|
--dry-run Show what would happen without making changes
|
|
104
105
|
--verbose Show detailed output
|
|
105
106
|
--quiet Suppress all output except errors
|
|
107
|
+
--skip-gitignore Do not touch the target project's .gitignore
|
|
106
108
|
--help, -h Show this help
|
|
107
109
|
|
|
108
110
|
Environment:
|
|
@@ -561,39 +563,63 @@ copy_if_missing() {
|
|
|
561
563
|
cp "$source" "$target"
|
|
562
564
|
}
|
|
563
565
|
|
|
564
|
-
# Ensure .gitignore contains agent-config
|
|
566
|
+
# Ensure .gitignore contains the managed agent-config block.
|
|
567
|
+
# Delegates to scripts/sync_gitignore.py so the installer and the
|
|
568
|
+
# standalone /sync-gitignore command share one source of truth
|
|
569
|
+
# (config/gitignore-block.txt). Honors --dry-run and --skip-gitignore.
|
|
565
570
|
ensure_gitignore() {
|
|
566
571
|
local project_root="$1"
|
|
567
572
|
local gitignore="$project_root/.gitignore"
|
|
573
|
+
local sync_script="$SOURCE_DIR/scripts/sync_gitignore.py"
|
|
574
|
+
local template="$SOURCE_DIR/config/gitignore-block.txt"
|
|
568
575
|
|
|
576
|
+
if $SKIP_GITIGNORE; then
|
|
577
|
+
log_verbose "skip .gitignore (--skip-gitignore)"
|
|
578
|
+
return 0
|
|
579
|
+
fi
|
|
580
|
+
|
|
581
|
+
# Match the pre-refactor behavior: don't create .gitignore in a
|
|
582
|
+
# project that doesn't use git / doesn't already have one.
|
|
569
583
|
if [[ ! -f "$gitignore" ]]; then
|
|
570
584
|
return 0
|
|
571
585
|
fi
|
|
572
586
|
|
|
573
|
-
if
|
|
574
|
-
|
|
587
|
+
if [[ ! -f "$sync_script" || ! -f "$template" ]]; then
|
|
588
|
+
log_warn ".gitignore sync skipped — script or template missing"
|
|
589
|
+
return 0
|
|
590
|
+
fi
|
|
591
|
+
|
|
592
|
+
local args=(--path "$gitignore" --template "$template" --quiet)
|
|
593
|
+
$DRY_RUN && args+=(--dry-run)
|
|
594
|
+
|
|
595
|
+
if python3 "$sync_script" "${args[@]}" >/dev/null 2>&1; then
|
|
596
|
+
log_verbose ".gitignore synced"
|
|
597
|
+
else
|
|
598
|
+
log_warn ".gitignore sync failed (exit $?)"
|
|
599
|
+
fi
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
# Install the consumer-facing CLI wrapper `./agent-config` at the project
|
|
603
|
+
# root. Gitignored, overwritten on every install, delegates to the master
|
|
604
|
+
# CLI shipped in the package (node_modules or vendor).
|
|
605
|
+
install_cli_wrapper() {
|
|
606
|
+
local project_root="$1"
|
|
607
|
+
local template="$SOURCE_DIR/templates/agent-config-wrapper.sh"
|
|
608
|
+
local target="$project_root/agent-config"
|
|
609
|
+
|
|
610
|
+
if [[ ! -f "$template" ]]; then
|
|
611
|
+
log_verbose "CLI wrapper template missing: $template — skipping"
|
|
612
|
+
return 0
|
|
575
613
|
fi
|
|
576
614
|
|
|
577
615
|
if $DRY_RUN; then
|
|
578
|
-
log_verbose "
|
|
616
|
+
log_verbose "install CLI wrapper → $target"
|
|
579
617
|
return
|
|
580
618
|
fi
|
|
581
619
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
# Agent config — symlinked from vendor (auto-managed)
|
|
586
|
-
.augment/skills/
|
|
587
|
-
.augment/commands/
|
|
588
|
-
.augment/guidelines/
|
|
589
|
-
.augment/templates/
|
|
590
|
-
.augment/contexts/
|
|
591
|
-
.augment/scripts/
|
|
592
|
-
.augment/README.md
|
|
593
|
-
|
|
594
|
-
# Agent config — NOT ignored (real copies, may contain project overrides)
|
|
595
|
-
# .augment/rules/
|
|
596
|
-
BLOCK
|
|
620
|
+
cp "$template" "$target"
|
|
621
|
+
chmod +x "$target"
|
|
622
|
+
log_info "Installed ./agent-config wrapper"
|
|
597
623
|
}
|
|
598
624
|
|
|
599
625
|
# --- Main ---
|
|
@@ -626,7 +652,10 @@ main() {
|
|
|
626
652
|
generate_windsurfrules "$TARGET_DIR"
|
|
627
653
|
create_gemini_md "$TARGET_DIR"
|
|
628
654
|
|
|
629
|
-
# 5.
|
|
655
|
+
# 5. Install consumer CLI wrapper (gitignored, overwritten on every install)
|
|
656
|
+
install_cli_wrapper "$TARGET_DIR"
|
|
657
|
+
|
|
658
|
+
# 6. Manage .gitignore
|
|
630
659
|
ensure_gitignore "$TARGET_DIR"
|
|
631
660
|
|
|
632
661
|
echo ""
|
package/scripts/mcp_render.py
CHANGED
|
@@ -31,16 +31,23 @@ import sys
|
|
|
31
31
|
from pathlib import Path
|
|
32
32
|
from typing import Any
|
|
33
33
|
|
|
34
|
-
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
35
|
-
SOURCE_FILE = PROJECT_ROOT / "mcp.json"
|
|
36
|
-
|
|
37
34
|
ENV_PLACEHOLDER = re.compile(r"\$\{env:([^}]+)\}")
|
|
38
35
|
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
# Project root defaults to the current working directory so the renderer
|
|
37
|
+
# works both for package maintainers (running from the package root via
|
|
38
|
+
# Taskfile) and for consumer projects (running via `./agent-config
|
|
39
|
+
# mcp:render` from their own repo root). Override with --project-root.
|
|
40
|
+
def default_project_root() -> Path:
|
|
41
|
+
return Path.cwd().resolve()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def in_project_targets(project_root: Path) -> dict[str, Path]:
|
|
45
|
+
return {
|
|
46
|
+
"cursor": project_root / ".cursor" / "mcp.json",
|
|
47
|
+
"windsurf": project_root / ".windsurf" / "mcp.json",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
44
51
|
CLAUDE_DESKTOP_TARGET = Path.home() / ".config" / "claude-desktop" / "claude_desktop_config.json"
|
|
45
52
|
|
|
46
53
|
|
|
@@ -104,20 +111,25 @@ def write_target(path: Path, content: dict[str, Any]) -> None:
|
|
|
104
111
|
path.write_text(serialized, encoding="utf-8")
|
|
105
112
|
|
|
106
113
|
|
|
107
|
-
def collect_targets(include_claude_desktop: bool) -> dict[str, Path]:
|
|
108
|
-
targets = dict(
|
|
114
|
+
def collect_targets(project_root: Path, include_claude_desktop: bool) -> dict[str, Path]:
|
|
115
|
+
targets = dict(in_project_targets(project_root))
|
|
109
116
|
if include_claude_desktop:
|
|
110
117
|
targets["claude-desktop"] = CLAUDE_DESKTOP_TARGET
|
|
111
118
|
return targets
|
|
112
119
|
|
|
113
120
|
|
|
121
|
+
def resolve_source(args: argparse.Namespace, project_root: Path) -> Path:
|
|
122
|
+
return Path(args.source) if args.source else project_root / "mcp.json"
|
|
123
|
+
|
|
124
|
+
|
|
114
125
|
def cmd_render(args: argparse.Namespace) -> int:
|
|
115
|
-
|
|
126
|
+
project_root = Path(args.project_root).resolve() if args.project_root else default_project_root()
|
|
127
|
+
data = load_source(resolve_source(args, project_root))
|
|
116
128
|
rendered, missing = render(data)
|
|
117
129
|
if missing:
|
|
118
130
|
print(format_missing_report(missing), file=sys.stderr)
|
|
119
131
|
return 1
|
|
120
|
-
targets = collect_targets(args.claude_desktop)
|
|
132
|
+
targets = collect_targets(project_root, args.claude_desktop)
|
|
121
133
|
for name, path in targets.items():
|
|
122
134
|
write_target(path, rendered)
|
|
123
135
|
print(f"✅ {name:16} → {path}")
|
|
@@ -125,20 +137,21 @@ def cmd_render(args: argparse.Namespace) -> int:
|
|
|
125
137
|
|
|
126
138
|
|
|
127
139
|
def cmd_check(args: argparse.Namespace) -> int:
|
|
128
|
-
|
|
140
|
+
project_root = Path(args.project_root).resolve() if args.project_root else default_project_root()
|
|
141
|
+
data = load_source(resolve_source(args, project_root))
|
|
129
142
|
rendered, missing = render(data)
|
|
130
143
|
if missing:
|
|
131
144
|
print(format_missing_report(missing), file=sys.stderr)
|
|
132
145
|
return 1
|
|
133
146
|
serialized = json.dumps(rendered, indent=2, sort_keys=True) + "\n"
|
|
134
|
-
targets = collect_targets(args.claude_desktop)
|
|
147
|
+
targets = collect_targets(project_root, args.claude_desktop)
|
|
135
148
|
diffs = []
|
|
136
149
|
for name, path in targets.items():
|
|
137
150
|
actual = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
138
151
|
if actual != serialized:
|
|
139
152
|
diffs.append((name, path))
|
|
140
153
|
if diffs:
|
|
141
|
-
print("❌ Targets out of date (run
|
|
154
|
+
print("❌ Targets out of date (run `./agent-config mcp:render`):", file=sys.stderr)
|
|
142
155
|
for name, path in diffs:
|
|
143
156
|
print(f" - {name}: {path}", file=sys.stderr)
|
|
144
157
|
return 1
|
|
@@ -148,7 +161,8 @@ def cmd_check(args: argparse.Namespace) -> int:
|
|
|
148
161
|
|
|
149
162
|
def main(argv: list[str] | None = None) -> int:
|
|
150
163
|
parser = argparse.ArgumentParser(description="Render mcp.json → per-tool config files.")
|
|
151
|
-
parser.add_argument("--source", default=
|
|
164
|
+
parser.add_argument("--source", default=None, help="Source mcp.json (default: <project-root>/mcp.json)")
|
|
165
|
+
parser.add_argument("--project-root", default=None, help="Project root for resolving source and targets (default: CWD)")
|
|
152
166
|
parser.add_argument("--claude-desktop", action="store_true", help="Also write Claude Desktop user-scope config")
|
|
153
167
|
parser.add_argument("--check", action="store_true", help="Dry-run; exit non-zero if targets are stale")
|
|
154
168
|
args = parser.parse_args(argv)
|