100xprism 2.3.1
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/LICENSE +21 -0
- package/README.md +196 -0
- package/VERSION +1 -0
- package/adapters/antigravity.sh +14 -0
- package/adapters/claude-code.sh +160 -0
- package/adapters/codex.sh +13 -0
- package/adapters/copilot.sh +13 -0
- package/adapters/cursor.sh +13 -0
- package/adapters/gemini.sh +13 -0
- package/adapters/lib/__pycache__/modules.cpython-312.pyc +0 -0
- package/adapters/lib/modules.py +592 -0
- package/adapters/lib/shared.sh +83 -0
- package/adapters/lib/sync_plugins.py +113 -0
- package/adapters/windsurf.sh +15 -0
- package/bin/100xprism.js +29 -0
- package/get.sh +24 -0
- package/install-project.sh +82 -0
- package/install.sh +281 -0
- package/lib/adapters/windows.js +429 -0
- package/lib/bootstrap.js +33 -0
- package/lib/init.js +19 -0
- package/lib/install.js +18 -0
- package/lib/migrate.js +52 -0
- package/lib/platform.js +22 -0
- package/lib/update.js +29 -0
- package/modules/_lib/reference.md +77 -0
- package/modules/a11y-auditor/SKILL.md +151 -0
- package/modules/ab-test-setup/SKILL.md +266 -0
- package/modules/ab-test-setup/evals/evals.json +105 -0
- package/modules/ab-test-setup/references/sample-size-guide.md +263 -0
- package/modules/ab-test-setup/references/test-templates.md +277 -0
- package/modules/ad-creative/SKILL.md +362 -0
- package/modules/ad-creative/evals/evals.json +90 -0
- package/modules/ad-creative/references/generative-tools.md +637 -0
- package/modules/ad-creative/references/platform-specs.md +213 -0
- package/modules/ai-seo/SKILL.md +398 -0
- package/modules/ai-seo/evals/evals.json +90 -0
- package/modules/ai-seo/references/content-patterns.md +285 -0
- package/modules/ai-seo/references/platform-ranking-factors.md +152 -0
- package/modules/analytics-tracking/SKILL.md +309 -0
- package/modules/analytics-tracking/evals/evals.json +90 -0
- package/modules/analytics-tracking/references/event-library.md +260 -0
- package/modules/analytics-tracking/references/ga4-implementation.md +300 -0
- package/modules/analytics-tracking/references/gtm-implementation.md +390 -0
- package/modules/architect/SKILL.md +282 -0
- package/modules/branch/SKILL.md +105 -0
- package/modules/churn-prevention/SKILL.md +424 -0
- package/modules/churn-prevention/evals/evals.json +93 -0
- package/modules/churn-prevention/references/cancel-flow-patterns.md +316 -0
- package/modules/churn-prevention/references/dunning-playbook.md +408 -0
- package/modules/cloud-security/SKILL.md +240 -0
- package/modules/cold-email/SKILL.md +178 -0
- package/modules/cold-email/evals/evals.json +94 -0
- package/modules/cold-email/references/benchmarks.md +83 -0
- package/modules/cold-email/references/follow-up-sequences.md +81 -0
- package/modules/cold-email/references/frameworks.md +90 -0
- package/modules/cold-email/references/personalization.md +79 -0
- package/modules/cold-email/references/subject-lines.md +53 -0
- package/modules/commit/SKILL.md +195 -0
- package/modules/competitor-alternatives/SKILL.md +256 -0
- package/modules/competitor-alternatives/evals/evals.json +93 -0
- package/modules/competitor-alternatives/references/content-architecture.md +271 -0
- package/modules/competitor-alternatives/references/templates.md +223 -0
- package/modules/connect/SKILL.md +894 -0
- package/modules/content-strategy/SKILL.md +359 -0
- package/modules/content-strategy/evals/evals.json +90 -0
- package/modules/context-dump/SKILL.md +67 -0
- package/modules/copy-editing/SKILL.md +447 -0
- package/modules/copy-editing/evals/evals.json +89 -0
- package/modules/copy-editing/references/plain-english-alternatives.md +394 -0
- package/modules/copywriting/SKILL.md +271 -0
- package/modules/copywriting/evals/evals.json +111 -0
- package/modules/copywriting/references/cold-email-benchmarks.md +83 -0
- package/modules/copywriting/references/cold-email-follow-ups.md +81 -0
- package/modules/copywriting/references/cold-email-frameworks.md +90 -0
- package/modules/copywriting/references/cold-email-personalization.md +79 -0
- package/modules/copywriting/references/cold-email-subject-lines.md +53 -0
- package/modules/copywriting/references/copy-frameworks.md +344 -0
- package/modules/copywriting/references/email-copy-guidelines.md +113 -0
- package/modules/copywriting/references/email-types.md +515 -0
- package/modules/copywriting/references/natural-transitions.md +272 -0
- package/modules/copywriting/references/sequence-templates.md +168 -0
- package/modules/data-query/SKILL.md +58 -0
- package/modules/data-viz/SKILL.md +225 -0
- package/modules/db/SKILL.md +205 -0
- package/modules/db/db-engines/_router.md +24 -0
- package/modules/db/db-engines/athena.md +16 -0
- package/modules/db/db-engines/cloud-sql.md +16 -0
- package/modules/db/db-engines/databricks.md +14 -0
- package/modules/db/db-engines/oracle.md +14 -0
- package/modules/db/db-engines/postgres.md +15 -0
- package/modules/db/db-engines/presto.md +14 -0
- package/modules/db/db-engines/snowflake.md +14 -0
- package/modules/docs/SKILL.md +100 -0
- package/modules/email-sequence/SKILL.md +309 -0
- package/modules/email-sequence/evals/evals.json +93 -0
- package/modules/email-sequence/references/copy-guidelines.md +113 -0
- package/modules/email-sequence/references/email-types.md +515 -0
- package/modules/email-sequence/references/sequence-templates.md +168 -0
- package/modules/enterprise-design/SKILL.md +75 -0
- package/modules/eval/SKILL.md +105 -0
- package/modules/figma-translator/SKILL.md +49 -0
- package/modules/fix-bugs/SKILL.md +104 -0
- package/modules/form-cro/SKILL.md +429 -0
- package/modules/form-cro/evals/evals.json +90 -0
- package/modules/free-tool-strategy/SKILL.md +178 -0
- package/modules/free-tool-strategy/evals/evals.json +90 -0
- package/modules/free-tool-strategy/references/tool-types.md +217 -0
- package/modules/gate/SKILL.md +232 -0
- package/modules/grill-me/SKILL.md +59 -0
- package/modules/interaction-engineer/SKILL.md +49 -0
- package/modules/issue/SKILL.md +272 -0
- package/modules/launch/SKILL.md +345 -0
- package/modules/launch-strategy/SKILL.md +353 -0
- package/modules/launch-strategy/evals/evals.json +91 -0
- package/modules/lint/SKILL.md +126 -0
- package/modules/marketing-ideas/SKILL.md +167 -0
- package/modules/marketing-ideas/evals/evals.json +90 -0
- package/modules/marketing-ideas/references/ideas-by-category.md +366 -0
- package/modules/marketing-psychology/SKILL.md +455 -0
- package/modules/marketing-psychology/evals/evals.json +88 -0
- package/modules/motion-designer/SKILL.md +214 -0
- package/modules/onboarding-cro/SKILL.md +220 -0
- package/modules/onboarding-cro/evals/evals.json +92 -0
- package/modules/onboarding-cro/references/experiments.md +258 -0
- package/modules/orchestrate/SKILL.md +77 -0
- package/modules/page-cro/SKILL.md +182 -0
- package/modules/page-cro/evals/evals.json +111 -0
- package/modules/page-cro/references/experiments.md +248 -0
- package/modules/page-cro/references/paywall-experiments.md +164 -0
- package/modules/paid-ads/SKILL.md +315 -0
- package/modules/paid-ads/evals/evals.json +90 -0
- package/modules/paid-ads/references/ad-copy-templates.md +207 -0
- package/modules/paid-ads/references/audience-targeting.md +243 -0
- package/modules/paid-ads/references/platform-setup-checklists.md +277 -0
- package/modules/paywall-upgrade-cro/SKILL.md +227 -0
- package/modules/paywall-upgrade-cro/evals/evals.json +93 -0
- package/modules/paywall-upgrade-cro/references/experiments.md +164 -0
- package/modules/popup-cro/SKILL.md +453 -0
- package/modules/popup-cro/evals/evals.json +94 -0
- package/modules/pr/SKILL.md +203 -0
- package/modules/pricing-strategy/SKILL.md +231 -0
- package/modules/pricing-strategy/evals/evals.json +90 -0
- package/modules/pricing-strategy/references/research-methods.md +152 -0
- package/modules/pricing-strategy/references/tier-structure.md +232 -0
- package/modules/product-marketing-context/SKILL.md +241 -0
- package/modules/product-marketing-context/evals/evals.json +85 -0
- package/modules/programmatic-seo/SKILL.md +238 -0
- package/modules/programmatic-seo/evals/evals.json +94 -0
- package/modules/programmatic-seo/references/playbooks.md +308 -0
- package/modules/push/SKILL.md +202 -0
- package/modules/referral-program/SKILL.md +255 -0
- package/modules/referral-program/evals/evals.json +89 -0
- package/modules/referral-program/references/affiliate-programs.md +164 -0
- package/modules/referral-program/references/program-examples.md +143 -0
- package/modules/release/SKILL.md +293 -0
- package/modules/revops/SKILL.md +343 -0
- package/modules/revops/evals/evals.json +91 -0
- package/modules/revops/references/automation-playbooks.md +290 -0
- package/modules/revops/references/lifecycle-definitions.md +278 -0
- package/modules/revops/references/routing-rules.md +203 -0
- package/modules/revops/references/scoring-models.md +247 -0
- package/modules/sales-enablement/SKILL.md +349 -0
- package/modules/sales-enablement/evals/evals.json +91 -0
- package/modules/sales-enablement/references/deck-frameworks.md +263 -0
- package/modules/sales-enablement/references/demo-scripts.md +355 -0
- package/modules/sales-enablement/references/objection-library.md +270 -0
- package/modules/sales-enablement/references/one-pager-templates.md +208 -0
- package/modules/schema-markup/SKILL.md +179 -0
- package/modules/schema-markup/evals/evals.json +87 -0
- package/modules/schema-markup/references/schema-examples.md +398 -0
- package/modules/security/SKILL.md +138 -0
- package/modules/seo-audit/SKILL.md +412 -0
- package/modules/seo-audit/evals/evals.json +136 -0
- package/modules/seo-audit/references/ai-writing-detection.md +200 -0
- package/modules/seo-audit/references/content-patterns.md +285 -0
- package/modules/seo-audit/references/platform-ranking-factors.md +152 -0
- package/modules/signup-flow-cro/SKILL.md +359 -0
- package/modules/signup-flow-cro/evals/evals.json +88 -0
- package/modules/site-architecture/SKILL.md +357 -0
- package/modules/site-architecture/evals/evals.json +88 -0
- package/modules/site-architecture/references/mermaid-templates.md +216 -0
- package/modules/site-architecture/references/navigation-patterns.md +305 -0
- package/modules/site-architecture/references/site-type-templates.md +293 -0
- package/modules/social-content/SKILL.md +278 -0
- package/modules/social-content/evals/evals.json +92 -0
- package/modules/social-content/references/platforms.md +170 -0
- package/modules/social-content/references/post-templates.md +177 -0
- package/modules/social-content/references/reverse-engineering.md +195 -0
- package/modules/spec/SKILL.md +81 -0
- package/modules/subagents/SKILL.md +123 -0
- package/modules/techdebt/SKILL.md +71 -0
- package/modules/terminal-setup/SKILL.md +49 -0
- package/modules/test/SKILL.md +493 -0
- package/modules/test/references/e2e-patterns.md +294 -0
- package/modules/update-claude-md/SKILL.md +52 -0
- package/modules/visual-system-architect/SKILL.md +53 -0
- package/package.json +44 -0
- package/plugins/plugins.json +43 -0
- package/shell/aliases.sh +24 -0
- package/shell/check-update.sh +212 -0
- package/templates/.env.example +199 -0
- package/templates/docker-compose.md +46 -0
- package/templates/node-frontend.md +56 -0
- package/templates/node-fullstack.md +59 -0
- package/templates/python-api.md +57 -0
- package/update.sh +231 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Single parser for modules/<slug>/SKILL.md.
|
|
3
|
+
|
|
4
|
+
Used by all adapters. Outputs JSON to stdout with one of these subcommands:
|
|
5
|
+
|
|
6
|
+
list — full manifest (all modules, sorted by category/name)
|
|
7
|
+
emit-concat <out> [limit] — write concatenated core bodies + on-demand index;
|
|
8
|
+
if <limit> set (chars), fall back to index-only mode
|
|
9
|
+
when over budget.
|
|
10
|
+
emit-cursor <project_dir> — write .cursor/rules/<slug>.mdc per module
|
|
11
|
+
emit-codex <project_dir> — write Codex-native AGENTS.md, repo skills, hooks
|
|
12
|
+
emit-claude-code — write ~/.claude/skills/<slug>/* + ~/.claude/commands/<slug>.md
|
|
13
|
+
emit-hooks [--sync] — idempotently merge first-party hooks into
|
|
14
|
+
~/.claude/settings.json from hooks/hooks.manifest.json.
|
|
15
|
+
Each hook is enabled per its manifest `default`, overridable
|
|
16
|
+
by its `toggle_env` env var (1/true=on, 0/false=off).
|
|
17
|
+
--sync only refreshes hooks already present (used on update).
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
import shutil
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
REPO = Path(__file__).resolve().parents[2]
|
|
29
|
+
MODULES_DIR = REPO / "modules"
|
|
30
|
+
|
|
31
|
+
# Markers + manifest let `emit-claude-code` prune ONLY 100xprism's own artifacts
|
|
32
|
+
# (never the user's hand-authored skills/commands) when a module is removed or
|
|
33
|
+
# merged upstream. See cmd_emit_claude_code.
|
|
34
|
+
GENERATED_MARKER = ".100xprism-generated"
|
|
35
|
+
ALIAS_MARKER = "<!-- 100xprism generated alias — regenerate, do not edit -->"
|
|
36
|
+
MANIFEST_NAME = ".100xprism-manifest.json"
|
|
37
|
+
|
|
38
|
+
# Modules removed in past releases. Cleaned up on update even for installs whose
|
|
39
|
+
# skills predate the manifest/marker (so they carry neither). Safe to trim once
|
|
40
|
+
# installs have cycled through a manifest-aware update.
|
|
41
|
+
REMOVED_MODULES = {"systems-architect", "conversion-copy"}
|
|
42
|
+
|
|
43
|
+
# Central model-routing map. Modules declare a bare alias (`model: haiku`) in
|
|
44
|
+
# frontmatter. Claude Code reads that alias natively and auto-resolves it to the
|
|
45
|
+
# latest version — so the alias is emitted verbatim there (see cmd_emit_claude_code,
|
|
46
|
+
# which copies frontmatter as-is). Every other adapter picks its own (often
|
|
47
|
+
# non-Claude) model, so they receive a vendor-neutral reasoning-tier hint instead of
|
|
48
|
+
# a Claude model name. `id`/`label` are the single source of truth for the deferred
|
|
49
|
+
# slash-command / documentation work; the emitters in this pass use `tier_hint`.
|
|
50
|
+
MODEL_ALIASES = {
|
|
51
|
+
"haiku": {"id": "claude-haiku-4-5", "label": "Claude Haiku 4.5", "tier_hint": "fast / low-cost (mechanical task)"},
|
|
52
|
+
"sonnet": {"id": "claude-sonnet-4-6", "label": "Claude Sonnet 4.6", "tier_hint": "balanced (moderate reasoning)"},
|
|
53
|
+
"opus": {"id": "claude-opus-4-8", "label": "Claude Opus 4.8", "tier_hint": "most capable (deep reasoning)"},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def tier_annotation(model: str) -> str:
|
|
58
|
+
"""Vendor-neutral routing hint for non-Claude adapters. '' for unknown/empty alias."""
|
|
59
|
+
info = MODEL_ALIASES.get(model.strip())
|
|
60
|
+
return f"_Suggested model tier: {info['tier_hint']}_" if info else ""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def short_description(desc: str) -> str:
|
|
64
|
+
"""First sentence of a description, capped at 140 chars — for tight rule triggering."""
|
|
65
|
+
d = desc.split(". ", 1)[0]
|
|
66
|
+
if len(d) > 140:
|
|
67
|
+
d = d[:137] + "..."
|
|
68
|
+
return d
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def render_command_alias(fm: dict, slug: str, body: str) -> str:
|
|
72
|
+
"""Slash-command alias that mirrors the skill's routing/guardrails in frontmatter.
|
|
73
|
+
|
|
74
|
+
Commands and skills share a frontmatter schema, so the alias carries description,
|
|
75
|
+
model routing, and allowed-tools instead of dropping them. argument-hint is emitted
|
|
76
|
+
generically only when the body actually consumes positional input.
|
|
77
|
+
"""
|
|
78
|
+
lines = ["---", f"description: {short_description(fm.get('description', ''))}"]
|
|
79
|
+
model = fm.get("model", "").strip()
|
|
80
|
+
if model:
|
|
81
|
+
lines.append(f"model: {model}")
|
|
82
|
+
allowed = fm.get("allowed-tools", "").strip()
|
|
83
|
+
if allowed:
|
|
84
|
+
lines.append(f"allowed-tools: {allowed}")
|
|
85
|
+
if "$ARGUMENTS" in body or "$1" in body:
|
|
86
|
+
lines.append("argument-hint: [arguments]")
|
|
87
|
+
lines += ["---", "", ALIAS_MARKER, "", f"Use the `{fm.get('name', slug)}` skill.", "", "$ARGUMENTS", ""]
|
|
88
|
+
return "\n".join(lines)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def split_frontmatter(text: str) -> tuple[dict, str]:
|
|
92
|
+
if not text.startswith("---\n"):
|
|
93
|
+
return {}, text
|
|
94
|
+
end = text.find("\n---\n", 4)
|
|
95
|
+
if end == -1:
|
|
96
|
+
return {}, text
|
|
97
|
+
fm_block = text[4:end]
|
|
98
|
+
body = text[end + 5 :]
|
|
99
|
+
fm: dict[str, str] = {}
|
|
100
|
+
current_key: str | None = None
|
|
101
|
+
for line in fm_block.splitlines():
|
|
102
|
+
if not line.strip():
|
|
103
|
+
continue
|
|
104
|
+
if line.startswith(" ") and current_key:
|
|
105
|
+
fm[current_key] = (fm[current_key] + " " + line.strip()).strip()
|
|
106
|
+
continue
|
|
107
|
+
if ":" in line:
|
|
108
|
+
key, _, val = line.partition(":")
|
|
109
|
+
current_key = key.strip()
|
|
110
|
+
fm[current_key] = val.strip()
|
|
111
|
+
return fm, body
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def list_modules() -> list[dict]:
|
|
115
|
+
out: list[dict] = []
|
|
116
|
+
for skill_md in sorted(MODULES_DIR.glob("*/SKILL.md")):
|
|
117
|
+
fm, body = split_frontmatter(skill_md.read_text())
|
|
118
|
+
out.append({
|
|
119
|
+
"slug": skill_md.parent.name,
|
|
120
|
+
"name": fm.get("name", skill_md.parent.name),
|
|
121
|
+
"description": fm.get("description", ""),
|
|
122
|
+
"category": fm.get("category", "uncategorized"),
|
|
123
|
+
"tier": fm.get("tier", "on-demand"),
|
|
124
|
+
"slash_command": fm.get("slash_command", ""),
|
|
125
|
+
"allowed_tools": fm.get("allowed-tools", ""),
|
|
126
|
+
"model": fm.get("model", ""),
|
|
127
|
+
"body": body,
|
|
128
|
+
"dir": str(skill_md.parent),
|
|
129
|
+
})
|
|
130
|
+
out.sort(key=lambda m: (m["tier"] != "core", m["category"], m["slug"]))
|
|
131
|
+
return out
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
CATEGORY_ORDER = [
|
|
135
|
+
"lifecycle", "quality", "engineering", "data", "design", "docs", "marketing", "uncategorized"
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def category_sort_key(c: str) -> int:
|
|
140
|
+
try:
|
|
141
|
+
return CATEGORY_ORDER.index(c)
|
|
142
|
+
except ValueError:
|
|
143
|
+
return len(CATEGORY_ORDER)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def emit_index(modules: list[dict], indent: str = "") -> str:
|
|
147
|
+
"""One-line-per-module index, grouped by category. Used for on-demand listing."""
|
|
148
|
+
lines: list[str] = []
|
|
149
|
+
by_cat: dict[str, list[dict]] = {}
|
|
150
|
+
for m in modules:
|
|
151
|
+
by_cat.setdefault(m["category"], []).append(m)
|
|
152
|
+
for cat in sorted(by_cat.keys(), key=category_sort_key):
|
|
153
|
+
lines.append(f"{indent}**{cat.title()}** ({len(by_cat[cat])}):")
|
|
154
|
+
for m in by_cat[cat]:
|
|
155
|
+
slash = f" `{m['slash_command']}`" if m["slash_command"] else ""
|
|
156
|
+
d = short_description(m["description"])
|
|
157
|
+
tier = tier_annotation(m.get("model", ""))
|
|
158
|
+
tier_suffix = f" {tier}" if tier else ""
|
|
159
|
+
lines.append(f"{indent}- `{m['slug']}`{slash} — {d}{tier_suffix}")
|
|
160
|
+
lines.append("")
|
|
161
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
HEADER = (
|
|
165
|
+
"# 100x Dev — Modules\n"
|
|
166
|
+
"# Generated by 100xprism (https://github.com/rajitsaha/100xprism)\n"
|
|
167
|
+
"# Source of truth: modules/<slug>/SKILL.md. Edit there and regenerate.\n\n"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def render_concat(modules: list[dict]) -> str:
|
|
172
|
+
"""Full concat: inline core module bodies + index of on-demand modules."""
|
|
173
|
+
out = [HEADER]
|
|
174
|
+
core = [m for m in modules if m["tier"] == "core"]
|
|
175
|
+
on_demand = [m for m in modules if m["tier"] != "core"]
|
|
176
|
+
|
|
177
|
+
out.append("## Core modules (always-on)\n")
|
|
178
|
+
for m in sorted(core, key=lambda x: (category_sort_key(x["category"]), x["slug"])):
|
|
179
|
+
out.append(f"---\n\n## {m['slug']}\n")
|
|
180
|
+
if m["slash_command"]:
|
|
181
|
+
out.append(f"_Slash command: `{m['slash_command']}`_\n")
|
|
182
|
+
tier = tier_annotation(m.get("model", ""))
|
|
183
|
+
if tier:
|
|
184
|
+
out.append(f"{tier}\n")
|
|
185
|
+
out.append(m["body"].lstrip("\n").rstrip() + "\n\n")
|
|
186
|
+
|
|
187
|
+
out.append("---\n\n## On-demand modules (invoke by name)\n\n")
|
|
188
|
+
out.append(
|
|
189
|
+
"These load only when triggered. Ask Claude to use the relevant module by "
|
|
190
|
+
"name when the situation matches.\n\n"
|
|
191
|
+
)
|
|
192
|
+
out.append(emit_index(on_demand))
|
|
193
|
+
return "".join(out)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def render_index_only(modules: list[dict]) -> str:
|
|
197
|
+
"""Index-only: every module gets one line. Used when over byte budget (Windsurf)."""
|
|
198
|
+
out = [HEADER, "## Modules available\n\n"]
|
|
199
|
+
out.append(
|
|
200
|
+
"100xprism installs modules in your global config (`~/.claude/skills/` or "
|
|
201
|
+
"`.cursor/rules/`). When the user's request matches one of these, use it.\n\n"
|
|
202
|
+
)
|
|
203
|
+
out.append(emit_index(modules))
|
|
204
|
+
return "".join(out)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def render_codex_agents(modules: list[dict]) -> str:
|
|
208
|
+
"""Compact Codex project guidance.
|
|
209
|
+
|
|
210
|
+
Codex has a default AGENTS.md byte budget, while repo skills load through
|
|
211
|
+
progressive disclosure. Keep AGENTS.md as routing/orientation text and emit
|
|
212
|
+
full module bodies into .agents/skills instead.
|
|
213
|
+
"""
|
|
214
|
+
out = [
|
|
215
|
+
"# 100x Dev for Codex\n",
|
|
216
|
+
"# Generated by 100xprism (https://github.com/rajitsaha/100xprism)\n",
|
|
217
|
+
"# Source of truth: modules/<slug>/SKILL.md. Regenerate instead of hand-editing.\n\n",
|
|
218
|
+
"## How Codex Should Use 100x Dev\n\n",
|
|
219
|
+
"- Full reusable workflows live in `.agents/skills/<slug>/SKILL.md` so Codex can load them on demand.\n",
|
|
220
|
+
"- When the user names a 100xprism slash workflow like `/gate`, treat it as a request to use the matching skill listed below.\n",
|
|
221
|
+
"- Prefer explicit skill invocation (`$gate`, `$commit`, `$test`, etc.) or `/skills` when available; custom prompt slash commands are intentionally not generated.\n",
|
|
222
|
+
"- Codex hooks, when generated, live in `.codex/hooks.json`. Review and trust them with `/hooks` before expecting enforcement.\n",
|
|
223
|
+
"- Claude Code plugins in `plugins/plugins.json` are not Codex plugins. Use Codex `/plugins` for Codex-native plugins and app/MCP integrations.\n\n",
|
|
224
|
+
"## 100xprism Command Map\n\n",
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
commands = [m for m in modules if m["slash_command"]]
|
|
228
|
+
for m in sorted(commands, key=lambda x: x["slash_command"]):
|
|
229
|
+
out.append(f"- `{m['slash_command']}` → `${m['slug']}` — {short_description(m['description'])}\n")
|
|
230
|
+
|
|
231
|
+
out.append("\n## Available Skills\n\n")
|
|
232
|
+
out.append(
|
|
233
|
+
"Codex can invoke these implicitly from their descriptions or explicitly by `$name` / `/skills`.\n\n"
|
|
234
|
+
)
|
|
235
|
+
out.append(emit_index(modules))
|
|
236
|
+
return "".join(out)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def cmd_list():
|
|
240
|
+
mods = list_modules()
|
|
241
|
+
print(json.dumps(mods, indent=2))
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def cmd_emit_concat(out_path: str, mode: str = "concat"):
|
|
245
|
+
"""mode: 'concat' (core bodies + on-demand index) or 'index' (one-liners only)."""
|
|
246
|
+
mods = list_modules()
|
|
247
|
+
if mode == "index":
|
|
248
|
+
text = render_index_only(mods)
|
|
249
|
+
else:
|
|
250
|
+
text = render_concat(mods)
|
|
251
|
+
Path(out_path).parent.mkdir(parents=True, exist_ok=True)
|
|
252
|
+
Path(out_path).write_text(text)
|
|
253
|
+
print(f"wrote {out_path} ({len(text)} bytes, mode={mode})")
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def cmd_emit_cursor(project_dir: str):
|
|
257
|
+
rules_dir = Path(project_dir) / ".cursor" / "rules"
|
|
258
|
+
if rules_dir.exists():
|
|
259
|
+
# remove only files we previously wrote (those with our generation marker)
|
|
260
|
+
for f in rules_dir.glob("*.mdc"):
|
|
261
|
+
try:
|
|
262
|
+
first = f.read_text().splitlines()[0:6]
|
|
263
|
+
if any("100xprism" in line for line in first):
|
|
264
|
+
f.unlink()
|
|
265
|
+
except OSError:
|
|
266
|
+
pass
|
|
267
|
+
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
268
|
+
mods = list_modules()
|
|
269
|
+
for m in mods:
|
|
270
|
+
# Map tier to Cursor rule type: core modules are always in context;
|
|
271
|
+
# on-demand modules are agent-requested via a tight first-sentence description.
|
|
272
|
+
always_apply = "true" if m["tier"] == "core" else "false"
|
|
273
|
+
cursor_fm = [
|
|
274
|
+
"---",
|
|
275
|
+
f"description: {short_description(m['description'])}",
|
|
276
|
+
"globs:",
|
|
277
|
+
f"alwaysApply: {always_apply}",
|
|
278
|
+
"---",
|
|
279
|
+
"",
|
|
280
|
+
f"<!-- generated by 100xprism from modules/{m['slug']}/SKILL.md -->",
|
|
281
|
+
"",
|
|
282
|
+
]
|
|
283
|
+
tier = tier_annotation(m.get("model", ""))
|
|
284
|
+
if tier:
|
|
285
|
+
cursor_fm += [tier, ""]
|
|
286
|
+
text = "\n".join(cursor_fm) + m["body"].lstrip("\n")
|
|
287
|
+
(rules_dir / f"{m['slug']}.mdc").write_text(text)
|
|
288
|
+
print(f"wrote {len(mods)} files to {rules_dir}")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def cmd_emit_codex(project_dir: str):
|
|
292
|
+
"""Write Codex-native project artifacts.
|
|
293
|
+
|
|
294
|
+
- AGENTS.md stays compact and under Codex's default project-doc budget.
|
|
295
|
+
- .agents/skills contains the full module directories for progressive loading.
|
|
296
|
+
- .codex/hooks.json wires first-party hooks for Codex's hook browser/trust flow.
|
|
297
|
+
"""
|
|
298
|
+
project = Path(project_dir)
|
|
299
|
+
mods = list_modules()
|
|
300
|
+
|
|
301
|
+
agents_file = project / "AGENTS.md"
|
|
302
|
+
agents_file.parent.mkdir(parents=True, exist_ok=True)
|
|
303
|
+
agents_file.write_text(render_codex_agents(mods))
|
|
304
|
+
|
|
305
|
+
skills_dir = project / ".agents" / "skills"
|
|
306
|
+
if skills_dir.exists():
|
|
307
|
+
for child in skills_dir.iterdir():
|
|
308
|
+
if not child.is_dir():
|
|
309
|
+
continue
|
|
310
|
+
marker = child / ".100xprism-generated"
|
|
311
|
+
if marker.exists():
|
|
312
|
+
shutil.rmtree(child)
|
|
313
|
+
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
314
|
+
skill_count = 0
|
|
315
|
+
for module_dir in sorted(MODULES_DIR.glob("*")):
|
|
316
|
+
if not module_dir.is_dir() or not (module_dir / "SKILL.md").is_file():
|
|
317
|
+
continue
|
|
318
|
+
target = skills_dir / module_dir.name
|
|
319
|
+
if target.exists():
|
|
320
|
+
marker = target / ".100xprism-generated"
|
|
321
|
+
if marker.exists():
|
|
322
|
+
shutil.rmtree(target)
|
|
323
|
+
else:
|
|
324
|
+
print(f"skipped existing non-100xprism skill: {target}", file=sys.stderr)
|
|
325
|
+
continue
|
|
326
|
+
shutil.copytree(module_dir, target)
|
|
327
|
+
(target / ".100xprism-generated").write_text(
|
|
328
|
+
"Generated by 100xprism from modules/<slug>/SKILL.md. Regenerate instead of editing here.\n"
|
|
329
|
+
)
|
|
330
|
+
skill_count += 1
|
|
331
|
+
|
|
332
|
+
hooks_dir = project / ".codex"
|
|
333
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
334
|
+
manifest = json.loads((HOOKS_DIR / "hooks.manifest.json").read_text())
|
|
335
|
+
hooks: dict[str, list[dict]] = {}
|
|
336
|
+
for hook in manifest.get("hooks", []):
|
|
337
|
+
if not _hook_enabled(hook):
|
|
338
|
+
continue
|
|
339
|
+
command = _hook_command(hook["script"])
|
|
340
|
+
hooks.setdefault(hook["event"], []).append({
|
|
341
|
+
"matcher": hook["matcher"],
|
|
342
|
+
"hooks": [{
|
|
343
|
+
"type": "command",
|
|
344
|
+
"command": command,
|
|
345
|
+
"statusMessage": f"100xprism: {hook['description']}",
|
|
346
|
+
}],
|
|
347
|
+
})
|
|
348
|
+
(hooks_dir / "hooks.json").write_text(json.dumps({"hooks": hooks}, indent=2) + "\n")
|
|
349
|
+
|
|
350
|
+
print(
|
|
351
|
+
f"wrote Codex AGENTS.md + {skill_count} repo skills + "
|
|
352
|
+
f"{sum(len(v) for v in hooks.values())} hook group(s) to {project}"
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _read_manifest(skills_dir: Path) -> dict:
|
|
357
|
+
try:
|
|
358
|
+
return json.loads((skills_dir / MANIFEST_NAME).read_text())
|
|
359
|
+
except (OSError, ValueError):
|
|
360
|
+
return {}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# Pre-rebrand marker names. Renamed in place so manifest/prune logic recognizes
|
|
364
|
+
# installs that were last emitted under the old `100x-dev` name.
|
|
365
|
+
_LEGACY_GENERATED_MARKER = ".100x-dev-generated"
|
|
366
|
+
_LEGACY_MANIFEST_NAME = ".100x-dev-manifest.json"
|
|
367
|
+
_LEGACY_ALIAS_MARKER = "<!-- 100x-dev generated alias — regenerate, do not edit -->"
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _migrate_legacy_markers(skills_dir: Path, commands_dir: Path) -> None:
|
|
371
|
+
legacy_manifest = skills_dir / _LEGACY_MANIFEST_NAME
|
|
372
|
+
if legacy_manifest.exists() and not (skills_dir / MANIFEST_NAME).exists():
|
|
373
|
+
legacy_manifest.rename(skills_dir / MANIFEST_NAME)
|
|
374
|
+
for child in skills_dir.iterdir():
|
|
375
|
+
if not child.is_dir():
|
|
376
|
+
continue
|
|
377
|
+
legacy = child / _LEGACY_GENERATED_MARKER
|
|
378
|
+
if legacy.exists() and not (child / GENERATED_MARKER).exists():
|
|
379
|
+
legacy.rename(child / GENERATED_MARKER)
|
|
380
|
+
for f in commands_dir.iterdir():
|
|
381
|
+
if not f.is_file() or f.suffix != ".md":
|
|
382
|
+
continue
|
|
383
|
+
try:
|
|
384
|
+
text = f.read_text()
|
|
385
|
+
except OSError:
|
|
386
|
+
continue
|
|
387
|
+
if _LEGACY_ALIAS_MARKER in text:
|
|
388
|
+
f.write_text(text.replace(_LEGACY_ALIAS_MARKER, ALIAS_MARKER))
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def cmd_emit_claude_code():
|
|
392
|
+
home = Path(os.environ.get("HOME", str(Path.home())))
|
|
393
|
+
skills_dir = home / ".claude" / "skills"
|
|
394
|
+
commands_dir = home / ".claude" / "commands"
|
|
395
|
+
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
396
|
+
commands_dir.mkdir(parents=True, exist_ok=True)
|
|
397
|
+
|
|
398
|
+
_migrate_legacy_markers(skills_dir, commands_dir)
|
|
399
|
+
prev = _read_manifest(skills_dir)
|
|
400
|
+
prev_skills = set(prev.get("skills", []))
|
|
401
|
+
prev_cmds = set(prev.get("commands", []))
|
|
402
|
+
|
|
403
|
+
current_slugs: list[str] = []
|
|
404
|
+
current_cmds: list[str] = []
|
|
405
|
+
skill_count = 0
|
|
406
|
+
cmd_count = 0
|
|
407
|
+
for module_dir in sorted(MODULES_DIR.glob("*")):
|
|
408
|
+
if not module_dir.is_dir():
|
|
409
|
+
continue
|
|
410
|
+
# A module is a dir with a SKILL.md. Shared-reference dirs (e.g. `_lib/`, which
|
|
411
|
+
# holds maintainer conventions in reference.md) have none — skip, don't emit them.
|
|
412
|
+
if not (module_dir / "SKILL.md").is_file():
|
|
413
|
+
continue
|
|
414
|
+
slug = module_dir.name
|
|
415
|
+
target = skills_dir / slug
|
|
416
|
+
if target.exists():
|
|
417
|
+
shutil.rmtree(target)
|
|
418
|
+
shutil.copytree(module_dir, target)
|
|
419
|
+
(target / GENERATED_MARKER).write_text(
|
|
420
|
+
"Generated by 100xprism from modules/<slug>/SKILL.md. Regenerate instead of editing here.\n"
|
|
421
|
+
)
|
|
422
|
+
current_slugs.append(slug)
|
|
423
|
+
skill_count += 1
|
|
424
|
+
|
|
425
|
+
# Slash command alias for any module with slash_command in frontmatter
|
|
426
|
+
skill_md = target / "SKILL.md"
|
|
427
|
+
fm, body = split_frontmatter(skill_md.read_text())
|
|
428
|
+
slash = fm.get("slash_command", "").lstrip("/")
|
|
429
|
+
if slash:
|
|
430
|
+
(commands_dir / f"{slash}.md").write_text(render_command_alias(fm, slug, body))
|
|
431
|
+
current_cmds.append(slash)
|
|
432
|
+
cmd_count += 1
|
|
433
|
+
|
|
434
|
+
cur_skill_set = set(current_slugs)
|
|
435
|
+
cur_cmd_set = set(current_cmds)
|
|
436
|
+
|
|
437
|
+
# Prune orphaned skills: anything we previously emitted (manifest), carrying
|
|
438
|
+
# our marker, or a known-removed module — that is no longer current. We only
|
|
439
|
+
# ever rmtree dirs that are unambiguously ours.
|
|
440
|
+
orphan_skills = (prev_skills | REMOVED_MODULES) - cur_skill_set
|
|
441
|
+
for child in skills_dir.iterdir():
|
|
442
|
+
if child.is_dir() and child.name not in cur_skill_set and (child / GENERATED_MARKER).exists():
|
|
443
|
+
orphan_skills.add(child.name)
|
|
444
|
+
pruned_skills = 0
|
|
445
|
+
for slug in sorted(orphan_skills):
|
|
446
|
+
p = skills_dir / slug
|
|
447
|
+
if p.is_dir() and ((p / GENERATED_MARKER).exists() or slug in prev_skills or slug in REMOVED_MODULES):
|
|
448
|
+
shutil.rmtree(p)
|
|
449
|
+
pruned_skills += 1
|
|
450
|
+
|
|
451
|
+
# Prune orphaned slash-command aliases we previously wrote (marker-guarded so
|
|
452
|
+
# a user's own command of the same name is never deleted).
|
|
453
|
+
pruned_cmds = 0
|
|
454
|
+
for name in sorted(prev_cmds - cur_cmd_set):
|
|
455
|
+
f = commands_dir / f"{name}.md"
|
|
456
|
+
try:
|
|
457
|
+
if f.is_file() and ALIAS_MARKER in f.read_text():
|
|
458
|
+
f.unlink()
|
|
459
|
+
pruned_cmds += 1
|
|
460
|
+
except OSError:
|
|
461
|
+
pass
|
|
462
|
+
|
|
463
|
+
(skills_dir / MANIFEST_NAME).write_text(
|
|
464
|
+
json.dumps({"skills": sorted(current_slugs), "commands": sorted(current_cmds)}, indent=2) + "\n"
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
suffix = ""
|
|
468
|
+
if pruned_skills or pruned_cmds:
|
|
469
|
+
suffix = f" (pruned {pruned_skills} stale skill(s), {pruned_cmds} stale alias(es))"
|
|
470
|
+
print(f"wrote {skill_count} skills + {cmd_count} slash command aliases{suffix}")
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
HOOKS_DIR = REPO / "hooks"
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _hook_command(script: str) -> str:
|
|
477
|
+
return f'python3 "{HOOKS_DIR / script}"'
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _hook_enabled(hook: dict) -> bool:
|
|
481
|
+
"""manifest `default`, overridden by the hook's `toggle_env` env var if set."""
|
|
482
|
+
env = hook.get("toggle_env")
|
|
483
|
+
if env and env in os.environ:
|
|
484
|
+
val = os.environ[env].strip().lower()
|
|
485
|
+
if val in ("1", "true", "yes", "on"):
|
|
486
|
+
return True
|
|
487
|
+
if val in ("0", "false", "no", "off", ""):
|
|
488
|
+
return False
|
|
489
|
+
return bool(hook.get("default"))
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _command_present(entries: list, command: str) -> bool:
|
|
493
|
+
return any(
|
|
494
|
+
h.get("command") == command
|
|
495
|
+
for e in entries
|
|
496
|
+
for h in e.get("hooks", [])
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _strip_command(entries: list, command: str) -> list:
|
|
501
|
+
"""Remove our command from every entry; drop entries left with no hooks."""
|
|
502
|
+
out = []
|
|
503
|
+
for e in entries:
|
|
504
|
+
kept = [h for h in e.get("hooks", []) if h.get("command") != command]
|
|
505
|
+
if kept:
|
|
506
|
+
out.append({**e, "hooks": kept})
|
|
507
|
+
return out
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def cmd_emit_hooks(sync: bool = False):
|
|
511
|
+
"""Idempotently merge first-party hooks into settings.json (declarative).
|
|
512
|
+
|
|
513
|
+
For each manifest hook we strip any existing copy of its command, then re-add a
|
|
514
|
+
single entry when it should be enabled — so re-running never duplicates entries and
|
|
515
|
+
flipping a toggle off removes the hook. `sync` keeps only hooks already present
|
|
516
|
+
(used on update, so we never silently enable a hook the user opted out of).
|
|
517
|
+
"""
|
|
518
|
+
manifest = json.loads((HOOKS_DIR / "hooks.manifest.json").read_text())
|
|
519
|
+
hooks_spec = manifest.get("hooks", [])
|
|
520
|
+
|
|
521
|
+
settings_file = Path(
|
|
522
|
+
os.environ.get("SETTINGS_FILE")
|
|
523
|
+
or os.path.expanduser("~/.claude/settings.json")
|
|
524
|
+
)
|
|
525
|
+
settings_file.parent.mkdir(parents=True, exist_ok=True)
|
|
526
|
+
try:
|
|
527
|
+
settings = json.loads(settings_file.read_text())
|
|
528
|
+
except (OSError, json.JSONDecodeError):
|
|
529
|
+
settings = {}
|
|
530
|
+
|
|
531
|
+
settings_hooks = settings.setdefault("hooks", {})
|
|
532
|
+
added, kept, removed = [], [], []
|
|
533
|
+
|
|
534
|
+
for hook in hooks_spec:
|
|
535
|
+
command = _hook_command(hook["script"])
|
|
536
|
+
event = hook["event"]
|
|
537
|
+
entries = settings_hooks.get(event, [])
|
|
538
|
+
was_present = _command_present(entries, command)
|
|
539
|
+
entries = _strip_command(entries, command) # dedupe / declarative reset
|
|
540
|
+
|
|
541
|
+
keep = was_present if sync else _hook_enabled(hook)
|
|
542
|
+
if keep:
|
|
543
|
+
entries.append({
|
|
544
|
+
"matcher": hook["matcher"],
|
|
545
|
+
"hooks": [{"type": "command", "command": command}],
|
|
546
|
+
})
|
|
547
|
+
(kept if was_present else added).append(hook["id"])
|
|
548
|
+
elif was_present:
|
|
549
|
+
removed.append(hook["id"])
|
|
550
|
+
|
|
551
|
+
if entries:
|
|
552
|
+
settings_hooks[event] = entries
|
|
553
|
+
elif event in settings_hooks:
|
|
554
|
+
del settings_hooks[event]
|
|
555
|
+
|
|
556
|
+
settings_file.write_text(json.dumps(settings, indent=2))
|
|
557
|
+
|
|
558
|
+
parts = []
|
|
559
|
+
if added:
|
|
560
|
+
parts.append(f"added {len(added)} ({', '.join(added)})")
|
|
561
|
+
if kept:
|
|
562
|
+
parts.append(f"refreshed {len(kept)}")
|
|
563
|
+
if removed:
|
|
564
|
+
parts.append(f"removed {len(removed)} ({', '.join(removed)})")
|
|
565
|
+
print(f"hooks: {'; '.join(parts) if parts else 'no changes'} → {settings_file}")
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def main(argv: list[str]):
|
|
569
|
+
if len(argv) < 2:
|
|
570
|
+
print(__doc__, file=sys.stderr)
|
|
571
|
+
return 2
|
|
572
|
+
cmd = argv[1]
|
|
573
|
+
if cmd == "list":
|
|
574
|
+
cmd_list()
|
|
575
|
+
elif cmd == "emit-concat":
|
|
576
|
+
cmd_emit_concat(argv[2], argv[3] if len(argv) > 3 else "")
|
|
577
|
+
elif cmd == "emit-cursor":
|
|
578
|
+
cmd_emit_cursor(argv[2])
|
|
579
|
+
elif cmd == "emit-codex":
|
|
580
|
+
cmd_emit_codex(argv[2])
|
|
581
|
+
elif cmd == "emit-claude-code":
|
|
582
|
+
cmd_emit_claude_code()
|
|
583
|
+
elif cmd == "emit-hooks":
|
|
584
|
+
cmd_emit_hooks(sync="--sync" in argv[2:])
|
|
585
|
+
else:
|
|
586
|
+
print(f"unknown command: {cmd}", file=sys.stderr)
|
|
587
|
+
return 2
|
|
588
|
+
return 0
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
if __name__ == "__main__":
|
|
592
|
+
sys.exit(main(sys.argv))
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# shared.sh — common logic for all 100xprism adapter scripts.
|
|
3
|
+
# Source this file; do not execute directly.
|
|
4
|
+
#
|
|
5
|
+
# All adapters dispatch to adapters/lib/modules.py for module reading and
|
|
6
|
+
# rendering. Adapters here provide thin shell wrappers + per-tool output paths.
|
|
7
|
+
|
|
8
|
+
_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
REPO_DIR="$(cd "$_LIB_DIR/../.." && pwd)"
|
|
10
|
+
MODULES_PY="$_LIB_DIR/modules.py"
|
|
11
|
+
|
|
12
|
+
GREEN='\033[0;32m'
|
|
13
|
+
YELLOW='\033[1;33m'
|
|
14
|
+
NC='\033[0m'
|
|
15
|
+
|
|
16
|
+
# _run_concat <project_path> <output_file_relative> <display_name> [<mode>] [<warning>]
|
|
17
|
+
#
|
|
18
|
+
# Used by tools that consume a single rules file (Codex AGENTS.md, Windsurf, etc.).
|
|
19
|
+
# mode is 'concat' (default — full core bodies + on-demand index) or 'index'
|
|
20
|
+
# (one-line per module; used when the tool has tight size limits).
|
|
21
|
+
_run_concat() {
|
|
22
|
+
local project_path="$1"
|
|
23
|
+
local output_rel="$2"
|
|
24
|
+
local display_name="$3"
|
|
25
|
+
local mode="${4:-concat}"
|
|
26
|
+
local warning_message="${5:-}"
|
|
27
|
+
|
|
28
|
+
local output_file="$project_path/$output_rel"
|
|
29
|
+
mkdir -p "$(dirname "$output_file")"
|
|
30
|
+
|
|
31
|
+
echo ""
|
|
32
|
+
echo "Generating $output_rel for $display_name..."
|
|
33
|
+
if [[ -n "$warning_message" ]]; then
|
|
34
|
+
echo -e "${YELLOW}${warning_message}${NC}"
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
python3 "$MODULES_PY" emit-concat "$output_file" "$mode"
|
|
38
|
+
|
|
39
|
+
_track_project "$project_path"
|
|
40
|
+
echo -e " ${GREEN}→ Generated $output_file ✓${NC}"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# _run_cursor <project_path>
|
|
44
|
+
# Cursor supports per-rule files with description-based auto-trigger, so we
|
|
45
|
+
# write one .cursor/rules/<slug>.mdc per module.
|
|
46
|
+
_run_cursor() {
|
|
47
|
+
local project_path="$1"
|
|
48
|
+
echo ""
|
|
49
|
+
echo "Generating .cursor/rules/ for Cursor..."
|
|
50
|
+
python3 "$MODULES_PY" emit-cursor "$project_path"
|
|
51
|
+
_track_project "$project_path"
|
|
52
|
+
echo -e " ${GREEN}→ Generated .cursor/rules/ in $project_path ✓${NC}"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# _run_codex <project_path>
|
|
56
|
+
# Codex supports repo-scoped skills and hooks, so we keep AGENTS.md compact and
|
|
57
|
+
# emit full module bodies into .agents/skills for progressive loading.
|
|
58
|
+
_run_codex() {
|
|
59
|
+
local project_path="$1"
|
|
60
|
+
echo ""
|
|
61
|
+
echo "Generating Codex project artifacts..."
|
|
62
|
+
python3 "$MODULES_PY" emit-codex "$project_path"
|
|
63
|
+
_track_project "$project_path"
|
|
64
|
+
echo -e " ${GREEN}→ Generated AGENTS.md, .agents/skills/, and .codex/hooks.json in $project_path ✓${NC}"
|
|
65
|
+
echo -e " ${YELLOW}→ In Codex, run /hooks to review and trust generated hooks.${NC}"
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_track_project() {
|
|
69
|
+
local project_path="$1"
|
|
70
|
+
local _tracked_file="$HOME/.100xprism/tracked-projects"
|
|
71
|
+
mkdir -p "$(dirname "$_tracked_file")"
|
|
72
|
+
local _abs_path
|
|
73
|
+
_abs_path="$(cd "$project_path" && pwd)"
|
|
74
|
+
if ! grep -qxF "$_abs_path" "$_tracked_file" 2>/dev/null; then
|
|
75
|
+
echo "$_abs_path" >> "$_tracked_file"
|
|
76
|
+
fi
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Back-compat: old adapters call _run_generate with a single output file.
|
|
80
|
+
# Keep it as an alias for _run_concat with default 'concat' mode.
|
|
81
|
+
_run_generate() {
|
|
82
|
+
_run_concat "$@"
|
|
83
|
+
}
|