Compare commits
12 Commits
338176abbe
...
e7f7e786db
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7f7e786db | ||
|
|
6a10ce1333 | ||
|
|
2851250cc1 | ||
|
|
1eaec93c13 | ||
|
|
f8b61e6745 | ||
|
|
9e0557eeac | ||
|
|
e2919eef18 | ||
|
|
1fe6f71751 | ||
| c287ca3340 | |||
| f905ef6314 | |||
| 8d5cfb69fd | |||
| f1a973c42d |
40
.claude/commands/dintel-account-mgr.md
Normal file
40
.claude/commands/dintel-account-mgr.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
description: D.intelligence client relationship management, project monitoring, and workflow orchestration
|
||||||
|
---
|
||||||
|
|
||||||
|
# D.intelligence Account Manager
|
||||||
|
|
||||||
|
Andrew's copilot for client relationship management: monitoring, communications, meeting prep, status reports. Mixed autonomy.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "client status", "미팅 준비"
|
||||||
|
- "project monitoring", "client update"
|
||||||
|
|
||||||
|
## Monitoring Rules (Autonomous)
|
||||||
|
1. **Deadline Alert**: Tasks due within 3 days, not Done
|
||||||
|
2. **Stale Task Alert**: In Progress > 2 weeks without update
|
||||||
|
3. **High Priority Alert**: A1/A2 tasks not started
|
||||||
|
4. **Daily Scan**: Status changes across all client DBs
|
||||||
|
5. **Weekly Summary**: Completed/in-progress/blocked per client
|
||||||
|
|
||||||
|
## Client Codes & Notion DBs
|
||||||
|
| Code | Client | DB ID |
|
||||||
|
|------|--------|-------|
|
||||||
|
| JHR | Josun Hotel & Resort | `529587363fe04a8da588337e8eb1aa0b` |
|
||||||
|
| JAM | Jamie Clinic | `4d76fb5bb4134e81883c21a8c4aa80c1` |
|
||||||
|
| SLA | Shilla Hotel | `1e0581e58a1e80e28802d19bf8d468c7` |
|
||||||
|
| Tasks Dashboard | All | `2c0581e58a1e816d9948c3f3591c372c` |
|
||||||
|
|
||||||
|
## Workflow Chain A: New Client Onboarding
|
||||||
|
#77 starts -> #74 designs service -> #73 pricing -> #72 formats -> #71 reviews -> #70 compliance -> Andrew sends
|
||||||
|
|
||||||
|
## Chain B: Ongoing Monitoring
|
||||||
|
#77 monitors Notion -> #72 formats reports -> #70 reviews
|
||||||
|
|
||||||
|
## Meeting Prep
|
||||||
|
Client context, task progress table, discussion topics, open items, proposed next steps
|
||||||
|
|
||||||
|
## Communication
|
||||||
|
- Client-facing: ~주시면/~드리겠습니다 polite
|
||||||
|
- Internal Slack: ~합니다 formal
|
||||||
|
- Korean-first, bilingual notation, include Notion links
|
||||||
41
.claude/commands/dintel-backoffice-mgr.md
Normal file
41
.claude/commands/dintel-backoffice-mgr.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
description: D.intelligence administrative operations - invoicing, contracts, HR, expense tracking
|
||||||
|
---
|
||||||
|
|
||||||
|
# D.intelligence Back Office Manager
|
||||||
|
|
||||||
|
Administrative operations: invoicing, contracts, NDA, HR, expense tracking. Draft & Wait autonomy.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "계약서", "인보이스"
|
||||||
|
- "invoice", "contract", "NDA"
|
||||||
|
|
||||||
|
## Key Responsibilities
|
||||||
|
1. **Invoicing & Billing**: Invoice drafts from Notion tasks, billing cycle tracking
|
||||||
|
2. **Contract Management**: Service contracts, NDAs, renewal tracking
|
||||||
|
3. **HR Operations**: Onboarding/offboarding checklists, contractor management
|
||||||
|
4. **Expense Tracking**: Monthly summaries, budget tracking, subscriptions
|
||||||
|
5. **Compliance**: Tax document reminders, regulatory tracking
|
||||||
|
6. **Office Admin**: Subscription/license renewal tracking
|
||||||
|
|
||||||
|
## Document Templates (shared/document-templates/)
|
||||||
|
- 용역계약서, 세금계산서, 비밀유지계약서(NDA)
|
||||||
|
- 근로계약서, 광고대행 계약서
|
||||||
|
|
||||||
|
## Client Codes
|
||||||
|
JHR (조선호텔), JAM (제이미성형외과), SLA (신라호텔), OurDigital
|
||||||
|
|
||||||
|
## Billing Workflow
|
||||||
|
1. Service delivered -> #72 confirms -> #76 generates invoice draft
|
||||||
|
2. `[DRAFT - Awaiting Review]` -> Andrew approves -> Invoice sent
|
||||||
|
3. Payment tracked -> Hours reconciled with Notion
|
||||||
|
|
||||||
|
## Writing Conventions
|
||||||
|
- Invoices/contracts: ~합니다 formal
|
||||||
|
- Internal memos: ~다 concise
|
||||||
|
- Financial amounts: KRW with comma separators (5,000,000원)
|
||||||
|
|
||||||
|
## Guardrails
|
||||||
|
- Never send without Andrew's approval
|
||||||
|
- Never commit pricing without sign-off
|
||||||
|
- Korean-first, bilingual notation
|
||||||
39
.claude/commands/dintel-brand-editor.md
Normal file
39
.claude/commands/dintel-brand-editor.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
description: D.intelligence brand-compliant copywriting and style evaluation
|
||||||
|
---
|
||||||
|
|
||||||
|
# D.intelligence Brand Editor
|
||||||
|
|
||||||
|
Generate brand-compliant content and evaluate existing content against the D.intelligence writing style guide.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "write copy", "카피 작성"
|
||||||
|
- "D.intelligence 콘텐츠", "Magazine D."
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
1. **Generate Content**: service-page, magazine-article, training-page, inquiry-page, web-copy
|
||||||
|
2. **Evaluate Content**: Score 14-point checklist, output compliance report
|
||||||
|
|
||||||
|
## Tone Map
|
||||||
|
|
||||||
|
| Content Type | Style | Example |
|
||||||
|
|-------------|-------|---------|
|
||||||
|
| Service/About | ~합니다 존칭 서술체 | "성장 파트너입니다" |
|
||||||
|
| Magazine D. | ~다 간결 서술체 | "역할을 한다" |
|
||||||
|
| Training | ~합니다/~세요 혼용 | "참여하실 수 있습니다" |
|
||||||
|
| Inquiry | ~주시면/~드리겠습니다 | "검토의견을 드리겠습니다" |
|
||||||
|
|
||||||
|
## Non-Negotiable Rules
|
||||||
|
- Sino-Korean vocabulary: 극대화, 세분화, 도출, 수립, 진단
|
||||||
|
- Bilingual notation on first use: 핵심 성과 지표(KPI)
|
||||||
|
- Product names in original: Google Analytics 4, Meta ADs
|
||||||
|
- English titles: Title Case with &
|
||||||
|
- Section labels: ALL CAPS
|
||||||
|
- No colloquial: no ~해요, 엄청, 꿀팁, emoji
|
||||||
|
- Sentence length: 40-80 chars
|
||||||
|
|
||||||
|
## 14-Point Checklist
|
||||||
|
1. Bilingual notation 2. Logical flow 3. No colloquial 4. Sino-Korean vocabulary
|
||||||
|
5. Correct endings 6. Proper commas 7. Category tag 8. Brand expressions
|
||||||
|
9. 결론적으로 conclusion 10. Data-driven 11. Title Case 12. ALL CAPS labels
|
||||||
|
13. CTA language 14. Bilingual layering
|
||||||
42
.claude/commands/dintel-brand-guardian.md
Normal file
42
.claude/commands/dintel-brand-guardian.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
description: D.intelligence brand compliance review with 100-point checklist
|
||||||
|
---
|
||||||
|
|
||||||
|
# D.intelligence Brand Guardian
|
||||||
|
|
||||||
|
Auto-trigger on D.intelligence content review or creation. 100-point brand compliance checklist.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "brand check", "브랜드 검수"
|
||||||
|
- "D.intelligence", "디인텔리전스"
|
||||||
|
|
||||||
|
## Brand Identity
|
||||||
|
- **Name**: D.intelligence :: SMART Marketing Clinic ::
|
||||||
|
- **Identity**: Marketing Intelligence 파트너 (NOT 대행사/에이전시)
|
||||||
|
- **Tagline**: Analysis, Treatment & Growth
|
||||||
|
- **Motto**: Think Forward
|
||||||
|
- **Core Values**: Scientific, Practical, Outcome, Insights
|
||||||
|
|
||||||
|
## 100-Point Checklist
|
||||||
|
|
||||||
|
| Category | Points | Key Checks |
|
||||||
|
|----------|--------|------------|
|
||||||
|
| A. Tone & Manner | 25 | Professional/Scientific tone; no exaggeration |
|
||||||
|
| B. Message Structure | 25 | Problem -> Data Evidence -> Solution -> Outcome |
|
||||||
|
| C. Service Architecture | 25 | Correct A1-A6, T1-T7, G1-G4 codes |
|
||||||
|
| D. Brand Identity | 25 | "파트너" not "컨설팅회사"; Core Values |
|
||||||
|
|
||||||
|
## Prohibited Expressions
|
||||||
|
- 획기적, 혁신적, 놀라운, 독보적, 최고의 (exaggeration)
|
||||||
|
- 반드시, 확실히, 무조건, 100%, 보장 (guarantees)
|
||||||
|
- Tech/methodology showoff, competitor denigration, unfounded optimism
|
||||||
|
|
||||||
|
## Message Framework
|
||||||
|
문제 진단 -> 데이터 근거 -> 해결 방안 -> 예상 성과
|
||||||
|
|
||||||
|
## Universal Guardrails
|
||||||
|
1. Never send without Andrew's approval
|
||||||
|
2. Never delete -- always archive
|
||||||
|
3. Never commit pricing without sign-off
|
||||||
|
4. Korean-first, bilingual notation
|
||||||
|
5. Never cross-reference client data
|
||||||
47
.claude/commands/dintel-doc-secretary.md
Normal file
47
.claude/commands/dintel-doc-secretary.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
description: D.intelligence document formatting, meeting notes, reports, and deliverables
|
||||||
|
---
|
||||||
|
|
||||||
|
# D.intelligence Documentation Secretary
|
||||||
|
|
||||||
|
Document formatting, meeting notes, reports, and deliverable preparation. Draft & Wait autonomy.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "format document", "회의록"
|
||||||
|
- "meeting notes", "report formatting"
|
||||||
|
|
||||||
|
## Document Types
|
||||||
|
1. **Meeting Notes**: Attendees, agenda, decisions, action items
|
||||||
|
2. **Reports**: Analysis, monthly, audit reports
|
||||||
|
3. **Proposals**: Client proposals with service module references
|
||||||
|
4. **Presentations**: PPTX with brand formatting
|
||||||
|
5. **Internal Memos**: Brief internal communications
|
||||||
|
|
||||||
|
## Document Templates (shared/document-templates/)
|
||||||
|
- Initial Brief, Digital Marketing Audit, Marketing Performance
|
||||||
|
- Website Diagnosis, Social Media Excellence, Blank Template
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
1. Receive raw content or meeting data
|
||||||
|
2. Apply template and brand formatting
|
||||||
|
3. Output draft with `[DRAFT - Awaiting Review]` watermark
|
||||||
|
4. **STOP and wait for Andrew's review**
|
||||||
|
5. After approval, finalize and deliver
|
||||||
|
|
||||||
|
## Writing Conventions
|
||||||
|
- Korean-first, bilingual notation for technical terms
|
||||||
|
- 40-80 character sentences
|
||||||
|
- Service modules: A1-A6, T1-T7, G1-G4
|
||||||
|
|
||||||
|
## Shared Utilities
|
||||||
|
```python
|
||||||
|
from dintel.brand import BRAND_NAME, COLOR_PRIMARY, SERVICE_MODULES
|
||||||
|
from dintel.document import create_branded_doc, save_doc
|
||||||
|
from dintel.notion import DB_TASKS_DASHBOARD, CLIENT_DB_MAP
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chain Collaborators
|
||||||
|
- #71 Brand Editor -> writing quality
|
||||||
|
- #70 Brand Guardian -> brand compliance
|
||||||
|
- #77 Account Manager -> client deliverables
|
||||||
|
- #76 Back Office -> internal documents
|
||||||
41
.claude/commands/dintel-marketing-mgr.md
Normal file
41
.claude/commands/dintel-marketing-mgr.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
description: D.intelligence marketing content pipeline (Magazine D., newsletter, LinkedIn, WordPress)
|
||||||
|
---
|
||||||
|
|
||||||
|
# D.intelligence Marketing Manager
|
||||||
|
|
||||||
|
Manage content pipeline: Magazine D. articles, newsletters, LinkedIn posts, WordPress content. Draft & Wait autonomy.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "콘텐츠 발행", "newsletter"
|
||||||
|
- "Magazine D.", "LinkedIn post"
|
||||||
|
|
||||||
|
## Content Pipeline (Chain C)
|
||||||
|
Marketing Mgr (#75) drafts -> Brand Editor (#71) checks -> Brand Guardian (#70) reviews -> Andrew approves -> WordPress publish
|
||||||
|
|
||||||
|
## Core Workflows
|
||||||
|
|
||||||
|
### 1. Magazine D. Article
|
||||||
|
- Structure: [카테고리 태그] 제목 -> 도입 -> 본론 1-2 -> 결론(결론적으로) -> 참고 링크
|
||||||
|
- Tone: ~다 간결 서술체, 논리적, 실무 관점
|
||||||
|
|
||||||
|
### 2. Newsletter
|
||||||
|
- 핵심 수치 + 실무 시사점 structure
|
||||||
|
- Industry insights, Magazine D. links, event announcements
|
||||||
|
|
||||||
|
### 3. LinkedIn Posts
|
||||||
|
- 업계 인사이트 + 적용 방법, data-driven, English hashtags
|
||||||
|
|
||||||
|
### 4. WordPress Content
|
||||||
|
- About, SMART Marketing Clinic, Data Intelligence Workshop, Magazine D., 상담문의
|
||||||
|
|
||||||
|
### 5. Content Calendar
|
||||||
|
- IDEA -> DRAFT -> REVIEW -> APPROVED -> PUBLISHED
|
||||||
|
|
||||||
|
### 6. SEO Integration
|
||||||
|
- Strategic keywords, meta optimization, internal linking
|
||||||
|
|
||||||
|
## Writing Rules
|
||||||
|
- Sino-Korean vocabulary, bilingual notation, Title Case English
|
||||||
|
- ALL CAPS section labels, no colloquial expressions
|
||||||
|
- 40-80 char sentences, 결론적으로 conclusion for articles
|
||||||
40
.claude/commands/dintel-quotation-mgr.md
Normal file
40
.claude/commands/dintel-quotation-mgr.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
description: D.intelligence quotation generation with multi-agent sub-system
|
||||||
|
---
|
||||||
|
|
||||||
|
# D.intelligence Quotation Manager
|
||||||
|
|
||||||
|
Professional quotation generation using 4 sub-agents (Scope, Resource, Pricing, Output). Draft & Wait autonomy.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "견적서", "quotation"
|
||||||
|
- "estimate", "pricing draft"
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
1. Receive client brief
|
||||||
|
2. **Scope Agent**: Map requirements to modules (A1-G4)
|
||||||
|
3. **Resource Agent**: Estimate hours, timeline, team
|
||||||
|
4. **Pricing Agent**: Calculate prices, apply discounts
|
||||||
|
5. **Output Generator**: Produce branded .xlsx
|
||||||
|
6. **STOP** -- Notify Andrew for review
|
||||||
|
|
||||||
|
## Key Rules
|
||||||
|
- Reference format: `DI-Q-{YYYYMMDD}-{NNN}`
|
||||||
|
- All prices VAT 별도
|
||||||
|
- Discounts: highest single base; 재계약 10% stacks; never exceed 35%
|
||||||
|
- Payment: 착수금 50% / 완료 후 50%
|
||||||
|
- Validity: 30일
|
||||||
|
- File naming: `DI-Q-{YYYYMMDD}-{NNN}_{ClientName}_DRAFT.xlsx`
|
||||||
|
|
||||||
|
## Quotation Templates (shared/quotation-templates/)
|
||||||
|
- Standard 2026, GA Analytics, Content Marketing, GA Training
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
```bash
|
||||||
|
python scripts/generate_quotation.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Guardrails
|
||||||
|
- CRITICAL: Never commit pricing without Andrew's sign-off
|
||||||
|
- All generated files marked as DRAFT with yellow-highlighted price cells
|
||||||
|
- Never send to clients without approval
|
||||||
46
.claude/commands/dintel-service-architect.md
Normal file
46
.claude/commands/dintel-service-architect.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
description: D.intelligence service scope design and module recommendation
|
||||||
|
---
|
||||||
|
|
||||||
|
# D.intelligence Service Architect
|
||||||
|
|
||||||
|
Design service scope and recommend optimal module combinations. Inquiry-driven workflow.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "서비스 설계", "service design"
|
||||||
|
- "module recommendation", "서비스 구성"
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### Step 1: Structured Inquiry (7 Questions)
|
||||||
|
1. Company Profile (industry, size, B2B/B2C)
|
||||||
|
2. Current Marketing Setup (channels, tools, team)
|
||||||
|
3. Primary Pain Points (top 3 ranked)
|
||||||
|
4. Business Goals (6-12 month KPIs)
|
||||||
|
5. Budget Range (flag for Andrew)
|
||||||
|
6. Timeline (start, milestones, deadlines)
|
||||||
|
7. Previous Experience (past consulting)
|
||||||
|
|
||||||
|
### Step 2: Pain Point -> Module Mapping
|
||||||
|
Examples: Data without interpretation -> A3+T3; No marketing structure -> A5+T5; Invisible in search -> A3+T6
|
||||||
|
|
||||||
|
### Step 3: Package Recommendation
|
||||||
|
|
||||||
|
| Package | Modules | Duration |
|
||||||
|
|---------|---------|----------|
|
||||||
|
| Starter | A3+A4+A5 | 4-6 weeks |
|
||||||
|
| Standard | Starter + T3/T5/T6 | 8-12 weeks |
|
||||||
|
| Premium | Starter + 2T + 1G | 4-6 months |
|
||||||
|
| SEO Intensive | A3+T6+G2 | 3-4 months |
|
||||||
|
|
||||||
|
### Step 4: Scope Document Output
|
||||||
|
Client overview, identified needs, recommended package, deliverables, timeline, notes for #73
|
||||||
|
|
||||||
|
### Step 5: Handoff to Quotation Manager (#73)
|
||||||
|
|
||||||
|
## Module Rules
|
||||||
|
- Always start with Analysis phase
|
||||||
|
- A3 = most common entry point
|
||||||
|
- T6 (Brand Visibility Treatment) = signature service
|
||||||
|
- Max 3 Analysis + 2 Treatment for initial engagement
|
||||||
|
- This agent does NOT set prices
|
||||||
44
.claude/commands/dintel-skill-update.md
Normal file
44
.claude/commands/dintel-skill-update.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
description: D.intelligence cross-skill consistency management meta-agent
|
||||||
|
---
|
||||||
|
|
||||||
|
# D.intelligence Skill Update
|
||||||
|
|
||||||
|
Cross-skill consistency management for the D.intelligence Agent Corps. Triggered by Andrew only.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "skill sync", "스킬 업데이트"
|
||||||
|
- "propagate change", "consistency check"
|
||||||
|
|
||||||
|
## Source of Truth (dintel-shared/)
|
||||||
|
- `src/dintel/brand.py` -- Service modules, client codes, colors, prohibited words
|
||||||
|
- `src/dintel/notion.py` -- DB IDs, status options, field definitions
|
||||||
|
- `src/dintel/document.py` -- DOCX utilities
|
||||||
|
- `src/dintel/excel.py` -- Excel utilities
|
||||||
|
- `references/` -- Brand guide, Notion schema, pricing, design system
|
||||||
|
|
||||||
|
## Dependency Map
|
||||||
|
|
||||||
|
| Change | Update Skills |
|
||||||
|
|--------|-------------|
|
||||||
|
| Pricing | #73, #74, #76 |
|
||||||
|
| Brand Guide | #70, #71, #72, #75 |
|
||||||
|
| Service Modules | #70, #71, #73, #74, #75 |
|
||||||
|
| Notion Schema | #72, #76, #77 |
|
||||||
|
| Client Codes | #72, #73, #76, #77 |
|
||||||
|
| Prohibited Words | #70, #71, #75 |
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
1. Andrew describes change
|
||||||
|
2. Look up Dependency Map -> list affected skills
|
||||||
|
3. Present Impact Report -> wait for approval
|
||||||
|
4. Update `dintel-shared/` first (source of truth)
|
||||||
|
5. Propagate to each affected skill
|
||||||
|
6. Run consistency verification
|
||||||
|
7. Log to `shared/change-log.md`
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
- Never execute without Andrew's approval
|
||||||
|
- Update source of truth FIRST, then propagate
|
||||||
|
- One change type at a time
|
||||||
|
- Always log changes and verify consistency
|
||||||
42
.claude/commands/gtm-editor.md
Normal file
42
.claude/commands/gtm-editor.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
description: GTM tag/trigger/variable creation via API with ES5 Custom HTML and dataLayer code generation
|
||||||
|
---
|
||||||
|
|
||||||
|
# GTM Editor
|
||||||
|
|
||||||
|
Create, modify, and deploy GTM configurations via API. Generates ES5-compliant Custom HTML tags.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "create GTM tag", "generate dataLayer", "modify trigger"
|
||||||
|
- "update variable", "write custom HTML", "manage GTM"
|
||||||
|
|
||||||
|
## MANDATORY: dataLayer First Workflow
|
||||||
|
|
||||||
|
1. **Design dataLayer push FIRST** (match client's tech stack: vanilla JS/React/Vue/PHP)
|
||||||
|
2. **Ask user**: "Can your devs add this to the page?"
|
||||||
|
3. **Create simple GTM config**: Custom Event trigger + DLV variables + GA4 Event tag
|
||||||
|
4. **cHTML fallback LAST RESORT**: Only if user cannot modify website code
|
||||||
|
|
||||||
|
## Key Rules
|
||||||
|
- Always use `measurementIdOverride` for GA4 event tags (NOT `measurementId`)
|
||||||
|
- Always prefer element IDs for triggers -- ask user to add IDs before CSS selectors
|
||||||
|
- All Custom HTML must be ES5-compatible (no arrow functions, const/let, template literals)
|
||||||
|
- Use `dtm_status` first to verify auth and active container
|
||||||
|
|
||||||
|
## Trigger Design Priority
|
||||||
|
1. Element ID (`Click ID equals "hero-btn-consult"`)
|
||||||
|
2. Data attribute (`[data-track="consult"]`)
|
||||||
|
3. Form ID (`Form ID equals "contact-form"`)
|
||||||
|
4. CSS selector (last resort -- document which selectors)
|
||||||
|
|
||||||
|
## Supported Tag Types
|
||||||
|
- `googtag` (GA4 config), `gaawe` (GA4 events), `awct` (Ads conversion)
|
||||||
|
- `html` (Custom HTML for Meta, PostHog, Kakao), `gclidw` (Conversion Linker)
|
||||||
|
|
||||||
|
## Korean Market Patterns
|
||||||
|
- Currency: KRW (no decimals)
|
||||||
|
- Payment buttons: 장바구니, 결제하기, 문의하기
|
||||||
|
- Platforms: Kakao Pixel, Naver Analytics
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
1. Analyze (Chrome DevTools) -> 2. Design events -> 3. Create via DTM Agent -> 4. Hand to gtm-validator -> 5. Document in Notion
|
||||||
49
.claude/commands/gtm-guardian.md
Normal file
49
.claude/commands/gtm-guardian.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
description: GTM lifecycle automation - progressive audit, version comparison, and lookup app
|
||||||
|
---
|
||||||
|
|
||||||
|
# GTM Guardian
|
||||||
|
|
||||||
|
GTM tagging lifecycle automation: progressive audit (Phase 6) and event lookup app (Phase 7).
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "GTM audit lifecycle", "container analysis"
|
||||||
|
- "GTM 유지보수", "버전 비교"
|
||||||
|
|
||||||
|
## Quick Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone D.intelligence GTM Toolkit
|
||||||
|
git clone https://github.com/ourdigital/dintel-gtm-agent.git
|
||||||
|
|
||||||
|
# Container analysis
|
||||||
|
python analyze_container.py GTM-XXXXXX.json --output report.md
|
||||||
|
|
||||||
|
# Version comparison
|
||||||
|
python diff_versions.py v1.json v2.json --output diff.md
|
||||||
|
|
||||||
|
# Unused element detection
|
||||||
|
python find_unused.py container.json --type all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 6: Progressive Audit
|
||||||
|
|
||||||
|
| Feature | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| Container Analysis | JSON parsing, structure analysis |
|
||||||
|
| Dependency Mapping | Tag-trigger-variable relationships |
|
||||||
|
| Version Diff | Change tracking between versions |
|
||||||
|
| Tag Validation | Automatic firing state verification |
|
||||||
|
|
||||||
|
### Audit Schedule
|
||||||
|
- Weekly: Tag firing validation
|
||||||
|
- Monthly: Full container review
|
||||||
|
- Quarterly: Architecture review
|
||||||
|
|
||||||
|
## Phase 7: Lookup App
|
||||||
|
Google Apps Script-based Event Taxonomy lookup app (Google Sheets -> Apps Script -> Web App).
|
||||||
|
|
||||||
|
## Notion Output
|
||||||
|
- Database: GTM Knowledge Base
|
||||||
|
- Properties: Project, Audit Date, Container ID, Status, Issues Count
|
||||||
|
- Reports in Korean; technical terms in English
|
||||||
42
.claude/commands/gtm-validator.md
Normal file
42
.claude/commands/gtm-validator.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
description: GTM QA - tag firing verification, trigger testing, naming conventions, cross-platform validation
|
||||||
|
---
|
||||||
|
|
||||||
|
# GTM Validator
|
||||||
|
|
||||||
|
Verify GTM implementations on live pages. Test triggers, validate dataLayer, check naming conventions.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "validate tags", "QA GTM", "debug GTM"
|
||||||
|
- "naming conventions", "GTM best practice"
|
||||||
|
|
||||||
|
## Validation Modes
|
||||||
|
|
||||||
|
### 1. Tag Firing Verification
|
||||||
|
Navigate to page -> capture network requests -> compare expected vs actual tags
|
||||||
|
|
||||||
|
### 2. Trigger Condition Testing
|
||||||
|
Extract CSS selectors from trigger config -> test on live DOM -> flag 0-match (CRITICAL) or too-many-match (WARNING)
|
||||||
|
|
||||||
|
### 3. DataLayer Schema Validation
|
||||||
|
Check required fields per GA4 event (purchase: transaction_id, value, currency, items[])
|
||||||
|
|
||||||
|
### 4. Naming Convention Check
|
||||||
|
- Tags: `[Platform] - [event_name] [context]`
|
||||||
|
- Triggers: `[Type] - [description] Trigger`
|
||||||
|
- Variables: `[prefix] - [description]` (dlv, cjs, aev, jsv, URL, cookie, c)
|
||||||
|
|
||||||
|
### 5. Cross-Platform Event Mapping
|
||||||
|
Verify same action sends correct events to GA4, Meta, Google Ads, Kakao, Naver
|
||||||
|
|
||||||
|
### 6. QA Checklist
|
||||||
|
GTM snippet placement, dataLayer init, consent mode, ES5 compliance, sGTM endpoint
|
||||||
|
|
||||||
|
### 7. Version Comparison
|
||||||
|
Compare tag counts and changes between container versions
|
||||||
|
|
||||||
|
## Key Rules
|
||||||
|
- Test on LIVE published version (not preview, unless debugging)
|
||||||
|
- Test on both desktop and mobile viewports
|
||||||
|
- CSS selector triggers = fragility risk; recommend adding IDs
|
||||||
|
- Document every test with screenshots/network evidence
|
||||||
51
.claude/commands/jamie-brand-audit.md
Normal file
51
.claude/commands/jamie-brand-audit.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
description: Jamie Clinic brand compliance auditor for content review and regulatory checks
|
||||||
|
---
|
||||||
|
|
||||||
|
# Jamie Brand Audit
|
||||||
|
|
||||||
|
Review existing content for brand alignment, tone/voice, and regulatory compliance. Guidance-only skill (no scripts).
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "review content", "brand audit"
|
||||||
|
- "Jamie 브랜드 검수", "콘텐츠 검토"
|
||||||
|
|
||||||
|
## Audit Process
|
||||||
|
|
||||||
|
1. Receive content to review
|
||||||
|
2. Check brand compliance against guidelines
|
||||||
|
3. Verify tone/voice alignment
|
||||||
|
4. Flag regulatory issues (medical advertising laws)
|
||||||
|
5. Provide feedback with specific corrections
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
|
||||||
|
### Voice & Tone
|
||||||
|
- 90% 격식체 ratio
|
||||||
|
- Correct honorifics (환자분/고객님)
|
||||||
|
- Jamie personality traits
|
||||||
|
|
||||||
|
### Brand Alignment
|
||||||
|
- Slogan consistency, core values reflected
|
||||||
|
- No competitor mentions
|
||||||
|
|
||||||
|
### Regulatory Compliance
|
||||||
|
- No exaggerated claims or guarantee language
|
||||||
|
- Proper disclosures
|
||||||
|
|
||||||
|
### Technical Accuracy
|
||||||
|
- Procedure facts correct, medical terms accurate
|
||||||
|
- Recovery times realistic
|
||||||
|
|
||||||
|
## Journal Graphic Review
|
||||||
|
- Background: Light Blue Gray (#E0E5EB)
|
||||||
|
- Text: Soft Black (#333333), sans-serif
|
||||||
|
- Medical terms include English translation
|
||||||
|
- No saturated colors, medical schematic style
|
||||||
|
- Sufficient white space, grid-based layout
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- Overall compliance score
|
||||||
|
- Specific issues with line references
|
||||||
|
- Suggested corrections
|
||||||
|
- Approval/revision recommendation
|
||||||
45
.claude/commands/jamie-brand-editor.md
Normal file
45
.claude/commands/jamie-brand-editor.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
description: Jamie Clinic branded content generation with medical compliance checking
|
||||||
|
---
|
||||||
|
|
||||||
|
# Jamie Brand Editor
|
||||||
|
|
||||||
|
Content generation for Jamie Plastic Surgery Clinic following brand voice and Korean medical advertising regulations.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "write Jamie blog", "Jamie content"
|
||||||
|
- "Jamie 콘텐츠", "제이미 블로그"
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/compliance_checker.py --input draft.md
|
||||||
|
python scripts/compliance_checker.py --input draft.md --verbose --output report.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Brand Voice
|
||||||
|
- 90% 격식체 (~습니다/~입니다)
|
||||||
|
- "환자분" for medical contexts
|
||||||
|
- "자연스러운" as key descriptor
|
||||||
|
- No exaggeration, realistic expectations
|
||||||
|
|
||||||
|
## Content Types
|
||||||
|
- Blog posts (블로그 포스팅)
|
||||||
|
- Procedure pages (시술 페이지)
|
||||||
|
- Ad copy (광고 카피)
|
||||||
|
- Social media (SNS 콘텐츠)
|
||||||
|
|
||||||
|
## Compliance Rules (Korean Medical Ad Law)
|
||||||
|
- No exaggerated claims or guarantee language
|
||||||
|
- No before/after comparison violations
|
||||||
|
- No competitor comparisons
|
||||||
|
- Proper disclosure requirements
|
||||||
|
|
||||||
|
## Journal Graphic Style
|
||||||
|
- Background: Light Blue Gray (#E0E5EB), Text: Soft Black (#333333)
|
||||||
|
- Medical schematic illustrations, clean line art
|
||||||
|
- Sans-serif fonts, generous white space
|
||||||
|
- Medical terms must include English in parentheses
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
1. Generate content -> 2. Run compliance checker -> 3. Fix issues -> 4. Submit to jamie-brand-audit
|
||||||
42
.claude/commands/jamie-instagram-manager.md
Normal file
42
.claude/commands/jamie-instagram-manager.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
description: Instagram account management for Jamie Plastic Surgery Clinic
|
||||||
|
---
|
||||||
|
|
||||||
|
# Jamie Instagram Manager
|
||||||
|
|
||||||
|
Instagram content planning, caption writing, comment replies, and performance analysis for Jamie Clinic.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "Instagram management", "IG strategy"
|
||||||
|
- "인스타그램 관리", "인스타 콘텐츠"
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
- Account performance analysis from Insights data
|
||||||
|
- Comment reply drafting (brand-aligned tone)
|
||||||
|
- Content calendar generation (weekly/monthly)
|
||||||
|
- Caption writing with hashtag optimization
|
||||||
|
- Reels/Stories idea generation
|
||||||
|
|
||||||
|
## Content 4 Pillars
|
||||||
|
1. **Q&A Reels (40%)**: YouTube Q&A -> 30-60s reels, Dark theme
|
||||||
|
2. **Daily Life (30%)**: Hospital atmosphere, staff, meals; photo-centric
|
||||||
|
3. **Card News (20%)**: Procedure info summaries, carousel 5-7 slides
|
||||||
|
4. **Patient Episodes (10%)**: Real model stories with disclaimer
|
||||||
|
|
||||||
|
## Brand Voice
|
||||||
|
- 해요체 60% + 습니다체 40% (friendly but restrained)
|
||||||
|
- "여러분" preferred; Max 1-2 soft emoji
|
||||||
|
- Slogan: "티안나게 수술하고, 티나게 예뻐지는"
|
||||||
|
|
||||||
|
## Posting Schedule
|
||||||
|
- Feed: 2-3x/week (1080x1350px)
|
||||||
|
- Stories: Daily 1-2x
|
||||||
|
- Reels: 1-2x/week (30-60s)
|
||||||
|
- Best times: 9AM, 12PM, 7PM
|
||||||
|
|
||||||
|
## DM Conversion Funnel
|
||||||
|
Instagram -> DM -> KakaoTalk -> Consultation -> Booking (30% target)
|
||||||
|
|
||||||
|
## Medical Compliance
|
||||||
|
- No price disclosure, no testimonial quotes, no competitor comparison
|
||||||
|
- B&A posts require disclaimer
|
||||||
39
.claude/commands/jamie-youtube-manager.md
Normal file
39
.claude/commands/jamie-youtube-manager.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
description: YouTube SEO auditor and content manager for Jamie Plastic Surgery Clinic
|
||||||
|
---
|
||||||
|
|
||||||
|
# Jamie YouTube Manager
|
||||||
|
|
||||||
|
YouTube SEO audit, metadata optimization, schema generation, and API batch updates for Jamie Clinic (18 videos).
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "YouTube SEO", "YT optimization"
|
||||||
|
- "Jamie 유튜브", "영상 최적화"
|
||||||
|
|
||||||
|
## API Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Project/our-claude-skills/custom-skills/43-jamie-youtube-manager/code/scripts
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
python jamie_youtube_api_test.py # Test connection
|
||||||
|
python jamie_video_info.py https://youtu.be/ID -v # Get video info
|
||||||
|
python jamie_channel_status.py # Channel status
|
||||||
|
python jamie_youtube_batch_update.py --dry-run # Preview batch update
|
||||||
|
python jamie_youtube_batch_update.py --execute # Execute update
|
||||||
|
```
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
- Video SEO Audit (score /100): title, description, chapters, captions, schema readiness
|
||||||
|
- Metadata Optimization: title formula `[시술명] + [핵심 베네핏] | 제이미성형외과`
|
||||||
|
- Description Generation with timestamps, CTA, contact info, disclaimers
|
||||||
|
- Chapter/Timestamp Generation from transcripts
|
||||||
|
- VideoObject Schema (JSON-LD) with Clip schema for chapters
|
||||||
|
- Shorts Optimization checklist
|
||||||
|
- Batch metadata update via YouTube API
|
||||||
|
|
||||||
|
## Medical Compliance
|
||||||
|
- Educational explanations allowed; testimonials/guarantees prohibited
|
||||||
|
- Required disclaimer: `※ 본 영상은 정보 제공 목적이며, 개인에 따라 결과가 다를 수 있습니다.`
|
||||||
|
|
||||||
|
## i18n Priority: Korean (required) > English (high) > Japanese/Chinese (medium)
|
||||||
50
.claude/commands/jamie-youtube-subtitle-checker.md
Normal file
50
.claude/commands/jamie-youtube-subtitle-checker.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
description: YouTube SBV subtitle correction and metadata generation for Jamie Clinic
|
||||||
|
---
|
||||||
|
|
||||||
|
# Jamie YouTube Subtitle Checker
|
||||||
|
|
||||||
|
SBV subtitle file typo correction and YouTube metadata generation.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "check subtitles", "subtitle QA"
|
||||||
|
- "자막 교정", "SBV 파일"
|
||||||
|
|
||||||
|
## Quick Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SBV typo correction
|
||||||
|
python scripts/sbv_corrector.py --input "subtitle.sbv" --output "subtitle_corrected.sbv"
|
||||||
|
|
||||||
|
# Chapter extraction
|
||||||
|
python scripts/chapter_extractor.py --input "subtitle.sbv"
|
||||||
|
|
||||||
|
# Full workflow (correction + chapters + metadata)
|
||||||
|
python scripts/process_subtitle.py --input "subtitle.sbv" --output-dir "./output"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
1. SBV parsing and text extraction
|
||||||
|
2. Typo auto-correction (typo_dictionary.json)
|
||||||
|
3. Medical terminology standardization
|
||||||
|
4. Chapter timestamp extraction
|
||||||
|
5. YouTube metadata generation
|
||||||
|
|
||||||
|
## Key Typo Patterns
|
||||||
|
|
||||||
|
| Category | Wrong | Correct |
|
||||||
|
|----------|-------|---------|
|
||||||
|
| Brand | 데이미, 재이미 | 제이미 |
|
||||||
|
| Procedure | 쌍거풀, 쌍커풀 | 쌍꺼풀 |
|
||||||
|
| Medical | 절계, 절게 | 절개 |
|
||||||
|
|
||||||
|
## Output Files
|
||||||
|
- `{filename}_corrected.sbv` -- Corrected subtitles
|
||||||
|
- `youtube_video_info.md` -- YouTube metadata (title, chapters, description, hashtags)
|
||||||
|
- `correction_report.md` -- Typo change log
|
||||||
|
|
||||||
|
## Quality Checklist
|
||||||
|
- Brand name "제이미성형외과" correct
|
||||||
|
- Procedure names use official terminology
|
||||||
|
- Chapters include 0:00 intro
|
||||||
|
- 3-5 hashtags included
|
||||||
35
.claude/commands/mac-optimizer.md
Normal file
35
.claude/commands/mac-optimizer.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
description: macOS system health audit and optimization (packages, environment, security, cleanup, resources)
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mac Optimizer
|
||||||
|
|
||||||
|
Run macOS system health audits and apply optimizations with user consent.
|
||||||
|
|
||||||
|
## Module Routing
|
||||||
|
|
||||||
|
| Keywords | Sub-command |
|
||||||
|
|----------|-------------|
|
||||||
|
| brew, npm, pip, packages, update, outdated | `/mac-packages` |
|
||||||
|
| path, shell, zshrc, environment, env vars | `/mac-environment` |
|
||||||
|
| security, firewall, sip, gatekeeper, filevault | `/mac-security` |
|
||||||
|
| cache, cleanup, logs, disk space, trash | `/mac-cleanup` |
|
||||||
|
| cpu, memory, disk, battery, processes, slow | `/mac-resources` |
|
||||||
|
| doctor, full check, health, everything | `/mac-doctor` (runs all 5) |
|
||||||
|
|
||||||
|
Default to **/mac-doctor** when the request is ambiguous.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Audit** — run read-only scripts, collect JSON findings
|
||||||
|
2. **Report** — present findings grouped by severity (critical → warning → info)
|
||||||
|
3. **Consent** — ask which actions to perform
|
||||||
|
4. **Execute** — dry-run first, then apply approved actions only
|
||||||
|
|
||||||
|
## Safety Rules
|
||||||
|
|
||||||
|
- Never execute cleanup without explicit user approval
|
||||||
|
- Always show sizes before deleting anything
|
||||||
|
- No sudo by default — ask first if needed
|
||||||
|
- Security module is read-only (findings + guidance only)
|
||||||
|
- Back up shell configs before modifying
|
||||||
55
.claude/commands/our-settings-audit.md
Normal file
55
.claude/commands/our-settings-audit.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
description: Claude Code settings optimization and token audit
|
||||||
|
---
|
||||||
|
|
||||||
|
# Claude Code Settings Optimizer
|
||||||
|
|
||||||
|
Self-audit and optimize Claude Code configuration for maximum token efficiency and performance.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "settings audit", "exceed response limit", "MCP error"
|
||||||
|
- "token usage", "context budget"
|
||||||
|
|
||||||
|
## Quick Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 scripts/run_audit.py
|
||||||
|
python3 scripts/analyze_tokens.py
|
||||||
|
python3 scripts/auto_fix.py --apply
|
||||||
|
```
|
||||||
|
|
||||||
|
## Audit Scope
|
||||||
|
|
||||||
|
### Token Budget Targets
|
||||||
|
|
||||||
|
| Component | Target | Max |
|
||||||
|
|-----------|--------|-----|
|
||||||
|
| CLAUDE.md | 2,000 | 3,000 |
|
||||||
|
| MCP Servers | 5,000 | 10,000 |
|
||||||
|
| Skills metadata | 500 | 1,000 |
|
||||||
|
| Working space | >140,000 | -- |
|
||||||
|
|
||||||
|
### MCP Server Strategy
|
||||||
|
|
||||||
|
| Strategy | Criteria |
|
||||||
|
|----------|----------|
|
||||||
|
| `always` | Essential for daily work (Playwright, Notion) |
|
||||||
|
| `lazy` | Occasionally needed (GitHub, Slack) |
|
||||||
|
| `disable` | Rarely used or token-heavy |
|
||||||
|
|
||||||
|
Every MCP server MUST have `serverInstructions` for Tool Search.
|
||||||
|
|
||||||
|
### CLAUDE.md Health Checks
|
||||||
|
- Line count (warn >200), token estimate (warn >3,000)
|
||||||
|
- Structure quality, redundant info, unnecessary repetition
|
||||||
|
|
||||||
|
### Auto-Fix (safe, with backup)
|
||||||
|
- Add `serverInstructions` to MCP servers
|
||||||
|
- Add frontmatter to commands missing it
|
||||||
|
- Suggest CLAUDE.md compression
|
||||||
|
|
||||||
|
## Output Report
|
||||||
|
- Token budget breakdown and percentages
|
||||||
|
- Health status (Good/Needs Attention/Critical)
|
||||||
|
- Findings (Critical/Warnings/Passing)
|
||||||
|
- Prioritized recommendations
|
||||||
38
.claude/commands/ourdigital-ad-manager.md
Normal file
38
.claude/commands/ourdigital-ad-manager.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
description: Ad copywriting and keyword research for OurDigital marketing
|
||||||
|
---
|
||||||
|
|
||||||
|
# OurDigital Ad Manager
|
||||||
|
|
||||||
|
Automated ad copywriting and keyword research. Activated with "ourdigital" keyword.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "ourdigital ad copy", "ourdigital 광고 카피"
|
||||||
|
- "ourdigital keyword", "ourdigital 키워드"
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate Google Ads copy
|
||||||
|
python code/scripts/generate_ads.py --product "SEO 컨설팅" --platform google --variations 3
|
||||||
|
|
||||||
|
# Keyword research
|
||||||
|
python code/scripts/keyword_research.py --seed "데이터 분석" --market korea
|
||||||
|
|
||||||
|
# Bulk ad generation
|
||||||
|
python code/scripts/generate_ads.py --input keywords.csv --platform naver --output ads.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
## Platform Specs
|
||||||
|
|
||||||
|
| Platform | Headline | Description |
|
||||||
|
|----------|----------|-------------|
|
||||||
|
| Google | 30x3 chars | 90x2 chars |
|
||||||
|
| Naver | 25 chars | 45 chars |
|
||||||
|
| Meta | 40 chars | 125 chars |
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
- Single/bulk ad copy generation with USP and CTA
|
||||||
|
- Keyword volume research with competitor analysis
|
||||||
|
- Competitor ad analysis
|
||||||
|
- Output: JSON, CSV (bulk upload), Markdown report
|
||||||
39
.claude/commands/ourdigital-backoffice.md
Normal file
39
.claude/commands/ourdigital-backoffice.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
description: Business document creation (quotes, proposals, cost analysis) for OurDigital
|
||||||
|
---
|
||||||
|
|
||||||
|
# OurDigital Backoffice
|
||||||
|
|
||||||
|
Business document generation with pricing automation. Activated with "ourdigital" keyword.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "ourdigital quote", "ourdigital 견적서"
|
||||||
|
- "ourdigital proposal", "ourdigital 제안서"
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate quote
|
||||||
|
python code/scripts/generate_quote.py --client "ABC Company" --services "seo_audit,gtm_setup"
|
||||||
|
|
||||||
|
# Create proposal
|
||||||
|
python code/scripts/create_proposal.py --client "XYZ Corp" --project "마케팅 자동화" --duration "3months"
|
||||||
|
|
||||||
|
# Cost analysis
|
||||||
|
python code/scripts/cost_analysis.py --project "GTM 구축" --investment 5000000 --period 12months
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service Codes & Base Prices
|
||||||
|
|
||||||
|
| Code | Service | Price |
|
||||||
|
|------|---------|-------|
|
||||||
|
| `seo_audit` | SEO Technical Audit | 4,000,000 |
|
||||||
|
| `seo_monthly` | SEO Monthly | 2,000,000/월 |
|
||||||
|
| `gtm_setup` | GTM Setup | 3,000,000 |
|
||||||
|
| `ga4_setup` | GA4 Implementation | 2,000,000 |
|
||||||
|
| `dashboard` | Dashboard Dev | 4,000,000 |
|
||||||
|
| `consulting` | Consulting Day | 1,500,000/일 |
|
||||||
|
| `workshop` | Workshop | 1,500,000/회 |
|
||||||
|
|
||||||
|
## Output Formats
|
||||||
|
- Markdown (default), PDF, DOCX
|
||||||
42
.claude/commands/ourdigital-blog.md
Normal file
42
.claude/commands/ourdigital-blog.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
description: Korean blog drafting for blog.ourdigital.org with Ghost CMS integration
|
||||||
|
---
|
||||||
|
|
||||||
|
# OurDigital Blog
|
||||||
|
|
||||||
|
Korean blog draft creation for blog.ourdigital.org. Activated with "ourdigital" keyword.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "ourdigital blog", "ourdigital 블로그"
|
||||||
|
- "ourdigital 한국어 포스트"
|
||||||
|
|
||||||
|
## Channel Profile
|
||||||
|
|
||||||
|
- URL: blog.ourdigital.org
|
||||||
|
- Language: Korean (영문 전문용어 병기)
|
||||||
|
- Tone: Analytical, Educational
|
||||||
|
- Platform: Ghost CMS
|
||||||
|
- Length: 1,500-3,000자
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Topic Clarification**: 주제, 타겟 독자, 깊이 수준
|
||||||
|
2. **Draft Structure**: 도입부(Hook) -> 본론(3-5 포인트) -> 결론(Summary + 열린 질문)
|
||||||
|
3. **Writing Style**: 분석적+개인적 톤, 한글 기본 영문 병기, 관찰->분석->함의
|
||||||
|
4. **SEO Metadata**: title(60자), meta_description(155자), slug, tags
|
||||||
|
5. **Output**: Markdown with YAML frontmatter
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python shared/scripts/export_blog.py --output ulysses
|
||||||
|
python shared/scripts/ghost_publish.py --draft
|
||||||
|
```
|
||||||
|
|
||||||
|
## Export Options
|
||||||
|
1. Ulysses -> `$ULYSSES_EXPORT_PATH`
|
||||||
|
2. Ghost Draft -> API push
|
||||||
|
3. Local -> `./output/blog/`
|
||||||
|
|
||||||
|
## Brand Compliance
|
||||||
|
- 분석적 톤, 기술+인간적 함의, 전문용어 영문 병기, 1,500-3,000자
|
||||||
47
.claude/commands/ourdigital-document.md
Normal file
47
.claude/commands/ourdigital-document.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
description: Notion-to-presentation workflow for OurDigital branded documents
|
||||||
|
---
|
||||||
|
|
||||||
|
# OurDigital Document
|
||||||
|
|
||||||
|
Automated Notion-to-presentation pipeline with brand styling.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "ourdigital document", "ourdigital 문서"
|
||||||
|
- "ourdigital presentation", "ourdigital 프레젠테이션"
|
||||||
|
|
||||||
|
## Pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
extract_notion.py -> synthesize_content.py -> apply_brand.py
|
||||||
|
| | |
|
||||||
|
research.json synthesis.json presentation.pptx
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/run_workflow.py --notion-url [URL] --output presentation.pptx
|
||||||
|
|
||||||
|
# Step-by-step
|
||||||
|
python scripts/extract_notion.py [URL] > research.json
|
||||||
|
python scripts/synthesize_content.py research.json > synthesis.json
|
||||||
|
python scripts/apply_brand.py synthesis.json --output presentation.pptx
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
| Script | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `run_workflow.py` | Orchestrate full pipeline |
|
||||||
|
| `extract_notion.py` | Extract content from Notion |
|
||||||
|
| `synthesize_content.py` | Analyze and structure |
|
||||||
|
| `apply_brand.py` | Apply corporate styling |
|
||||||
|
|
||||||
|
## Output Formats
|
||||||
|
- PowerPoint (.pptx)
|
||||||
|
- Figma (via API)
|
||||||
|
- HTML preview
|
||||||
|
|
||||||
|
## Brand Configuration
|
||||||
|
See `code/assets/brand_config.json` for logo, colors (OurDigital palette), fonts (Poppins/Lora), slide templates.
|
||||||
44
.claude/commands/ourdigital-journal.md
Normal file
44
.claude/commands/ourdigital-journal.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
description: English essay creation for journal.ourdigital.org with Ghost CMS integration
|
||||||
|
---
|
||||||
|
|
||||||
|
# OurDigital Journal
|
||||||
|
|
||||||
|
English essay and article creation for journal.ourdigital.org. Activated with "ourdigital" keyword.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "ourdigital journal", "ourdigital essay"
|
||||||
|
- "ourdigital English article"
|
||||||
|
|
||||||
|
## Channel Profile
|
||||||
|
|
||||||
|
- URL: journal.ourdigital.org
|
||||||
|
- Language: English
|
||||||
|
- Tone: Conversational, Poetic, Reflective
|
||||||
|
- Platform: Ghost CMS
|
||||||
|
- Length: 1,000-2,000 words
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Topic Exploration**: Angle, audience, depth level
|
||||||
|
2. **Essay Structure**: Opening (evocative) -> Exploration (3-4 observations) -> Synthesis -> Closing (open-ended)
|
||||||
|
3. **Writing Style**: First-person reflective, philosophical-technical hybrid, epistemic humility, cultural bridging
|
||||||
|
4. **Output**: Markdown with YAML frontmatter (title, meta_description, slug, tags)
|
||||||
|
|
||||||
|
## Content Types
|
||||||
|
|
||||||
|
| Type | Length |
|
||||||
|
|------|--------|
|
||||||
|
| Personal Essay | 1,000-1,500 words |
|
||||||
|
| Cultural Observation | 1,500-2,000 words |
|
||||||
|
| Industry Insight | 1,200-1,800 words |
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python shared/scripts/ghost_publish.py --file essay.md --channel journal --draft
|
||||||
|
```
|
||||||
|
|
||||||
|
## Export Options
|
||||||
|
1. Ghost Draft -> API push
|
||||||
|
2. Local -> `./output/journal/`
|
||||||
39
.claude/commands/ourdigital-skill-creator.md
Normal file
39
.claude/commands/ourdigital-skill-creator.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
description: Meta skill for creating new OurDigital Claude Skills with scaffolding
|
||||||
|
---
|
||||||
|
|
||||||
|
# OurDigital Skill Creator
|
||||||
|
|
||||||
|
Create and manage OurDigital Claude Skills with automated scaffolding and Notion sync.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "ourdigital 스킬 만들기", "ourdigital skill creator"
|
||||||
|
- "create ourdigital skill", "new ourdigital skill"
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python shared/scripts/init_skill.py {skill-name} --number XX
|
||||||
|
python shared/scripts/validate_skill.py XX-ourdigital-{skill-name}
|
||||||
|
python shared/scripts/sync_notion.py XX-ourdigital-{skill-name} --action=create
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Suitability Check**: Clear trigger, focused scope, reusable resources, no overlap (>=3 passes)
|
||||||
|
2. **Requirements Q&A**: Purpose, trigger, tools, output, environment
|
||||||
|
3. **Generate Structure**: `mkdir -p XX-ourdigital-{name}/{code,desktop,shared/{references,templates,scripts},docs/logs}`
|
||||||
|
4. **YAML Frontmatter**: Standard template with name, description, triggers, features
|
||||||
|
5. **Validation**: Directory structure, SKILL.md in both versions, content 800-1,200 words
|
||||||
|
6. **Notion Sync**: Record to Working with AI database
|
||||||
|
|
||||||
|
## Skill Categories
|
||||||
|
|
||||||
|
| Number | Category |
|
||||||
|
|--------|----------|
|
||||||
|
| 01-09 | OurDigital Core |
|
||||||
|
| 10 | Meta (this skill) |
|
||||||
|
| 11-19 | SEO |
|
||||||
|
| 20-29 | GTM/Analytics |
|
||||||
|
| 31-39 | Notion |
|
||||||
|
| 40-49 | Jamie Clinic |
|
||||||
42
.claude/commands/ourdigital-trainer.md
Normal file
42
.claude/commands/ourdigital-trainer.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
description: Training material creation and workshop planning for OurDigital
|
||||||
|
---
|
||||||
|
|
||||||
|
# OurDigital Trainer
|
||||||
|
|
||||||
|
Training material generation with template-based curriculum and evaluation forms.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "ourdigital training", "ourdigital 교육"
|
||||||
|
- "ourdigital workshop", "ourdigital 워크샵"
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate curriculum
|
||||||
|
python code/scripts/generate_curriculum.py --topic "데이터 리터러시" --duration "4hours" --audience "마케터"
|
||||||
|
|
||||||
|
# Create workshop agenda
|
||||||
|
python code/scripts/create_workshop.py --topic "AI 활용법" --format "half-day" --participants 20
|
||||||
|
|
||||||
|
# Generate evaluation form
|
||||||
|
python code/scripts/create_evaluation.py --training "SEO 기초" --type "pre-post"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workshop Formats
|
||||||
|
|
||||||
|
| Format | Duration | Modules |
|
||||||
|
|--------|----------|---------|
|
||||||
|
| mini | 2 hours | 2 |
|
||||||
|
| half-day | 4 hours | 3-4 |
|
||||||
|
| full-day | 8 hours | 5-6 |
|
||||||
|
| series | Multiple | Custom |
|
||||||
|
|
||||||
|
## Evaluation Types
|
||||||
|
- pre-post: 학습 전후 비교
|
||||||
|
- satisfaction: 만족도 조사
|
||||||
|
- knowledge: 지식 평가
|
||||||
|
- application: 현업 적용도
|
||||||
|
|
||||||
|
## Output Formats
|
||||||
|
- Markdown (default), JSON, Google Slides export
|
||||||
40
.claude/commands/seo-comprehensive-audit.md
Normal file
40
.claude/commands/seo-comprehensive-audit.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
description: Unified 6-stage SEO audit orchestrator with weighted health score
|
||||||
|
---
|
||||||
|
|
||||||
|
# SEO Comprehensive Audit
|
||||||
|
|
||||||
|
Runs 6 sub-skill audits, merges results into unified report with weighted health score (0-100), pushes to Notion.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "comprehensive SEO", "full SEO audit"
|
||||||
|
- "종합 SEO 감사"
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/seo_audit_orchestrator.py --url https://example.com
|
||||||
|
python scripts/seo_audit_orchestrator.py --url https://example.com --skip-local --skip-gsc
|
||||||
|
python scripts/seo_audit_orchestrator.py --url https://example.com --json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pipeline (6 Stages)
|
||||||
|
|
||||||
|
| Stage | Audit | Weight |
|
||||||
|
|-------|-------|--------|
|
||||||
|
| 1 | Technical SEO (robots, sitemap) | 20% |
|
||||||
|
| 2 | On-Page SEO (meta, headings, links) | 20% |
|
||||||
|
| 3 | Core Web Vitals (LCP, CLS, INP) | 25% |
|
||||||
|
| 4 | Schema Validation | 15% |
|
||||||
|
| 5 | Local SEO (GBP, Naver, NAP) | 10% |
|
||||||
|
| 6 | Search Console (rankings, CTR) | 10% |
|
||||||
|
|
||||||
|
## Stage 5: Local SEO Key Requirements
|
||||||
|
- Business Identity extraction is MANDATORY first step
|
||||||
|
- Korean market: GBP and Naver Smart Place = Critical; Kakao Map = High
|
||||||
|
- GBP/Naver Map are JS-rendered; report unfound as "not discoverable via web search"
|
||||||
|
|
||||||
|
## Notion Output
|
||||||
|
- Database ID: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||||
|
- Report in Korean; technical English terms as-is
|
||||||
|
- Audit ID format: COMP-YYYYMMDD-NNN
|
||||||
45
.claude/commands/seo-core-web-vitals.md
Normal file
45
.claude/commands/seo-core-web-vitals.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
description: Core Web Vitals analyzer using PageSpeed Insights API (LCP, CLS, INP, FID)
|
||||||
|
---
|
||||||
|
|
||||||
|
# SEO Core Web Vitals
|
||||||
|
|
||||||
|
PageSpeed Insights API client for CWV measurement and recommendations.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "Core Web Vitals", "page speed"
|
||||||
|
- "LCP", "CLS", "INP", "performance audit"
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PAGESPEED_API_KEY=your_api_key
|
||||||
|
python scripts/pagespeed_client.py --url https://example.com
|
||||||
|
python scripts/pagespeed_client.py --url https://example.com --strategy both
|
||||||
|
python scripts/pagespeed_client.py --urls urls.txt --output results.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## CWV Thresholds
|
||||||
|
|
||||||
|
| Metric | Good | Poor |
|
||||||
|
|--------|------|------|
|
||||||
|
| LCP | <=2.5s | >4s |
|
||||||
|
| FID | <=100ms | >300ms |
|
||||||
|
| CLS | <=0.1 | >0.25 |
|
||||||
|
| INP | <=200ms | >500ms |
|
||||||
|
|
||||||
|
## Common Fixes
|
||||||
|
|
||||||
|
| Issue | Fix |
|
||||||
|
|-------|-----|
|
||||||
|
| Large LCP | Optimize images, preload critical resources |
|
||||||
|
| High CLS | Set image dimensions, avoid injected content |
|
||||||
|
| Poor INP | Reduce JavaScript, optimize event handlers |
|
||||||
|
| Slow TTFB | Improve server response, use CDN |
|
||||||
|
|
||||||
|
## Rate Limits
|
||||||
|
- No API key: 25/day; With key: 25,000/day
|
||||||
|
|
||||||
|
## Notion Output
|
||||||
|
- Database: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||||
|
- Korean report, Audit ID: [TYPE]-YYYYMMDD-NNN
|
||||||
42
.claude/commands/seo-local-audit.md
Normal file
42
.claude/commands/seo-local-audit.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
description: Local SEO audit for Korean-market businesses (GBP, Naver Place, NAP, citations)
|
||||||
|
---
|
||||||
|
|
||||||
|
# SEO Local Audit
|
||||||
|
|
||||||
|
Korean-market local SEO: business identity, GBP, Naver Smart Place, Kakao Map, NAP consistency, citations, LocalBusiness schema.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "local SEO", "Google Business Profile"
|
||||||
|
- "Naver Place", "NAP consistency"
|
||||||
|
|
||||||
|
## Workflow (7 Steps)
|
||||||
|
|
||||||
|
1. **Business Identity (MANDATORY FIRST)**: Extract from JSON-LD, contact page, footer
|
||||||
|
2. **Website NAP Extraction**: Establish canonical NAP baseline
|
||||||
|
3. **GBP Verification**: Layered discovery (provided URL -> website -> WebSearch fallback)
|
||||||
|
4. **Naver Smart Place**: Layered discovery with Korean search queries
|
||||||
|
5. **Kakao Map Verification**: Check website links, then WebSearch
|
||||||
|
6. **Citation Discovery**: Industry directories (강남언니, 바비톡, 굿닥, etc.)
|
||||||
|
7. **LocalBusiness Schema Validation**: JSON-LD completeness check
|
||||||
|
|
||||||
|
## Scoring Weights
|
||||||
|
|
||||||
|
| Component | Weight |
|
||||||
|
|-----------|--------|
|
||||||
|
| NAP Consistency | 20% |
|
||||||
|
| GBP Optimization | 20% |
|
||||||
|
| Naver Smart Place | 20% |
|
||||||
|
| LocalBusiness Schema | 15% |
|
||||||
|
| Kakao Map | 10% |
|
||||||
|
| Citations | 10% |
|
||||||
|
| Business Identity | 5% |
|
||||||
|
|
||||||
|
## Key Rules
|
||||||
|
- GBP and Naver Map are JS-rendered -- use WebSearch for discovery
|
||||||
|
- "Not discoverable via web search" != "does not exist"
|
||||||
|
- Korean market: Naver Smart Place is as important as GBP
|
||||||
|
|
||||||
|
## Notion Output
|
||||||
|
- Database: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||||
|
- Category: Local SEO, Audit ID: LOCAL-YYYYMMDD-NNN
|
||||||
40
.claude/commands/seo-on-page-audit.md
Normal file
40
.claude/commands/seo-on-page-audit.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
description: On-page SEO analyzer for meta tags, headings, links, images, and Open Graph
|
||||||
|
---
|
||||||
|
|
||||||
|
# SEO On-Page Audit
|
||||||
|
|
||||||
|
Single-page optimization analysis: meta tags, heading structure, links, images, OG data.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "on-page SEO", "meta tags"
|
||||||
|
- "heading analysis", "page SEO check"
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/page_analyzer.py --url https://example.com
|
||||||
|
python scripts/page_analyzer.py --url https://example.com --json
|
||||||
|
python scripts/page_analyzer.py --urls urls.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Analysis Categories
|
||||||
|
- **Meta Tags**: Title (length, keywords), description, canonical, robots
|
||||||
|
- **Heading Structure**: H1 presence/count, hierarchy validation, keyword placement
|
||||||
|
- **Links**: Internal/external count, broken links, nofollow distribution
|
||||||
|
- **Images**: Alt attributes, file sizes, lazy loading
|
||||||
|
- **Open Graph**: OG title/description/image, Twitter Cards
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
| Issue | Severity |
|
||||||
|
|-------|----------|
|
||||||
|
| Missing H1 | High |
|
||||||
|
| Title too long (>60) | Medium |
|
||||||
|
| No meta description | High |
|
||||||
|
| Images without alt | Medium |
|
||||||
|
| Multiple H1 tags | Medium |
|
||||||
|
|
||||||
|
## Notion Output
|
||||||
|
- Database: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||||
|
- Korean report, Audit ID: [TYPE]-YYYYMMDD-NNN
|
||||||
42
.claude/commands/seo-search-console.md
Normal file
42
.claude/commands/seo-search-console.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
description: Google Search Console data retrieval (rankings, CTR, impressions, sitemaps)
|
||||||
|
---
|
||||||
|
|
||||||
|
# SEO Search Console
|
||||||
|
|
||||||
|
GSC API client for search analytics, sitemap status, and index coverage.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "Search Console", "rankings"
|
||||||
|
- "GSC data", "search performance"
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/gsc_client.py --site sc-domain:example.com --action summary
|
||||||
|
python scripts/gsc_client.py --site sc-domain:example.com --action queries --limit 100
|
||||||
|
python scripts/gsc_client.py --site sc-domain:example.com --action pages
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- Credentials: `~/.credential/ourdigital-seo-agent.json`
|
||||||
|
- Service account: `ourdigital-seo-agent@ourdigital-insights.iam.gserviceaccount.com`
|
||||||
|
|
||||||
|
## Actions
|
||||||
|
|
||||||
|
| Action | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `summary` | Clicks, impressions, CTR, position overview |
|
||||||
|
| `queries` | Top search queries |
|
||||||
|
| `pages` | Top pages by clicks |
|
||||||
|
| `sitemaps` | Sitemap submission status |
|
||||||
|
| `coverage` | Index coverage issues |
|
||||||
|
|
||||||
|
## Site Property Formats
|
||||||
|
- Domain: `sc-domain:example.com`
|
||||||
|
- URL prefix: `https://www.example.com/`
|
||||||
|
|
||||||
|
## Notion Output
|
||||||
|
- Database: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||||
|
- Korean report, Audit ID: [TYPE]-YYYYMMDD-NNN
|
||||||
44
.claude/commands/seo-technical-audit.md
Normal file
44
.claude/commands/seo-technical-audit.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
description: Technical SEO audit for robots.txt, XML sitemap, and URL crawlability
|
||||||
|
---
|
||||||
|
|
||||||
|
# SEO Technical Audit
|
||||||
|
|
||||||
|
Crawlability fundamentals: robots.txt validation, XML sitemap analysis, URL accessibility checking.
|
||||||
|
|
||||||
|
## Triggers
|
||||||
|
- "technical SEO", "crawlability"
|
||||||
|
- "robots.txt", "sitemap validation"
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/robots_checker.py --url https://example.com
|
||||||
|
python scripts/sitemap_validator.py --url https://example.com/sitemap.xml
|
||||||
|
python scripts/sitemap_crawler.py --sitemap https://example.com/sitemap.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
| Script | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `robots_checker.py` | Parse robots.txt, test URL rules |
|
||||||
|
| `sitemap_validator.py` | Validate XML structure, URL count, lastmod |
|
||||||
|
| `sitemap_crawler.py` | Async check URL accessibility, HTTP status |
|
||||||
|
|
||||||
|
## Common Issues Detected
|
||||||
|
|
||||||
|
| Issue | Severity |
|
||||||
|
|-------|----------|
|
||||||
|
| Missing sitemap declaration | Medium |
|
||||||
|
| Blocking CSS/JS resources | High |
|
||||||
|
| URLs returning 404 | High |
|
||||||
|
| Exceeds 50,000 URL limit | High |
|
||||||
|
| Non-canonical URLs included | Medium |
|
||||||
|
|
||||||
|
## Config
|
||||||
|
- `CRAWL_DELAY=1.0`, `MAX_CONCURRENT=20`, `REQUEST_TIMEOUT=30`
|
||||||
|
|
||||||
|
## Notion Output
|
||||||
|
- Database: `2c8581e5-8a1e-8035-880b-e38cefc2f3ef`
|
||||||
|
- Korean report, Audit ID: [TYPE]-YYYYMMDD-NNN
|
||||||
28
AGENTS.md
28
AGENTS.md
@@ -129,9 +129,19 @@ Task 2: general-purpose - "Implement the planned skill" # Needs Task 1 result
|
|||||||
|
|
||||||
### GTM Skills (60-69)
|
### GTM Skills (60-69)
|
||||||
|
|
||||||
- Use **gtm-manager** agent for GTM-specific debugging
|
Three specialized skills with clear handoff workflow: **audit → edit → validate**
|
||||||
- Requires Chrome GTM Debug profile for live testing
|
|
||||||
- Scripts interact with GTM API and dataLayer
|
| Skill | Purpose | MCP Tools |
|
||||||
|
|-------|---------|-----------|
|
||||||
|
| 60-gtm-audit | Scan pages, gap analysis, tag design blueprints | DTM Agent + Chrome DevTools |
|
||||||
|
| 61-gtm-editor | Create/modify tags, triggers, variables via GTM API | DTM Agent + Chrome DevTools |
|
||||||
|
| 62-gtm-validator | QA tag firing, trigger selectors, naming conventions | DTM Agent + Chrome DevTools |
|
||||||
|
|
||||||
|
- **gtm-audit** Mode D outputs blueprints → hand off to **gtm-editor** for implementation
|
||||||
|
- **gtm-editor** creates tags → hand off to **gtm-validator** for QA verification
|
||||||
|
- **gtm-validator** finds broken selectors → hand back to **gtm-editor** for fixes
|
||||||
|
- All three use DTM Agent MCP for container operations and Chrome DevTools MCP for browser inspection
|
||||||
|
- Audit reports write to Notion GTM Audit Log database
|
||||||
|
|
||||||
### Jamie Clinic Skills (40-49)
|
### Jamie Clinic Skills (40-49)
|
||||||
|
|
||||||
@@ -172,6 +182,16 @@ Task(
|
|||||||
|
|
||||||
**Parallel workflows:** Set `NOTEBOOKLM_HOME` per agent to avoid context conflicts.
|
**Parallel workflows:** Set `NOTEBOOKLM_HOME` per agent to avoid context conflicts.
|
||||||
|
|
||||||
|
### Mac Optimizer (92)
|
||||||
|
|
||||||
|
- **Claude Code only** — no Desktop version
|
||||||
|
- Runs read-only audit scripts that output JSON lines, then presents findings as severity-ranked tables
|
||||||
|
- 5 audit modules: packages, environment, security, cleanup, resources
|
||||||
|
- `doctor` mode runs all 5 sequentially and presents unified report
|
||||||
|
- **Always ask user consent** before executing any cleanup or system changes
|
||||||
|
- Scripts are in `custom-skills/92-mac-optimizer/scripts/`
|
||||||
|
- Reference docs in `custom-skills/92-mac-optimizer/references/`
|
||||||
|
|
||||||
### Reference Curator Skills (90-99)
|
### Reference Curator Skills (90-99)
|
||||||
|
|
||||||
- Use **reference-curator-pipeline** for full automated curation workflows
|
- Use **reference-curator-pipeline** for full automated curation workflows
|
||||||
@@ -204,7 +224,7 @@ For long-running tasks, use `run_in_background: true`:
|
|||||||
|
|
||||||
```
|
```
|
||||||
# Good candidates for background execution:
|
# Good candidates for background execution:
|
||||||
- Full skill audit across all 52 skills
|
- Full skill audit across all 62 skills
|
||||||
- Running Python tests on multiple skills
|
- Running Python tests on multiple skills
|
||||||
- Generating comprehensive documentation
|
- Generating comprehensive documentation
|
||||||
|
|
||||||
|
|||||||
27
CLAUDE.md
27
CLAUDE.md
@@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
**GitHub**: https://github.com/ourdigital/our-claude-skills
|
**GitHub**: https://github.com/ourdigital/our-claude-skills
|
||||||
|
|
||||||
This is a Claude Skills collection repository containing:
|
This is a Claude Skills collection repository containing:
|
||||||
- **custom-skills/**: 61 custom skills for OurDigital workflows, SEO, GTM, Jamie Brand, NotebookLM, Notion, D.intelligence Agent Corps, Reference Curation, and Multi-Agent Collaboration
|
- **custom-skills/**: 62 custom skills for OurDigital workflows, SEO, GTM, Jamie Brand, NotebookLM, Notion, D.intelligence Agent Corps, Reference Curation, Mac Optimizer, TUI Designer, and Multi-Agent Collaboration
|
||||||
- **example-skills/**: Reference examples from Anthropic's official skills repository
|
- **example-skills/**: Reference examples from Anthropic's official skills repository
|
||||||
- **official-skills/**: Notion integration skills (3rd party)
|
- **official-skills/**: Notion integration skills (3rd party)
|
||||||
- **reference/**: Skill format requirements documentation
|
- **reference/**: Skill format requirements documentation
|
||||||
@@ -68,9 +68,9 @@ This is a Claude Skills collection repository containing:
|
|||||||
|
|
||||||
| # | Skill | Purpose | Trigger |
|
| # | Skill | Purpose | Trigger |
|
||||||
|---|-------|---------|---------|
|
|---|-------|---------|---------|
|
||||||
| 60 | gtm-audit | GTM container audit | "audit GTM", "GTM analysis" |
|
| 60 | gtm-audit | GTM audit — page scan, site audit, gap analysis, tag design | "GTM audit", "scan page", "tracking coverage", "gap analysis" |
|
||||||
| 61 | gtm-manager | GTM management + dataLayer | "GTM manager", "dataLayer" |
|
| 61 | gtm-editor | GTM tag/trigger/variable creation via API, ES5 Custom HTML, dataLayer | "create GTM tag", "generate dataLayer", "modify trigger", "manage GTM" |
|
||||||
| 62 | gtm-guardian | Live tag monitoring & validation | "monitor GTM", "tag validation" |
|
| 62 | gtm-validator | GTM QA — tag firing, trigger testing, naming conventions, cross-platform | "validate tags", "QA GTM", "debug GTM", "naming conventions" |
|
||||||
|
|
||||||
### Notion Tools (31-39)
|
### Notion Tools (31-39)
|
||||||
|
|
||||||
@@ -85,7 +85,6 @@ This is a Claude Skills collection repository containing:
|
|||||||
|---|-------|---------|---------|
|
|---|-------|---------|---------|
|
||||||
| 40 | jamie-brand-editor | Branded content generation | "write Jamie blog", "Jamie content" |
|
| 40 | jamie-brand-editor | Branded content generation | "write Jamie blog", "Jamie content" |
|
||||||
| 41 | jamie-brand-audit | Content review/compliance | "review content", "brand audit" |
|
| 41 | jamie-brand-audit | Content review/compliance | "review content", "brand audit" |
|
||||||
| 42 | jamie-journal-editor | Clinic journal editing | "Jamie journal", "진료 일지" |
|
|
||||||
| 43 | jamie-youtube-manager | YouTube SEO audit & management | "YouTube SEO", "YT optimization" |
|
| 43 | jamie-youtube-manager | YouTube SEO audit & management | "YouTube SEO", "YT optimization" |
|
||||||
| 44 | jamie-youtube-subtitle-checker | YouTube subtitle validation | "check subtitles", "subtitle QA" |
|
| 44 | jamie-youtube-subtitle-checker | YouTube subtitle validation | "check subtitles", "subtitle QA" |
|
||||||
| 45 | jamie-instagram-manager | Instagram account management | "Instagram management", "IG strategy" |
|
| 45 | jamie-instagram-manager | Instagram account management | "Instagram management", "IG strategy" |
|
||||||
@@ -121,7 +120,14 @@ This is a Claude Skills collection repository containing:
|
|||||||
|
|
||||||
**User Guide:** `custom-skills/dintel-shared/USER-GUIDE.md`
|
**User Guide:** `custom-skills/dintel-shared/USER-GUIDE.md`
|
||||||
|
|
||||||
### Reference Curator & Multi-Agent (90-99)
|
### System & Design Tools (92-93)
|
||||||
|
|
||||||
|
| # | Skill | Purpose | Trigger |
|
||||||
|
|---|-------|---------|---------|
|
||||||
|
| 92 | mac-optimizer | macOS system health audit & optimization (Claude Code only) | "audit my mac", "system health", "clean up caches", "check security", "update packages" |
|
||||||
|
| 93 | tui-designer | TUI wizard interface design (Norton Commander / Gopher style, Rich) | "build TUI", "CLI wizard", "terminal UI", "Rich TUI" |
|
||||||
|
|
||||||
|
### Reference Curator & Multi-Agent (90-91)
|
||||||
|
|
||||||
| # | Skill | Purpose | Trigger |
|
| # | Skill | Purpose | Trigger |
|
||||||
|---|-------|---------|---------|
|
|---|-------|---------|---------|
|
||||||
@@ -247,15 +253,14 @@ our-claude-skills/
|
|||||||
│ ├── 34-seo-reporting-dashboard/
|
│ ├── 34-seo-reporting-dashboard/
|
||||||
│ │
|
│ │
|
||||||
│ ├── 60-gtm-audit/
|
│ ├── 60-gtm-audit/
|
||||||
│ ├── 61-gtm-manager/
|
│ ├── 61-gtm-editor/
|
||||||
│ ├── 62-gtm-guardian/
|
│ ├── 62-gtm-validator/
|
||||||
│ │
|
│ │
|
||||||
│ ├── 31-notion-organizer/
|
│ ├── 31-notion-organizer/
|
||||||
│ ├── 32-notion-writer/
|
│ ├── 32-notion-writer/
|
||||||
│ │
|
│ │
|
||||||
│ ├── 40-jamie-brand-editor/
|
│ ├── 40-jamie-brand-editor/
|
||||||
│ ├── 41-jamie-brand-audit/
|
│ ├── 41-jamie-brand-audit/
|
||||||
│ ├── 42-jamie-journal-editor/
|
|
||||||
│ ├── 43-jamie-youtube-manager/
|
│ ├── 43-jamie-youtube-manager/
|
||||||
│ ├── 44-jamie-youtube-subtitle-checker/
|
│ ├── 44-jamie-youtube-subtitle-checker/
|
||||||
│ ├── 45-jamie-instagram-manager/
|
│ ├── 45-jamie-instagram-manager/
|
||||||
@@ -290,6 +295,10 @@ our-claude-skills/
|
|||||||
│ │
|
│ │
|
||||||
│ ├── 91-multi-agent-guide/
|
│ ├── 91-multi-agent-guide/
|
||||||
│ │
|
│ │
|
||||||
|
│ ├── 92-mac-optimizer/ # macOS system health toolkit (Claude Code only)
|
||||||
|
│ │
|
||||||
|
│ ├── 93-tui-designer/ # TUI wizard design (Rich, Norton Commander style)
|
||||||
|
│ │
|
||||||
│ └── 99_archive/
|
│ └── 99_archive/
|
||||||
│
|
│
|
||||||
├── example-skills/skills-main/
|
├── example-skills/skills-main/
|
||||||
|
|||||||
20
README.md
20
README.md
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> **Internal R&D Repository** - This repository is restricted for internal use only.
|
> **Internal R&D Repository** - This repository is restricted for internal use only.
|
||||||
|
|
||||||
A collection of **61 custom Claude Skills** for OurDigital workflows, D.intelligence Agent Corps (9-agent business operations suite), Jamie Plastic Surgery Clinic brand management, SEO/GTM tools, NotebookLM automation, Notion integrations, reference documentation curation, and multi-agent collaboration.
|
A collection of **62 custom Claude Skills** for OurDigital workflows, D.intelligence Agent Corps (9-agent business operations suite), Jamie Plastic Surgery Clinic brand management, SEO/GTM tools, NotebookLM automation, Notion integrations, reference documentation curation, macOS system optimization, TUI wizard design, and multi-agent collaboration.
|
||||||
|
|
||||||
## Quick Install
|
## Quick Install
|
||||||
|
|
||||||
@@ -88,9 +88,9 @@ cd our-claude-skills/custom-skills/_ourdigital-shared
|
|||||||
|
|
||||||
| # | Skill | Purpose |
|
| # | Skill | Purpose |
|
||||||
|---|-------|---------|
|
|---|-------|---------|
|
||||||
| 60 | `gtm-audit` | GTM container audit with Chrome DevTools |
|
| 60 | `gtm-audit` | GTM container audit — page scan, site audit, gap analysis, tag design (DTM Agent + Chrome DevTools MCP) |
|
||||||
| 61 | `gtm-manager` | GTM management + dataLayer generation |
|
| 61 | `gtm-editor` | GTM tag/trigger/variable creation via API, ES5 Custom HTML, dataLayer generation, workspace lifecycle |
|
||||||
| 62 | `gtm-guardian` | Live tag monitoring & validation |
|
| 62 | `gtm-validator` | GTM QA — tag firing verification, trigger testing, dataLayer schema, naming conventions, cross-platform mapping |
|
||||||
|
|
||||||
### Notion Tools (31-39)
|
### Notion Tools (31-39)
|
||||||
|
|
||||||
@@ -105,7 +105,6 @@ cd our-claude-skills/custom-skills/_ourdigital-shared
|
|||||||
|---|-------|---------|
|
|---|-------|---------|
|
||||||
| 40 | `jamie-brand-editor` | Branded content generation |
|
| 40 | `jamie-brand-editor` | Branded content generation |
|
||||||
| 41 | `jamie-brand-audit` | Content review/compliance |
|
| 41 | `jamie-brand-audit` | Content review/compliance |
|
||||||
| 42 | `jamie-journal-editor` | Clinic journal editing |
|
|
||||||
| 43 | `jamie-youtube-manager` | YouTube SEO audit & management |
|
| 43 | `jamie-youtube-manager` | YouTube SEO audit & management |
|
||||||
| 44 | `jamie-youtube-subtitle-checker` | YouTube subtitle validation |
|
| 44 | `jamie-youtube-subtitle-checker` | YouTube subtitle validation |
|
||||||
| 45 | `jamie-instagram-manager` | Instagram account management |
|
| 45 | `jamie-instagram-manager` | Instagram account management |
|
||||||
@@ -121,7 +120,14 @@ cd our-claude-skills/custom-skills/_ourdigital-shared
|
|||||||
|
|
||||||
**Prerequisites:** `pip install notebooklm-py && playwright install chromium && notebooklm login`
|
**Prerequisites:** `pip install notebooklm-py && playwright install chromium && notebooklm login`
|
||||||
|
|
||||||
### Reference Curator & Multi-Agent (90-99)
|
### System & Design Tools (92-93)
|
||||||
|
|
||||||
|
| # | Skill | Purpose |
|
||||||
|
|---|-------|---------|
|
||||||
|
| 92 | `mac-optimizer` | macOS system health audit & optimization (Claude Code only) |
|
||||||
|
| 93 | `tui-designer` | TUI wizard interface design (Norton Commander / Gopher style, Rich) |
|
||||||
|
|
||||||
|
### Reference Curator & Multi-Agent (90-91)
|
||||||
|
|
||||||
| # | Skill | Purpose |
|
| # | Skill | Purpose |
|
||||||
|---|-------|---------|
|
|---|-------|---------|
|
||||||
@@ -177,6 +183,8 @@ our-claude-skills/
|
|||||||
│ ├── 50-53 (NotebookLM tools)
|
│ ├── 50-53 (NotebookLM tools)
|
||||||
│ ├── 90-reference-curator/
|
│ ├── 90-reference-curator/
|
||||||
│ ├── 91-multi-agent-guide/
|
│ ├── 91-multi-agent-guide/
|
||||||
|
│ ├── 92-mac-optimizer/
|
||||||
|
│ ├── 93-tui-designer/
|
||||||
│ └── 99_archive/
|
│ └── 99_archive/
|
||||||
│
|
│
|
||||||
├── example-skills/ # Anthropic reference examples
|
├── example-skills/ # Anthropic reference examples
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# JHR — josunhotel.com Gotchas
|
||||||
|
|
||||||
|
Client: **JHR** (Josun Hotels & Resorts / 조선호텔앤리조트)
|
||||||
|
Domain: josunhotel.com + subdomains (gjb, gjj, jpg, grp)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Intentionally Truncated Directory Names in robots.txt
|
||||||
|
|
||||||
|
**Date discovered**: 2026-03-20
|
||||||
|
**Audit ID**: COMP-20260320-001
|
||||||
|
|
||||||
|
The following `Disallow` paths in robots.txt look like typos but are **intentional directory names** on the server:
|
||||||
|
|
||||||
|
| robots.txt path | Looks like it should be | Actual status |
|
||||||
|
|-----------------|------------------------|---------------|
|
||||||
|
| `/cnfirm` | `/confirm` | **Intentional** — real directory name |
|
||||||
|
| `/mber` | `/member` | **Intentional** — real directory name |
|
||||||
|
|
||||||
|
**Do NOT recommend renaming these** in audit reports. The server-side application uses these truncated names by design.
|
||||||
|
|
||||||
|
### Context
|
||||||
|
|
||||||
|
The josunhotel.com backend (Java/.do endpoints) uses shortened directory names for certain internal paths. This is likely a legacy naming convention from the original application architecture.
|
||||||
|
|
||||||
|
### Audit Impact
|
||||||
|
|
||||||
|
When running Technical SEO audits (Stage 1), skip the "typo in Disallow paths" finding for `/cnfirm` and `/mber`. These are valid paths that correctly block crawler access to confirmation and member-related pages.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# JHR — lescapehotel.com Gotchas
|
||||||
|
|
||||||
|
Client: **JHR** (Josun Hotels & Resorts)
|
||||||
|
Domain: lescapehotel.com → les.josunhotel.com (migrated)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Domain Migration Complete — Monitor Redirects & Link Juice
|
||||||
|
|
||||||
|
**Date discovered**: 2025-12-14
|
||||||
|
**Updated**: 2026-03-20
|
||||||
|
**Status**: Migration COMPLETE
|
||||||
|
|
||||||
|
The L'Escape Hotel domain migration from `lescapehotel.com` to `les.josunhotel.com` has been completed.
|
||||||
|
|
||||||
|
### What to Monitor (Watchtower)
|
||||||
|
|
||||||
|
- **301 redirects**: Verify all old lescapehotel.com URLs still 301 to correct les.josunhotel.com counterparts
|
||||||
|
- **Link juice preservation**: Check that backlinks from external sites pointing to lescapehotel.com are passing authority through redirects
|
||||||
|
- **Redirect chains**: Watch for double redirects (lescapehotel.com → old intermediate → les.josunhotel.com)
|
||||||
|
- **GSC coverage**: Monitor les.josunhotel.com indexing in Google Search Console
|
||||||
|
- **Referring domains**: Track referring domain count in Ahrefs/GSC to ensure backlink equity is maintained
|
||||||
|
|
||||||
|
### Audit Checklist (Run Periodically)
|
||||||
|
|
||||||
|
1. Sample 10-20 old lescapehotel.com URLs — verify 301 status
|
||||||
|
2. Check for any 404s on les.josunhotel.com that should have redirects
|
||||||
|
3. Compare organic traffic trends pre/post migration
|
||||||
|
4. Verify old sitemap URLs are all redirecting properly
|
||||||
|
5. Check Naver Search Advisor for indexing status
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# SLA — shillahotels.com Gotchas
|
||||||
|
|
||||||
|
Client: **SLA** (Shilla Hotels & Resorts / 신라호텔)
|
||||||
|
Domain: shillahotels.com
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. www1 / www2 Are Dev/Stage Hosts, NOT Production
|
||||||
|
|
||||||
|
**Date discovered**: 2026-03-20
|
||||||
|
**Context**: SLA new integrated website under construction
|
||||||
|
|
||||||
|
The Shilla Hotels domain uses numbered subdomains for development environments:
|
||||||
|
|
||||||
|
| Subdomain | Purpose | Status |
|
||||||
|
|-----------|---------|--------|
|
||||||
|
| `www1.shillahotels.com` | Development | Active — deployed intermittently |
|
||||||
|
| `www2.shillahotels.com` | Staging | Active — deployed intermittently |
|
||||||
|
| `www.shillahotels.com` | Production (current) | Live |
|
||||||
|
| TBD (new integrated site) | Production (future) | **Target launch: end of June 2026** |
|
||||||
|
|
||||||
|
**Do NOT run technical SEO audits against www1 or www2** unless specifically instructed. These are pre-production environments and will have:
|
||||||
|
- No robots.txt
|
||||||
|
- No XML sitemap
|
||||||
|
- Incomplete meta tags
|
||||||
|
- Missing canonical/hreflang tags
|
||||||
|
|
||||||
|
These are expected states for dev/stage, not audit findings.
|
||||||
|
|
||||||
|
### When to Scan
|
||||||
|
|
||||||
|
The client will notify when www1/www2 need technical SEO element checks. Do NOT proactively audit these subdomains.
|
||||||
|
|
||||||
|
## 2. No robots.txt or Sitemap on Dev/Stage Hosts
|
||||||
|
|
||||||
|
**Date discovered**: 2026-03-20
|
||||||
|
|
||||||
|
robots.txt and sitemap.xml are intentionally not deployed to www1 and www2. This is expected for pre-production environments. Do NOT flag as issues.
|
||||||
|
|
||||||
|
Wait for client instruction before scanning technical SEO elements on these hosts.
|
||||||
141
custom-skills/60-gtm-audit/code/SKILL.md
Normal file
141
custom-skills/60-gtm-audit/code/SKILL.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
---
|
||||||
|
name: gtm-audit
|
||||||
|
description: >
|
||||||
|
Automated GTM audit and analysis. Scans websites for tracking coverage, compares
|
||||||
|
against GTM container config, identifies gaps, and reports to Notion. Uses Chrome
|
||||||
|
DevTools MCP for browser inspection and DTM Agent MCP for container operations.
|
||||||
|
Triggers on "GTM audit", "audit website", "scan page", "check tracking",
|
||||||
|
"tracking coverage", "gap analysis", "site audit", "tag firing check".
|
||||||
|
NOT for creating/modifying tags (use gtm-editor) or validating/QA (use gtm-validator).
|
||||||
|
---
|
||||||
|
|
||||||
|
# GTM Audit Skill
|
||||||
|
|
||||||
|
Analyze GTM implementations by scanning websites and comparing against container configuration.
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
### Browser Inspection (Chrome DevTools MCP)
|
||||||
|
- `navigate_page` — Load target URL
|
||||||
|
- `evaluate_script` — Capture dataLayer, detect GTM containers, check JS globals
|
||||||
|
- `list_network_requests` — Capture tag firing network requests
|
||||||
|
- `take_screenshot` — Document page state
|
||||||
|
- `click` / `hover` — Simulate user interactions
|
||||||
|
- `list_console_messages` — Check for JS errors
|
||||||
|
|
||||||
|
### GTM Container (DTM Agent MCP)
|
||||||
|
- `dtm_status` — Check auth and active container
|
||||||
|
- `dtm_list_tags` / `dtm_list_triggers` / `dtm_list_variables` — Get container config
|
||||||
|
- `dtm_audit_scan` — Scan page with collected data (returns structured PageData)
|
||||||
|
- `dtm_audit_gap` — Run gap analysis (returns GapReport with OK/MISSING/ORPHAN)
|
||||||
|
|
||||||
|
### Reporting (Notion MCP)
|
||||||
|
- Write findings to GTM Audit Log: `collection://2cf581e5-8a1e-815a-a7f8-000b88b93585`
|
||||||
|
|
||||||
|
## 4 Audit Modes
|
||||||
|
|
||||||
|
### Mode A: Page Scan (`/gtm-audit scan <url>`)
|
||||||
|
|
||||||
|
Quick single-page diagnostics. No GTM container needed.
|
||||||
|
|
||||||
|
1. `navigate_page` to URL, wait for load
|
||||||
|
2. `evaluate_script`: capture dataLayer
|
||||||
|
```javascript
|
||||||
|
JSON.stringify(window.dataLayer || [])
|
||||||
|
```
|
||||||
|
3. `evaluate_script`: detect GTM containers
|
||||||
|
```javascript
|
||||||
|
(function(){var s=document.querySelectorAll('script[src*="googletagmanager.com/gtm.js"]');var ids=[];s.forEach(function(el){var m=el.src.match(/id=(GTM-[A-Z0-9]+)/);if(m)ids.push(m[1]);});return JSON.stringify(ids);})()
|
||||||
|
```
|
||||||
|
4. `evaluate_script`: detect tracking pixels
|
||||||
|
```javascript
|
||||||
|
(function(){var f=[];if(window.fbq)f.push('Meta Pixel');if(window.gtag)f.push('GA4');if(window.clarity)f.push('Clarity');if(window.kakaoPixel)f.push('Kakao');if(window.wcs)f.push('Naver');if(window.ttq)f.push('TikTok');return JSON.stringify(f);})()
|
||||||
|
```
|
||||||
|
5. `list_network_requests` — filter for tag domains
|
||||||
|
6. Call `dtm_audit_scan` with collected html, network_requests, datalayer_json
|
||||||
|
7. Report findings
|
||||||
|
|
||||||
|
### Mode B: Site Audit (`/gtm-audit site <url>`)
|
||||||
|
|
||||||
|
Full website coverage audit with auto-discovery.
|
||||||
|
|
||||||
|
1. Navigate to root URL
|
||||||
|
2. `evaluate_script`: extract all internal links
|
||||||
|
3. Classify pages by type (home, product, service, form, blog, about)
|
||||||
|
4. For each page type: run Mode A scan
|
||||||
|
5. Build coverage matrix (page type × event × platform)
|
||||||
|
6. Run Mode C gap analysis against container
|
||||||
|
7. Write comprehensive report to Notion
|
||||||
|
|
||||||
|
### Mode C: Gap Analysis (`/gtm-audit gap <url>`) — MOST IMPORTANT
|
||||||
|
|
||||||
|
Compare GTM container config against actual tag firing.
|
||||||
|
|
||||||
|
1. `dtm_status` — verify auth and active container
|
||||||
|
2. `dtm_list_tags` + `dtm_list_triggers` — get expected configuration
|
||||||
|
3. `navigate_page` to URL
|
||||||
|
4. `evaluate_script` — capture dataLayer
|
||||||
|
5. `list_network_requests` — capture actual tag firings
|
||||||
|
6. Call `dtm_audit_gap` with network_requests
|
||||||
|
7. Analyze gaps: MISSING (in GTM, not firing), ORPHAN (firing, not in GTM)
|
||||||
|
8. Write to Notion GTM Audit Log
|
||||||
|
|
||||||
|
### Mode D: Tag Design (`/gtm-audit design <url>`)
|
||||||
|
|
||||||
|
AI-powered analysis of what SHOULD be tracked on a page.
|
||||||
|
|
||||||
|
1. `navigate_page` to URL
|
||||||
|
2. `take_screenshot` — visual reference
|
||||||
|
3. `evaluate_script` — inspect DOM: forms, buttons, links, product elements
|
||||||
|
4. `evaluate_script` — capture existing dataLayer schema
|
||||||
|
5. `dtm_list_tags` — check existing configuration
|
||||||
|
6. Design recommendations: which events, triggers, variables are needed
|
||||||
|
7. Output as structured blueprint (hand off to gtm-editor for implementation)
|
||||||
|
|
||||||
|
## Detection Patterns
|
||||||
|
|
||||||
|
### Cloudflare Zaraz Proxy
|
||||||
|
URLs with `/1tbv/` path proxy GA4/Ads through the site's domain:
|
||||||
|
```
|
||||||
|
https://site.com/1tbv/ag/g/c?tid=G-XXXXX → GA4
|
||||||
|
https://site.com/1tbv/ag/g/c?tid=AW-XXXXX → Google Ads
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server-Side GTM (sGTM)
|
||||||
|
Custom subdomains: `stms.*`, `sgtm.*`, `ss.*`, `tag.*`
|
||||||
|
```
|
||||||
|
https://stms.site.com/g/collect?tid=G-XXXXX → GA4 via sGTM
|
||||||
|
```
|
||||||
|
|
||||||
|
### GA4 Multiple Endpoints
|
||||||
|
One GA4 page_view fires to multiple endpoints — this is normal:
|
||||||
|
- `google-analytics.com/g/collect`
|
||||||
|
- `analytics.google.com/g/s/collect`
|
||||||
|
- `stats.g.doubleclick.net/g/collect`
|
||||||
|
- `www.google.com/ccm/collect` (Consent Mode)
|
||||||
|
- Zaraz proxy endpoints
|
||||||
|
|
||||||
|
## Notion Report Fields
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| Site | domain name |
|
||||||
|
| URL | full URL |
|
||||||
|
| Container IDs | GTM-XXXXX |
|
||||||
|
| Audit Status | Pass / Warning / Fail |
|
||||||
|
| GTM Status | Installed / Not Found / Multiple Containers |
|
||||||
|
| Tags Fired | [GA4, Google Ads, Meta Pixel, Kakao, Naver, ...] |
|
||||||
|
| Journey Type | full / pageview / scroll / form / checkout / datalayer |
|
||||||
|
| Critical Issues | count |
|
||||||
|
| Issues Count | count |
|
||||||
|
| Summary | AI-generated findings |
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Always check `dtm_status` before gap analysis
|
||||||
|
- Wait for page to fully load before capturing network requests
|
||||||
|
- Use `evaluate_script` to check consent mode before interpreting missing tags
|
||||||
|
- Orphan count may be high due to GA4 multi-endpoint — note this in reports
|
||||||
|
- **In Mode D (tag design), check elements for `id` attributes** — if missing, recommend adding IDs before designing CSS-based triggers. Suggest naming: `[section]-[element]-[action]`
|
||||||
|
- Hand off tag creation work to `gtm-editor` skill
|
||||||
|
- Hand off QA/validation work to `gtm-validator` skill
|
||||||
@@ -196,6 +196,121 @@ dataLayer.push({
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Content Grouping Issues (RegEx Table Variables)
|
||||||
|
|
||||||
|
### How Content Grouping Works in GTM
|
||||||
|
|
||||||
|
Content grouping uses **RegEx Table** variables to categorize pages by URL/path patterns.
|
||||||
|
The variable takes an input (typically `{{Page Path}}` or `{{Page URL}}`), tests it against
|
||||||
|
an ordered list of regex patterns, and returns the first matching output value.
|
||||||
|
If no pattern matches, it falls to the **Default Value** (usually "Other").
|
||||||
|
|
||||||
|
**Key settings:**
|
||||||
|
- **Full Matches Only**: OFF — partial regex match is sufficient
|
||||||
|
- **Capture Groups and Replace**: OFF — output is the literal value, not a regex replacement
|
||||||
|
- **Ignore Case**: ON — case-insensitive matching
|
||||||
|
|
||||||
|
### New Page Paths Fall Through to "Other"
|
||||||
|
|
||||||
|
**Symptoms**: New sections (campaigns, special events, landing pages) show as "Other" in GA4 content group reports.
|
||||||
|
|
||||||
|
**Root cause**: The RegEx Table was built for the site's original URL structure. New paths like `/specialEvent/`, `/campaign/`, `/timesale/` don't match any existing pattern.
|
||||||
|
|
||||||
|
**Example**: JHR site has `/specialEvent/landersshoppingfesta2026/teaser.do` but only `/package/` and `/event/` are in the regex table, not `/specialEvent/`.
|
||||||
|
|
||||||
|
**Fix**: Add regex rule for the new path pattern. Place it BEFORE the default.
|
||||||
|
|
||||||
|
**Gotcha — two variables to update**: Sites often have parallel content group variables:
|
||||||
|
1. **Page Path based** (e.g., `(\/m)?\/specialEvent\/(.*)`) — for internal reports
|
||||||
|
2. **Page URL based** (e.g., full URL with domain regex) — for cross-domain or subdomain tracking
|
||||||
|
|
||||||
|
Both must be updated or they'll produce inconsistent groupings.
|
||||||
|
|
||||||
|
### RegEx Pattern Conventions (JHR Example)
|
||||||
|
|
||||||
|
Page Path patterns follow this structure:
|
||||||
|
```
|
||||||
|
(\/m)?\/section\/(.*) → Korean Category Label
|
||||||
|
```
|
||||||
|
- `(\/m)?` — optional mobile path prefix
|
||||||
|
- `\/section\/` — the URL path segment to match
|
||||||
|
- `(.*)` — match anything after
|
||||||
|
|
||||||
|
Page URL patterns include domain matching:
|
||||||
|
```
|
||||||
|
^https\:\/\/(www|app|jpg|gjb|gjj|grp|\.)?josunhotel\.com(\/m)?\/section\/(.*)
|
||||||
|
```
|
||||||
|
- Multi-subdomain support via alternation group
|
||||||
|
- Same `(\/m)?` mobile prefix handling
|
||||||
|
|
||||||
|
### Content Group Categories (Hospitality Reference)
|
||||||
|
|
||||||
|
| Pattern | Category (KR) | Category (EN) |
|
||||||
|
|---------|---------------|---------------|
|
||||||
|
| `/intro.do`, `/main.do` | 메인페이지 | Main Page |
|
||||||
|
| `/about/`, `/press/` | 기업 소개 | Corporate Info |
|
||||||
|
| `/hotel/` | 호텔 소개 | Hotel Info |
|
||||||
|
| `/rooms/` | 객실 소개 | Room Info |
|
||||||
|
| `/dining/` | 다이닝 이용 | Dining |
|
||||||
|
| `/facilities/`, `/meeting/`, `/fitness/` | 부대시설 소개 | Facilities |
|
||||||
|
| `/package/`, `/event/` | 이벤트 \| 패키지 조회 | Events & Packages |
|
||||||
|
| `/specialEvent/` | 캠페인 \| 프로모션 | Campaign & Promotion |
|
||||||
|
| `/timesale/`, `/campaign/` *(placeholder — not actual paths yet, replace when specific paths are registered)* | 캠페인 \| 프로모션 | Campaign & Promotion |
|
||||||
|
| `/activity/` | 액티비티 소개 | Activities |
|
||||||
|
| `/step[0-4]` | 룸, 패키지 예약 프로세스 | Booking Process |
|
||||||
|
| `/resve/dining/` | 다이닝 예약 프로세스 | Dining Booking |
|
||||||
|
| `/membership/` | 멤버쉽 | Membership |
|
||||||
|
| `/mypage/`, `/login/`, `/join/` | 웹회원 관련 | Member Portal |
|
||||||
|
| `/retail/`, `/product/`, `/office/` | 리테일 관련 | Retail |
|
||||||
|
| `/voc/`, `/customer/` | 고객지원 | Customer Support |
|
||||||
|
| `/policy/` | 웹사이트 정책 | Website Policy |
|
||||||
|
| `/upload_file/event/html/` | 캠페인 \| 프로모션 | Campaign & Promotion |
|
||||||
|
| Default | Other | Other |
|
||||||
|
|
||||||
|
### Two Content Group Variables — Old vs New (JHR)
|
||||||
|
|
||||||
|
The JHR container has two content group variables for menu categories:
|
||||||
|
|
||||||
|
| | **Contents Group 2** (ID:1234) | **Contents Group new** (ID:1826) |
|
||||||
|
|---|---|---|
|
||||||
|
| Input | `{{Page Path}}` | `{{Page URL}}` |
|
||||||
|
| Rules | 20 | 24 (replacement) |
|
||||||
|
| Domain matching | No — path only | Yes — full URL with subdomain alternation |
|
||||||
|
| Subdomain coverage | N/A | `www\|app\|jpg\|gjb\|gjj\|grp\|les\|\.` |
|
||||||
|
| Cross-domain | No | Yes — booking, josuntns, lounge subdomains |
|
||||||
|
| Mobile path | `(\/m)?` prefix | `(\/m)?` prefix within URL pattern |
|
||||||
|
|
||||||
|
**"Contents Group new" (ID:1826) is the replacement** — it handles subdomains (booking, T&S, lounge, les) that the old path-based variable cannot distinguish. When auditing, check which variable is referenced in the GA4 config tag — the newer one should be used.
|
||||||
|
|
||||||
|
**Gotcha — adding new subdomains**: When a new hotel brand subdomain launches (e.g., `les.josunhotel.com`), the subdomain alternation group `(www|app|...|les|\.)` in ALL main-domain rules must be updated. This affects 14+ rules in the URL-based variable.
|
||||||
|
|
||||||
|
### Gotcha — Subdomain Regex Dot Separator
|
||||||
|
|
||||||
|
**Bug**: `(www|app|jpg|les|\.)?josunhotel\.com` does NOT match `www.josunhotel.com`.
|
||||||
|
|
||||||
|
The `\.` inside the alternation group matches a literal dot OR `www` matches `www`, but neither consumes the `.` between the subdomain and `josunhotel`. The regex engine sees `www` then expects `josunhotel` but finds `.josunhotel`.
|
||||||
|
|
||||||
|
**Fix**: Move the dot outside the group as an optional separator:
|
||||||
|
```
|
||||||
|
BROKEN: (www|app|jpg|gjb|gjj|grp|les|\.)?josunhotel\.com
|
||||||
|
FIXED: (www|app|jpg|gjb|gjj|grp|les)?\.?josunhotel\.com
|
||||||
|
```
|
||||||
|
|
||||||
|
This correctly matches: `www.josunhotel.com`, `les.josunhotel.com`, `josunhotel.com` (bare), `app.josunhotel.com`.
|
||||||
|
|
||||||
|
**Note**: GTM's "Full Matches Only = OFF" means `re.search` (partial match) is used, which may accidentally match the broken pattern on some URLs. But strict `re.match` or JavaScript `pattern.test()` will fail. Always test with the full URL in JavaScript console.
|
||||||
|
|
||||||
|
### Audit Checklist for Content Grouping
|
||||||
|
|
||||||
|
- [ ] Run `dtm var info <var_id>` to dump the full regex table
|
||||||
|
- [ ] Check if new site sections have matching patterns
|
||||||
|
- [ ] Verify both Path and URL variables are in sync
|
||||||
|
- [ ] Check GA4 config tag references the correct variable
|
||||||
|
- [ ] Test in GTM Preview: navigate to new page, check variable value
|
||||||
|
- [ ] After update, verify in GA4 Realtime > Content Group report
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Performance Issues
|
## Performance Issues
|
||||||
|
|
||||||
### Tags Slowing Page Load
|
### Tags Slowing Page Load
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
# JHR GA4 Tagging Reference
|
||||||
|
|
||||||
|
Source: [JHR - User Behaviour, Event Planning & GA setup](https://docs.google.com/spreadsheets/d/1rhzYApbZ4wbe1gE-E43E-mlRdLl_etkIOZnqs1g9X1U)
|
||||||
|
|
||||||
|
Container: GTM-NP6NJMP (JHR Website - 통합)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Tag Naming Convention
|
||||||
|
|
||||||
|
Format: `{Platform} {TriggerElement} - {Purpose} - {EventName}`
|
||||||
|
|
||||||
|
### Platform Categories
|
||||||
|
|
||||||
|
| Platform | Usage |
|
||||||
|
|----------|-------|
|
||||||
|
| GA4 | Google Analytics 4 events |
|
||||||
|
| Google Ads | Conversion tracking |
|
||||||
|
| Meta | Facebook/Instagram pixel |
|
||||||
|
| Kakao | Kakao pixel/moment |
|
||||||
|
| Marketing | Cross-platform marketing tags |
|
||||||
|
| Analytics | Analytics configuration tags |
|
||||||
|
| Site | Site-specific utility tags |
|
||||||
|
| SEO | SEO-related tags |
|
||||||
|
| Test | Testing/debug tags |
|
||||||
|
| {{Campaign Name}} | Campaign-specific tags (e.g., "LandersFesta") |
|
||||||
|
|
||||||
|
### Trigger Elements (what triggers the tag)
|
||||||
|
|
||||||
|
| Element | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| pageview | Page load / virtual pageview |
|
||||||
|
| click | Generic click |
|
||||||
|
| link | Link click |
|
||||||
|
| button | Button click |
|
||||||
|
| tab | Tab selection |
|
||||||
|
| form | Form submission |
|
||||||
|
| layer | Layer/modal/popup view |
|
||||||
|
| popup | Popup interaction |
|
||||||
|
| banner | Banner click/impression |
|
||||||
|
| dropdown | Dropdown selection |
|
||||||
|
| radio | Radio button selection |
|
||||||
|
| checkbox | Checkbox toggle |
|
||||||
|
| ajax | AJAX request complete |
|
||||||
|
| dataLayer | DataLayer push event |
|
||||||
|
|
||||||
|
### Tag Purposes (page/feature area)
|
||||||
|
|
||||||
|
| Purpose | Area |
|
||||||
|
|---------|------|
|
||||||
|
| booking | Room/package reservation process |
|
||||||
|
| dining | Dining reservation process |
|
||||||
|
| membership | Club Josun membership |
|
||||||
|
| package | Package/event listing & detail |
|
||||||
|
| theme | Theme package pages |
|
||||||
|
| campaign | Campaign/promotion pages |
|
||||||
|
| event | Event pages |
|
||||||
|
| timesale | Time-limited sale pages |
|
||||||
|
| survey | Survey/feedback |
|
||||||
|
| ecommerce | E-commerce tracking |
|
||||||
|
| SEO | SEO schema/config |
|
||||||
|
| site | Site-wide interactions |
|
||||||
|
| config | Configuration/settings |
|
||||||
|
| ux | UX tracking |
|
||||||
|
| test | Testing |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
GA4 pageview - booking - room_booking_step1
|
||||||
|
GA4 button - theme - theme_pkg_detail
|
||||||
|
GA4 click - campaign - kakao_channel_add
|
||||||
|
Kakao banner - timesale - {{Event Name}}
|
||||||
|
Meta form - event - {{Event Name}}
|
||||||
|
Marketing - meta pixel - join_complete
|
||||||
|
Marketing - Kakao Pixel - package detail
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Inherited Settings Variables
|
||||||
|
|
||||||
|
Tags inherit common parameters through shared settings variables.
|
||||||
|
This avoids duplicating parameters across hundreds of tags.
|
||||||
|
|
||||||
|
### GA4 Configuration Setting (shared across ALL GA4 tags)
|
||||||
|
|
||||||
|
| Parameter | Variable | Description |
|
||||||
|
|-----------|----------|-------------|
|
||||||
|
| login_status | {{cjs - login status Variable}} | Login state |
|
||||||
|
| user_lang | locale={{cjs - page language Variable}} | Page language |
|
||||||
|
| contents_group_hotel | {{Contents Group 1 - Hotel Site}} | Hotel brand grouping |
|
||||||
|
| contents_group_menu | {{Contents Group new - Menu Categories}} | Page section grouping |
|
||||||
|
| naver_query | {{URL referrer - naver query}} | Naver search query |
|
||||||
|
| naver_oquery | {{URL referrer - naver oquery}} | Naver original query |
|
||||||
|
|
||||||
|
### GA4 Event Settings (added to event tags)
|
||||||
|
|
||||||
|
| Parameter | Variable |
|
||||||
|
|-----------|----------|
|
||||||
|
| page_path | {{Page Path}} |
|
||||||
|
| login_status | {{cjs - login status Variable}} |
|
||||||
|
| user_lang | {{cjs - page language Variable}} |
|
||||||
|
|
||||||
|
### GA4 eCommerce Settings (added to ecommerce tags)
|
||||||
|
|
||||||
|
| Parameter | Variable |
|
||||||
|
|-----------|----------|
|
||||||
|
| pkg_name | {{dlv - eCommerce - item_name}} |
|
||||||
|
| pkg_hotel | {{dlv - eCommerce - item_brand}} |
|
||||||
|
| pkg_price | {{dlv - eCommerce - price}} |
|
||||||
|
| pkg_room_type | {{dlv - eCommerce - item_variant}} |
|
||||||
|
| pkg_id | {{dlv - eCommerce - item_id}} |
|
||||||
|
| page_path | {{Page Path}} |
|
||||||
|
|
||||||
|
### GA4 Promotion Settings (added to package/event detail tags)
|
||||||
|
|
||||||
|
| Parameter | Variable |
|
||||||
|
|-----------|----------|
|
||||||
|
| page_path | {{Page Path}} |
|
||||||
|
| login_status | {{cjs - login status Variable}} |
|
||||||
|
| user_lang | {{cjs - page language Variable}} |
|
||||||
|
| packageSn | {{URL query - packageSn Variable}} |
|
||||||
|
| themeSn | {{URL query - themeSn Variable}} |
|
||||||
|
| eventSn | {{URL query - eventSn Variable}} |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Standard Parameter Keys
|
||||||
|
|
||||||
|
| Parameter Name | Allowed Values / Variable |
|
||||||
|
|----------------|--------------------------|
|
||||||
|
| event_type | click |
|
||||||
|
| ui | GNB, modal, button, tab, link, banner, footer |
|
||||||
|
| page_path | {{Page Path}} |
|
||||||
|
| login_status | (from cjs variable) |
|
||||||
|
| user_lang | (from cjs variable) |
|
||||||
|
| pkg_name | (package/event title) |
|
||||||
|
| click_text | (clicked element text) |
|
||||||
|
| click_url | (clicked element URL) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Property Codes
|
||||||
|
|
||||||
|
| # | Hotel Name | URL | Website Code | Report Code |
|
||||||
|
|---|-----------|-----|-------------|-------------|
|
||||||
|
| 1 | 조선 팰리스 | jpg.josunhotel.com | JPY | JPG |
|
||||||
|
| 2 | 웨스틴 조선 서울 | josunhotel.com/hotel/westinSeoul.do | TWC | WJS |
|
||||||
|
| 3 | 웨스틴 조선 부산 | josunhotel.com/hotel/westinBusan.do | TWCB | WJB |
|
||||||
|
| 4 | 그랜드 조선 부산 | gjb.josunhotel.com | GJB | GJB |
|
||||||
|
| 5 | 그랜드 조선 제주 | gjj.josunhotel.com | GJJ | GJJ |
|
||||||
|
| 6 | 레스케이프 호텔 | les.josunhotel.com | LESCAPE | LES |
|
||||||
|
| 7 | 그래비티 조선 서울 판교 | grp.josunhotel.com | GRP | GVP |
|
||||||
|
| 8 | 포포인츠 서울역 | josunhotel.com/hotel/fpbsSeoul.do | FPBSS | FPS |
|
||||||
|
| 9 | 포포인츠 서울 명동 | josunhotel.com/hotel/fpbsMyeongdong.do | FPBSM | FPM |
|
||||||
|
| - | 전사 공통 | josunhotel.com | JOSUN HOTEL | JHR |
|
||||||
|
| 10 | 파라스파라 | paraspara.co.kr | - | - |
|
||||||
|
| 11 | 코랄로 바이 조선 | coralloyangyang.com | CBJ | YYH |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Meta / Kakao Pixel Event Mapping
|
||||||
|
|
||||||
|
### Meta (Facebook) Pixel
|
||||||
|
|
||||||
|
| User Action | Event Name | Pixel Code | GTM Tag Name |
|
||||||
|
|-------------|-----------|------------|--------------|
|
||||||
|
| 회원가입 완료 | 등록완료 | CompleteRegistration | Marketing - meta pixel - join_complete |
|
||||||
|
| 이벤트 자세히 보기 | 콘텐츠 조회 | ViewContent | Marketing - meta Pixel - package detail |
|
||||||
|
| 관심상품 담기 | 위시리스트에 추가 | AddToWishlist | Marketing - meta Pixel - bookmark |
|
||||||
|
| 룸/패키지 예약 클릭 | 예약 | Schedule | Marketing - mata Pixel - bookmark |
|
||||||
|
| 잠재고객 | 잠재고객 | Lead | (not configured) |
|
||||||
|
|
||||||
|
### Kakao Pixel
|
||||||
|
|
||||||
|
| User Action | Event Name | Event Code | GTM Tag Name |
|
||||||
|
|-------------|-----------|------------|--------------|
|
||||||
|
| 회원가입 완료 | 회원가입 | CompleteRegistration | Marketing - Kakao Pixel - join member complete |
|
||||||
|
| 이벤트 자세히 보기 | 콘텐츠/상품 조회 | ViewContent | Marketing - Kakao Pixel - package detail |
|
||||||
|
| 관심상품 담기 | 관심상품추가 | AddToWishList | Marketing - Kakao Pixel - bookmark |
|
||||||
|
| 룸/패키지 예약 클릭 | 구매 | Purchase | Marketing - Kakao Pixel - click booking |
|
||||||
|
| 잠재고객 (멤버십) | 잠재고객 | Participation - Consulting | Marketing - Kakao Pixel - button - membership_detail_view |
|
||||||
|
| 잠재고객 (매스프로모션) | 잠재고객 | Participation - PreBooking | Marketing - Kakao pixel - button - massPromotion_detail |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Custom Channel Grouping (JHR)
|
||||||
|
|
||||||
|
| Order | Channel | Custom Rules |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| 1 | Direct | (default) |
|
||||||
|
| 2 | Cross-network | (default) |
|
||||||
|
| 3 | Paid Shopping | (default) |
|
||||||
|
| 4 | Paid Search | (default) |
|
||||||
|
| 5 | Paid Social | + kakao / sms |
|
||||||
|
| 6 | Paid Video | (default) |
|
||||||
|
| 7 | Paid Other | (default) |
|
||||||
|
| 8 | Display | (default) |
|
||||||
|
| 9 | Map Search | place.map.kakao.com, m.place.naver.com, pcmap.place.naver.com, map.kakao.com |
|
||||||
|
| 10 | Organic Shopping | (default) |
|
||||||
|
| 11 | Organic Social | (default) |
|
||||||
|
| 12 | Organic Video | (default) |
|
||||||
|
| 13 | Organic Search | (default) |
|
||||||
|
| 14 | Email | (default) |
|
||||||
|
| 15 | Affiliates | + event.ssg.com, marriott.com, blossom.shinsegae.com |
|
||||||
|
| 16 | Referral | (default) |
|
||||||
|
| 17-19 | Audio / SMS / Mobile Push | (default) |
|
||||||
|
| - | Unassigned | hotelQR / QRcoupon |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. eCommerce Product Categories
|
||||||
|
|
||||||
|
| Category | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| ROOM ONLY | 객실 상품만 제공 |
|
||||||
|
| 멤버십 전용 | CJM 회원 전용 상품 |
|
||||||
|
| 선결제 | 선결제 상품 (현재 미등록) |
|
||||||
|
| 얼리버드 | 조기 예약 상품 |
|
||||||
|
| 조식 | 조식 포함 패키지 |
|
||||||
|
| 패키지 | 프로모션 오퍼 포함 패키지 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Event Catalog (173 events)
|
||||||
|
|
||||||
|
### Booking Process Events
|
||||||
|
| Event Name | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| room_search | Room search initiated |
|
||||||
|
| room_search_home | Room search from homepage |
|
||||||
|
| room_tab_room_only | Room only tab selected |
|
||||||
|
| room_tab_package | Package tab selected |
|
||||||
|
| room_tab_members_only | Members only tab selected |
|
||||||
|
| room_booking_step4 | Booking step 4 (confirmation) |
|
||||||
|
| room_booking_promo | Promo code applied |
|
||||||
|
| book_room | Room booking completed |
|
||||||
|
| begin_checkout | Checkout started |
|
||||||
|
| purchase | Purchase completed |
|
||||||
|
| refund | Refund processed |
|
||||||
|
|
||||||
|
### Dining Events
|
||||||
|
| Event Name | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| dining_booking_start | Dining booking initiated |
|
||||||
|
| dining_hotel_select | Hotel selected for dining |
|
||||||
|
| dining_restaurant_select | Restaurant selected |
|
||||||
|
| dining_step0_view ~ dining_step4_view | Dining booking steps |
|
||||||
|
| dining_book | Dining reservation completed |
|
||||||
|
| dining_menu_view_click | Menu viewed |
|
||||||
|
| dining_search_home | Dining search from home |
|
||||||
|
|
||||||
|
### Package & Event Events
|
||||||
|
| Event Name | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| package_list_view | Package listing viewed |
|
||||||
|
| event_list_view | Event listing viewed |
|
||||||
|
| event_see_detail | Event detail clicked |
|
||||||
|
| theme_pkg_detail | Theme package detail clicked |
|
||||||
|
| pageview_theme_pkg | Theme package page viewed |
|
||||||
|
| bookmark_promotion | Package/event bookmarked |
|
||||||
|
| event_share | Event shared (Kakao/email/SMS/URL) |
|
||||||
|
| massPromotion_detail | Mass promotion detail viewed |
|
||||||
|
|
||||||
|
### Membership Events
|
||||||
|
| Event Name | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| memberED2_list_view | Membership list viewed |
|
||||||
|
| memberED2_join | Membership join started |
|
||||||
|
| memberED2_detail_{TIER} | Tier detail viewed (BLACK/GOLD/PLATINUM/PREMIER/GRANDJOSUN) |
|
||||||
|
| memberED2_detail_share | Membership shared |
|
||||||
|
| memberED2_inquiry | Membership inquiry |
|
||||||
|
|
||||||
|
### Navigation & Site Events
|
||||||
|
| Event Name | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| GNB_all_menu | All menu opened |
|
||||||
|
| GNB_sub_menu_click | Sub menu clicked |
|
||||||
|
| main_select_hotel | Hotel selected from main |
|
||||||
|
| main_room_booking | Room booking from main |
|
||||||
|
| main_dining_booking | Dining booking from main |
|
||||||
|
| hotel_layer_view | Hotel info layer viewed |
|
||||||
|
| login_click | Login clicked |
|
||||||
|
| join_click | Join clicked |
|
||||||
|
| lang_change | Language changed |
|
||||||
|
|
||||||
|
### E-commerce Events (GA4 Standard)
|
||||||
|
| Event Name | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| view_item_list | Item list viewed |
|
||||||
|
| view_item | Item detail viewed |
|
||||||
|
| add_to_cart | Added to cart |
|
||||||
|
| add_to_wishlist | Added to wishlist |
|
||||||
|
| remove_from_wishlist | Removed from wishlist |
|
||||||
|
| begin_checkout | Checkout started |
|
||||||
|
| purchase | Purchase completed |
|
||||||
|
| refund | Refund |
|
||||||
294
custom-skills/61-gtm-editor/code/SKILL.md
Normal file
294
custom-skills/61-gtm-editor/code/SKILL.md
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
---
|
||||||
|
name: gtm-editor
|
||||||
|
description: >
|
||||||
|
GTM implementation toolkit. Creates, updates, and modifies GTM tags, triggers,
|
||||||
|
variables via API. Generates Custom HTML with ES5 compliance. Handles workspace
|
||||||
|
lifecycle, DOM analysis for trigger design, and dataLayer code generation.
|
||||||
|
Triggers on "create GTM tag", "generate dataLayer", "modify trigger", "update variable",
|
||||||
|
"inject tracking code", "write custom HTML", "manage GTM", "design tags",
|
||||||
|
"create conversion tag", "set up tracking".
|
||||||
|
NOT for auditing (use gtm-audit) or validation/QA (use gtm-validator).
|
||||||
|
---
|
||||||
|
|
||||||
|
# GTM Editor Skill
|
||||||
|
|
||||||
|
Create, modify, and deploy GTM configurations via API. Generates ES5-compliant Custom HTML tags.
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
### GTM Container Management (DTM Agent MCP)
|
||||||
|
- `dtm_status` — Check auth and active account/container
|
||||||
|
- `dtm_set_account` / `dtm_set_container` — Switch context
|
||||||
|
- `dtm_list_tags` / `dtm_get_tag` / `dtm_create_tag` / `dtm_update_tag` / `dtm_delete_tag`
|
||||||
|
- `dtm_list_triggers` / `dtm_get_trigger` / `dtm_create_trigger` / `dtm_delete_trigger`
|
||||||
|
- `dtm_list_variables` / `dtm_get_variable` / `dtm_create_variable` / `dtm_delete_variable`
|
||||||
|
- `dtm_list_folders` / `dtm_list_workspaces` / `dtm_get_workspace_status`
|
||||||
|
- `dtm_list_versions` / `dtm_get_live_version` / `dtm_create_version`
|
||||||
|
|
||||||
|
### Browser Analysis (Chrome DevTools MCP)
|
||||||
|
- `navigate_page` — Load page for DOM analysis
|
||||||
|
- `evaluate_script` — Inspect forms, buttons, links, CSS selectors
|
||||||
|
- `take_snapshot` — Get page structure for trigger design
|
||||||
|
- `list_network_requests` — Verify tags fire after changes
|
||||||
|
|
||||||
|
### Reporting (Notion MCP)
|
||||||
|
- Write implementation plans and tag configurations to Notion
|
||||||
|
|
||||||
|
## Tagging Workflow: dataLayer First (MANDATORY)
|
||||||
|
|
||||||
|
**Always push complexity into the dataLayer, not GTM triggers.**
|
||||||
|
|
||||||
|
### Step 1: Design the dataLayer push FIRST
|
||||||
|
|
||||||
|
Before creating any GTM configuration, design the `dataLayer.push()` that the website should implement. Present this to the user as a code snippet matching their tech stack:
|
||||||
|
|
||||||
|
**For vanilla JS / static HTML (ES5):**
|
||||||
|
```javascript
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
window.dataLayer.push({
|
||||||
|
'event': 'generate_lead',
|
||||||
|
'lead_type': document.getElementById('requestType').value,
|
||||||
|
'form_id': 'contact-form'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**For React / Next.js / TypeScript:**
|
||||||
|
```typescript
|
||||||
|
declare global { interface Window { dataLayer: Record<string, any>[]; } }
|
||||||
|
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
window.dataLayer.push({
|
||||||
|
event: 'generate_lead',
|
||||||
|
lead_type: selectedType,
|
||||||
|
form_id: 'contact-form',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**For Vue:**
|
||||||
|
```javascript
|
||||||
|
window.dataLayer?.push({
|
||||||
|
event: 'generate_lead',
|
||||||
|
lead_type: this.selectedType,
|
||||||
|
form_id: 'contact-form',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**For PHP / WordPress:**
|
||||||
|
```php
|
||||||
|
add_action('wp_footer', function() { ?>
|
||||||
|
<script>
|
||||||
|
document.getElementById('contact-form').addEventListener('submit', function() {
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
window.dataLayer.push({
|
||||||
|
'event': 'generate_lead',
|
||||||
|
'lead_type': document.getElementById('requestType').value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php });
|
||||||
|
```
|
||||||
|
|
||||||
|
**ASK the user:** "Here's the dataLayer code for your developers. Can they add this to the page? What's your tech stack?"
|
||||||
|
|
||||||
|
### Step 2: Create simple GTM config
|
||||||
|
|
||||||
|
Once dataLayer push is agreed:
|
||||||
|
- **Trigger:** Custom Event `{{_event}}` equals `generate_lead` (simple, robust)
|
||||||
|
- **Variables:** DataLayer Variable for each parameter (e.g., `dlv - lead_type`)
|
||||||
|
- **Tag:** GA4 Event with `measurementIdOverride` + DLV parameters
|
||||||
|
|
||||||
|
### Step 3: cHTML fallback (LAST RESORT)
|
||||||
|
|
||||||
|
Only if user confirms they CANNOT modify the website code:
|
||||||
|
1. Create a Custom HTML tag that listens for DOM events
|
||||||
|
2. The cHTML pushes to dataLayer internally
|
||||||
|
3. A Custom Event trigger picks it up (same clean pattern)
|
||||||
|
|
||||||
|
**All cHTML must be ES5-compatible** (see ES5 section below).
|
||||||
|
|
||||||
|
### Decision Tree
|
||||||
|
```
|
||||||
|
Track user action?
|
||||||
|
├─ Can devs add dataLayer.push()? → YES → Simple CE trigger ✅
|
||||||
|
├─ Can't modify code, element has ID? → Use Click ID trigger ✅
|
||||||
|
├─ No ID? → Ask user to add one first
|
||||||
|
└─ Nothing possible? → cHTML + CE trigger (last resort)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Capabilities
|
||||||
|
|
||||||
|
### 1. Tag Creation via GTM API
|
||||||
|
|
||||||
|
Create tags directly in GTM — no manual pasting. The API handles workspace lifecycle automatically (auto-creates new workspace if current one is published).
|
||||||
|
|
||||||
|
**GA4 Event Tags:**
|
||||||
|
```
|
||||||
|
GA4 event tags use type "gaawe" with:
|
||||||
|
- eventName: the GA4 event name (snake_case)
|
||||||
|
- measurementIdOverride: {{GA4 Measurement ID variable}} (NOT measurementId)
|
||||||
|
- eventParameters: list of name/value maps
|
||||||
|
```
|
||||||
|
|
||||||
|
**CRITICAL: measurementIdOverride, NOT measurementId**
|
||||||
|
GA4 event tags inherit from config — use `measurementIdOverride` to reference the measurement ID variable. The API rejects `measurementId`.
|
||||||
|
|
||||||
|
### 2. Trigger Design from DOM Analysis
|
||||||
|
|
||||||
|
Analyze page DOM to design precise triggers:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Extract forms
|
||||||
|
(function(){var forms=document.querySelectorAll('form');return JSON.stringify(Array.from(forms).map(function(f){return{id:f.id,action:f.action,fields:Array.from(f.querySelectorAll('input,textarea,select')).map(function(el){return{name:el.name,type:el.type,id:el.id}})}}));})()
|
||||||
|
|
||||||
|
// Extract CTAs and buttons
|
||||||
|
(function(){return JSON.stringify(Array.from(document.querySelectorAll('a[class*="btn"],button,[role="button"]')).map(function(el){return{text:(el.textContent||'').trim().substring(0,50),href:el.href||'',classes:el.className,id:el.id}}));})()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Trigger types:**
|
||||||
|
| Type | Use For | Key Parameter |
|
||||||
|
|------|---------|---------------|
|
||||||
|
| `linkClick` | `<a>` elements | `autoEventFilter` with CSS selector |
|
||||||
|
| `click` | Any element | `filter` on Click ID/Classes |
|
||||||
|
| `formSubmission` | Form submit | `filter` on Form ID |
|
||||||
|
| `customEvent` | dataLayer events | `custom_event_filter` on `{{_event}}` |
|
||||||
|
| `scrollDepth` | Scroll tracking | `verticalThresholdsPercent` |
|
||||||
|
| `timer` | Time on page | `interval`, `limit` |
|
||||||
|
| `domReady` | DOM loaded | No filter needed |
|
||||||
|
|
||||||
|
### 3. Custom HTML Generation (ES5 MANDATORY)
|
||||||
|
|
||||||
|
GTM Custom HTML uses a JavaScript compiler that does NOT support ES2020+.
|
||||||
|
|
||||||
|
**FORBIDDEN:**
|
||||||
|
```javascript
|
||||||
|
// NO: optional chaining, arrow functions, const/let, template literals,
|
||||||
|
// destructuring, spread, async/await
|
||||||
|
```
|
||||||
|
|
||||||
|
**REQUIRED:**
|
||||||
|
```javascript
|
||||||
|
// YES: var, function(){}, string concatenation, explicit property access
|
||||||
|
(function() {
|
||||||
|
var form = document.getElementById('contact-form');
|
||||||
|
if (!form) return;
|
||||||
|
form.addEventListener('submit', function() {
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
window.dataLayer.push({
|
||||||
|
'event': 'generate_lead',
|
||||||
|
'lead_type': form.querySelector('#requestType').value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Workspace Lifecycle Management
|
||||||
|
|
||||||
|
GTM workspaces become **read-only after publishing**. The DTM Agent auto-handles this:
|
||||||
|
- If create/update fails with "Workspace is already submitted"
|
||||||
|
- Auto-creates a new workspace
|
||||||
|
- Retries the operation
|
||||||
|
- Caches the workspace ID for subsequent calls
|
||||||
|
|
||||||
|
**You don't need to manage workspaces manually.** Just call `dtm_create_tag` etc. and it works.
|
||||||
|
|
||||||
|
### 5. DataLayer Code Generation
|
||||||
|
|
||||||
|
Generate dataLayer push code for common tracking scenarios:
|
||||||
|
|
||||||
|
**E-commerce:**
|
||||||
|
```javascript
|
||||||
|
// purchase event
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
window.dataLayer.push({ ecommerce: null }); // Clear previous
|
||||||
|
window.dataLayer.push({
|
||||||
|
'event': 'purchase',
|
||||||
|
'ecommerce': {
|
||||||
|
'transaction_id': /* order ID */,
|
||||||
|
'currency': 'KRW',
|
||||||
|
'value': /* total */,
|
||||||
|
'items': [{ item_id: '', item_name: '', price: 0, quantity: 1 }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Forms/Leads:**
|
||||||
|
```javascript
|
||||||
|
window.dataLayer.push({
|
||||||
|
'event': 'generate_lead',
|
||||||
|
'lead_type': /* form type */,
|
||||||
|
'form_id': /* form element ID */
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Korean Market Patterns
|
||||||
|
|
||||||
|
| Context | Pattern |
|
||||||
|
|---------|---------|
|
||||||
|
| Currency | KRW (no decimals) |
|
||||||
|
| Payment buttons | 장바구니, 결제하기, 주문하기, 문의하기 |
|
||||||
|
| Payment methods | 카카오페이, 네이버페이, 토스, 신용카드 |
|
||||||
|
| Platforms | Kakao Pixel (`track.kakao.com`), Naver Analytics (`wcs.naver.net`) |
|
||||||
|
|
||||||
|
## Supported Tag Types
|
||||||
|
|
||||||
|
| GTM Type | Name | Platform |
|
||||||
|
|----------|------|----------|
|
||||||
|
| `googtag` | Google Tag | GA4 config |
|
||||||
|
| `gaawe` | GA4 Event | GA4 events |
|
||||||
|
| `gclidw` | Conversion Linker | Google Ads |
|
||||||
|
| `awct` | Ads Conversion | Google Ads |
|
||||||
|
| `html` | Custom HTML | Any (Meta, PostHog, Kakao, etc.) |
|
||||||
|
| `cvt_MQDKZ` | Clarity | Microsoft Clarity |
|
||||||
|
| `cvt_WF3R3` | Amplitude | Amplitude |
|
||||||
|
|
||||||
|
## FB Conversions API Integration
|
||||||
|
|
||||||
|
When creating tags that should trigger FB CAPI:
|
||||||
|
1. Check existing FBEventName mapping table (Regex Table variable)
|
||||||
|
2. If your custom event isn't mapped, add a new rule: `event_name → FB_Standard_Event`
|
||||||
|
3. Key mappings: `generate_lead → Lead`, `purchase → Purchase`, `page_view → PageView`
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Analyze**: Use Chrome DevTools to inspect the page (or receive from gtm-audit Mode D)
|
||||||
|
2. **Design**: Determine events, triggers, variables needed
|
||||||
|
3. **Create**: Use DTM Agent MCP tools to create in GTM
|
||||||
|
4. **Verify**: Hand off to gtm-validator for QA
|
||||||
|
5. **Document**: Write implementation details to Notion
|
||||||
|
|
||||||
|
## Trigger Design: IDs First (MANDATORY)
|
||||||
|
|
||||||
|
**Always prefer element `id` over CSS selectors.** IDs survive redesigns; CSS selectors break.
|
||||||
|
|
||||||
|
**Before creating ANY click/form trigger:**
|
||||||
|
1. Check if the target element has an `id` attribute via Chrome DevTools `evaluate_script`
|
||||||
|
2. **If it has an ID** → use `Click ID` or `Form ID` filter
|
||||||
|
3. **If it does NOT have an ID** → **ASK the user** before falling back to CSS:
|
||||||
|
> "This element doesn't have an ID. Can you add one to the website/app code?
|
||||||
|
> Suggested: `id="[section]-[element]-[action]"` e.g., `id="hero-btn-consult"`"
|
||||||
|
4. Only use CSS selectors when the user explicitly confirms they cannot modify the HTML
|
||||||
|
|
||||||
|
**Priority order:**
|
||||||
|
1. Element ID (`Click ID equals "hero-btn-consult"`)
|
||||||
|
2. Data attribute (`Click Element matches CSS [data-track="consult"]`)
|
||||||
|
3. Form ID (`Form ID equals "contact-form"`)
|
||||||
|
4. CSS selector (last resort — document which selectors are used)
|
||||||
|
|
||||||
|
**Suggested ID naming:** `[section]-[element]-[action]`
|
||||||
|
- Navigation: `nav-about`, `nav-services`, `nav-insights`
|
||||||
|
- CTAs: `hero-btn-consult`, `cta-btn-booking`
|
||||||
|
- Forms: `contact-form`, `newsletter-form`
|
||||||
|
- Footer: `footer-social-linkedin`, `footer-email`
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Always use `dtm_status` first to verify auth and active container
|
||||||
|
- Always use `measurementIdOverride` for GA4 event tags (NOT `measurementId`)
|
||||||
|
- **Always prefer element IDs for triggers — ask user to add IDs before using CSS selectors**
|
||||||
|
- All Custom HTML must be ES5-compatible
|
||||||
|
- Use `--dry-run` conceptually — verify trigger selectors via Chrome DevTools before creating
|
||||||
|
- Rate limit: space API calls 1 second apart in batch operations
|
||||||
|
- Never delete tags without explicit user confirmation — move to Archive folder instead
|
||||||
|
- After creating tags, suggest user run gtm-validator to verify
|
||||||
@@ -196,6 +196,121 @@ dataLayer.push({
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Content Grouping Issues (RegEx Table Variables)
|
||||||
|
|
||||||
|
### How Content Grouping Works in GTM
|
||||||
|
|
||||||
|
Content grouping uses **RegEx Table** variables to categorize pages by URL/path patterns.
|
||||||
|
The variable takes an input (typically `{{Page Path}}` or `{{Page URL}}`), tests it against
|
||||||
|
an ordered list of regex patterns, and returns the first matching output value.
|
||||||
|
If no pattern matches, it falls to the **Default Value** (usually "Other").
|
||||||
|
|
||||||
|
**Key settings:**
|
||||||
|
- **Full Matches Only**: OFF — partial regex match is sufficient
|
||||||
|
- **Capture Groups and Replace**: OFF — output is the literal value, not a regex replacement
|
||||||
|
- **Ignore Case**: ON — case-insensitive matching
|
||||||
|
|
||||||
|
### New Page Paths Fall Through to "Other"
|
||||||
|
|
||||||
|
**Symptoms**: New sections (campaigns, special events, landing pages) show as "Other" in GA4 content group reports.
|
||||||
|
|
||||||
|
**Root cause**: The RegEx Table was built for the site's original URL structure. New paths like `/specialEvent/`, `/campaign/`, `/timesale/` don't match any existing pattern.
|
||||||
|
|
||||||
|
**Example**: JHR site has `/specialEvent/landersshoppingfesta2026/teaser.do` but only `/package/` and `/event/` are in the regex table, not `/specialEvent/`.
|
||||||
|
|
||||||
|
**Fix**: Add regex rule for the new path pattern. Place it BEFORE the default.
|
||||||
|
|
||||||
|
**Gotcha — two variables to update**: Sites often have parallel content group variables:
|
||||||
|
1. **Page Path based** (e.g., `(\/m)?\/specialEvent\/(.*)`) — for internal reports
|
||||||
|
2. **Page URL based** (e.g., full URL with domain regex) — for cross-domain or subdomain tracking
|
||||||
|
|
||||||
|
Both must be updated or they'll produce inconsistent groupings.
|
||||||
|
|
||||||
|
### RegEx Pattern Conventions (JHR Example)
|
||||||
|
|
||||||
|
Page Path patterns follow this structure:
|
||||||
|
```
|
||||||
|
(\/m)?\/section\/(.*) → Korean Category Label
|
||||||
|
```
|
||||||
|
- `(\/m)?` — optional mobile path prefix
|
||||||
|
- `\/section\/` — the URL path segment to match
|
||||||
|
- `(.*)` — match anything after
|
||||||
|
|
||||||
|
Page URL patterns include domain matching:
|
||||||
|
```
|
||||||
|
^https\:\/\/(www|app|jpg|gjb|gjj|grp|\.)?josunhotel\.com(\/m)?\/section\/(.*)
|
||||||
|
```
|
||||||
|
- Multi-subdomain support via alternation group
|
||||||
|
- Same `(\/m)?` mobile prefix handling
|
||||||
|
|
||||||
|
### Content Group Categories (Hospitality Reference)
|
||||||
|
|
||||||
|
| Pattern | Category (KR) | Category (EN) |
|
||||||
|
|---------|---------------|---------------|
|
||||||
|
| `/intro.do`, `/main.do` | 메인페이지 | Main Page |
|
||||||
|
| `/about/`, `/press/` | 기업 소개 | Corporate Info |
|
||||||
|
| `/hotel/` | 호텔 소개 | Hotel Info |
|
||||||
|
| `/rooms/` | 객실 소개 | Room Info |
|
||||||
|
| `/dining/` | 다이닝 이용 | Dining |
|
||||||
|
| `/facilities/`, `/meeting/`, `/fitness/` | 부대시설 소개 | Facilities |
|
||||||
|
| `/package/`, `/event/` | 이벤트 \| 패키지 조회 | Events & Packages |
|
||||||
|
| `/specialEvent/` | 캠페인 \| 프로모션 | Campaign & Promotion |
|
||||||
|
| `/timesale/`, `/campaign/` *(placeholder — not actual paths yet, replace when specific paths are registered)* | 캠페인 \| 프로모션 | Campaign & Promotion |
|
||||||
|
| `/activity/` | 액티비티 소개 | Activities |
|
||||||
|
| `/step[0-4]` | 룸, 패키지 예약 프로세스 | Booking Process |
|
||||||
|
| `/resve/dining/` | 다이닝 예약 프로세스 | Dining Booking |
|
||||||
|
| `/membership/` | 멤버쉽 | Membership |
|
||||||
|
| `/mypage/`, `/login/`, `/join/` | 웹회원 관련 | Member Portal |
|
||||||
|
| `/retail/`, `/product/`, `/office/` | 리테일 관련 | Retail |
|
||||||
|
| `/voc/`, `/customer/` | 고객지원 | Customer Support |
|
||||||
|
| `/policy/` | 웹사이트 정책 | Website Policy |
|
||||||
|
| `/upload_file/event/html/` | 캠페인 \| 프로모션 | Campaign & Promotion |
|
||||||
|
| Default | Other | Other |
|
||||||
|
|
||||||
|
### Two Content Group Variables — Old vs New (JHR)
|
||||||
|
|
||||||
|
The JHR container has two content group variables for menu categories:
|
||||||
|
|
||||||
|
| | **Contents Group 2** (ID:1234) | **Contents Group new** (ID:1826) |
|
||||||
|
|---|---|---|
|
||||||
|
| Input | `{{Page Path}}` | `{{Page URL}}` |
|
||||||
|
| Rules | 20 | 24 (replacement) |
|
||||||
|
| Domain matching | No — path only | Yes — full URL with subdomain alternation |
|
||||||
|
| Subdomain coverage | N/A | `www\|app\|jpg\|gjb\|gjj\|grp\|les\|\.` |
|
||||||
|
| Cross-domain | No | Yes — booking, josuntns, lounge subdomains |
|
||||||
|
| Mobile path | `(\/m)?` prefix | `(\/m)?` prefix within URL pattern |
|
||||||
|
|
||||||
|
**"Contents Group new" (ID:1826) is the replacement** — it handles subdomains (booking, T&S, lounge, les) that the old path-based variable cannot distinguish. When auditing, check which variable is referenced in the GA4 config tag — the newer one should be used.
|
||||||
|
|
||||||
|
**Gotcha — adding new subdomains**: When a new hotel brand subdomain launches (e.g., `les.josunhotel.com`), the subdomain alternation group `(www|app|...|les|\.)` in ALL main-domain rules must be updated. This affects 14+ rules in the URL-based variable.
|
||||||
|
|
||||||
|
### Gotcha — Subdomain Regex Dot Separator
|
||||||
|
|
||||||
|
**Bug**: `(www|app|jpg|les|\.)?josunhotel\.com` does NOT match `www.josunhotel.com`.
|
||||||
|
|
||||||
|
The `\.` inside the alternation group matches a literal dot OR `www` matches `www`, but neither consumes the `.` between the subdomain and `josunhotel`. The regex engine sees `www` then expects `josunhotel` but finds `.josunhotel`.
|
||||||
|
|
||||||
|
**Fix**: Move the dot outside the group as an optional separator:
|
||||||
|
```
|
||||||
|
BROKEN: (www|app|jpg|gjb|gjj|grp|les|\.)?josunhotel\.com
|
||||||
|
FIXED: (www|app|jpg|gjb|gjj|grp|les)?\.?josunhotel\.com
|
||||||
|
```
|
||||||
|
|
||||||
|
This correctly matches: `www.josunhotel.com`, `les.josunhotel.com`, `josunhotel.com` (bare), `app.josunhotel.com`.
|
||||||
|
|
||||||
|
**Note**: GTM's "Full Matches Only = OFF" means `re.search` (partial match) is used, which may accidentally match the broken pattern on some URLs. But strict `re.match` or JavaScript `pattern.test()` will fail. Always test with the full URL in JavaScript console.
|
||||||
|
|
||||||
|
### Audit Checklist for Content Grouping
|
||||||
|
|
||||||
|
- [ ] Run `dtm var info <var_id>` to dump the full regex table
|
||||||
|
- [ ] Check if new site sections have matching patterns
|
||||||
|
- [ ] Verify both Path and URL variables are in sync
|
||||||
|
- [ ] Check GA4 config tag references the correct variable
|
||||||
|
- [ ] Test in GTM Preview: navigate to new page, check variable value
|
||||||
|
- [ ] After update, verify in GA4 Realtime > Content Group report
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Performance Issues
|
## Performance Issues
|
||||||
|
|
||||||
### Tags Slowing Page Load
|
### Tags Slowing Page Load
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
# JHR GA4 Tagging Reference
|
||||||
|
|
||||||
|
Source: [JHR - User Behaviour, Event Planning & GA setup](https://docs.google.com/spreadsheets/d/1rhzYApbZ4wbe1gE-E43E-mlRdLl_etkIOZnqs1g9X1U)
|
||||||
|
|
||||||
|
Container: GTM-NP6NJMP (JHR Website - 통합)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Tag Naming Convention
|
||||||
|
|
||||||
|
Format: `{Platform} {TriggerElement} - {Purpose} - {EventName}`
|
||||||
|
|
||||||
|
### Platform Categories
|
||||||
|
|
||||||
|
| Platform | Usage |
|
||||||
|
|----------|-------|
|
||||||
|
| GA4 | Google Analytics 4 events |
|
||||||
|
| Google Ads | Conversion tracking |
|
||||||
|
| Meta | Facebook/Instagram pixel |
|
||||||
|
| Kakao | Kakao pixel/moment |
|
||||||
|
| Marketing | Cross-platform marketing tags |
|
||||||
|
| Analytics | Analytics configuration tags |
|
||||||
|
| Site | Site-specific utility tags |
|
||||||
|
| SEO | SEO-related tags |
|
||||||
|
| Test | Testing/debug tags |
|
||||||
|
| {{Campaign Name}} | Campaign-specific tags (e.g., "LandersFesta") |
|
||||||
|
|
||||||
|
### Trigger Elements (what triggers the tag)
|
||||||
|
|
||||||
|
| Element | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| pageview | Page load / virtual pageview |
|
||||||
|
| click | Generic click |
|
||||||
|
| link | Link click |
|
||||||
|
| button | Button click |
|
||||||
|
| tab | Tab selection |
|
||||||
|
| form | Form submission |
|
||||||
|
| layer | Layer/modal/popup view |
|
||||||
|
| popup | Popup interaction |
|
||||||
|
| banner | Banner click/impression |
|
||||||
|
| dropdown | Dropdown selection |
|
||||||
|
| radio | Radio button selection |
|
||||||
|
| checkbox | Checkbox toggle |
|
||||||
|
| ajax | AJAX request complete |
|
||||||
|
| dataLayer | DataLayer push event |
|
||||||
|
|
||||||
|
### Tag Purposes (page/feature area)
|
||||||
|
|
||||||
|
| Purpose | Area |
|
||||||
|
|---------|------|
|
||||||
|
| booking | Room/package reservation process |
|
||||||
|
| dining | Dining reservation process |
|
||||||
|
| membership | Club Josun membership |
|
||||||
|
| package | Package/event listing & detail |
|
||||||
|
| theme | Theme package pages |
|
||||||
|
| campaign | Campaign/promotion pages |
|
||||||
|
| event | Event pages |
|
||||||
|
| timesale | Time-limited sale pages |
|
||||||
|
| survey | Survey/feedback |
|
||||||
|
| ecommerce | E-commerce tracking |
|
||||||
|
| SEO | SEO schema/config |
|
||||||
|
| site | Site-wide interactions |
|
||||||
|
| config | Configuration/settings |
|
||||||
|
| ux | UX tracking |
|
||||||
|
| test | Testing |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
GA4 pageview - booking - room_booking_step1
|
||||||
|
GA4 button - theme - theme_pkg_detail
|
||||||
|
GA4 click - campaign - kakao_channel_add
|
||||||
|
Kakao banner - timesale - {{Event Name}}
|
||||||
|
Meta form - event - {{Event Name}}
|
||||||
|
Marketing - meta pixel - join_complete
|
||||||
|
Marketing - Kakao Pixel - package detail
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Inherited Settings Variables
|
||||||
|
|
||||||
|
Tags inherit common parameters through shared settings variables.
|
||||||
|
This avoids duplicating parameters across hundreds of tags.
|
||||||
|
|
||||||
|
### GA4 Configuration Setting (shared across ALL GA4 tags)
|
||||||
|
|
||||||
|
| Parameter | Variable | Description |
|
||||||
|
|-----------|----------|-------------|
|
||||||
|
| login_status | {{cjs - login status Variable}} | Login state |
|
||||||
|
| user_lang | locale={{cjs - page language Variable}} | Page language |
|
||||||
|
| contents_group_hotel | {{Contents Group 1 - Hotel Site}} | Hotel brand grouping |
|
||||||
|
| contents_group_menu | {{Contents Group new - Menu Categories}} | Page section grouping |
|
||||||
|
| naver_query | {{URL referrer - naver query}} | Naver search query |
|
||||||
|
| naver_oquery | {{URL referrer - naver oquery}} | Naver original query |
|
||||||
|
|
||||||
|
### GA4 Event Settings (added to event tags)
|
||||||
|
|
||||||
|
| Parameter | Variable |
|
||||||
|
|-----------|----------|
|
||||||
|
| page_path | {{Page Path}} |
|
||||||
|
| login_status | {{cjs - login status Variable}} |
|
||||||
|
| user_lang | {{cjs - page language Variable}} |
|
||||||
|
|
||||||
|
### GA4 eCommerce Settings (added to ecommerce tags)
|
||||||
|
|
||||||
|
| Parameter | Variable |
|
||||||
|
|-----------|----------|
|
||||||
|
| pkg_name | {{dlv - eCommerce - item_name}} |
|
||||||
|
| pkg_hotel | {{dlv - eCommerce - item_brand}} |
|
||||||
|
| pkg_price | {{dlv - eCommerce - price}} |
|
||||||
|
| pkg_room_type | {{dlv - eCommerce - item_variant}} |
|
||||||
|
| pkg_id | {{dlv - eCommerce - item_id}} |
|
||||||
|
| page_path | {{Page Path}} |
|
||||||
|
|
||||||
|
### GA4 Promotion Settings (added to package/event detail tags)
|
||||||
|
|
||||||
|
| Parameter | Variable |
|
||||||
|
|-----------|----------|
|
||||||
|
| page_path | {{Page Path}} |
|
||||||
|
| login_status | {{cjs - login status Variable}} |
|
||||||
|
| user_lang | {{cjs - page language Variable}} |
|
||||||
|
| packageSn | {{URL query - packageSn Variable}} |
|
||||||
|
| themeSn | {{URL query - themeSn Variable}} |
|
||||||
|
| eventSn | {{URL query - eventSn Variable}} |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Standard Parameter Keys
|
||||||
|
|
||||||
|
| Parameter Name | Allowed Values / Variable |
|
||||||
|
|----------------|--------------------------|
|
||||||
|
| event_type | click |
|
||||||
|
| ui | GNB, modal, button, tab, link, banner, footer |
|
||||||
|
| page_path | {{Page Path}} |
|
||||||
|
| login_status | (from cjs variable) |
|
||||||
|
| user_lang | (from cjs variable) |
|
||||||
|
| pkg_name | (package/event title) |
|
||||||
|
| click_text | (clicked element text) |
|
||||||
|
| click_url | (clicked element URL) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Property Codes
|
||||||
|
|
||||||
|
| # | Hotel Name | URL | Website Code | Report Code |
|
||||||
|
|---|-----------|-----|-------------|-------------|
|
||||||
|
| 1 | 조선 팰리스 | jpg.josunhotel.com | JPY | JPG |
|
||||||
|
| 2 | 웨스틴 조선 서울 | josunhotel.com/hotel/westinSeoul.do | TWC | WJS |
|
||||||
|
| 3 | 웨스틴 조선 부산 | josunhotel.com/hotel/westinBusan.do | TWCB | WJB |
|
||||||
|
| 4 | 그랜드 조선 부산 | gjb.josunhotel.com | GJB | GJB |
|
||||||
|
| 5 | 그랜드 조선 제주 | gjj.josunhotel.com | GJJ | GJJ |
|
||||||
|
| 6 | 레스케이프 호텔 | les.josunhotel.com | LESCAPE | LES |
|
||||||
|
| 7 | 그래비티 조선 서울 판교 | grp.josunhotel.com | GRP | GVP |
|
||||||
|
| 8 | 포포인츠 서울역 | josunhotel.com/hotel/fpbsSeoul.do | FPBSS | FPS |
|
||||||
|
| 9 | 포포인츠 서울 명동 | josunhotel.com/hotel/fpbsMyeongdong.do | FPBSM | FPM |
|
||||||
|
| - | 전사 공통 | josunhotel.com | JOSUN HOTEL | JHR |
|
||||||
|
| 10 | 파라스파라 | paraspara.co.kr | - | - |
|
||||||
|
| 11 | 코랄로 바이 조선 | coralloyangyang.com | CBJ | YYH |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Meta / Kakao Pixel Event Mapping
|
||||||
|
|
||||||
|
### Meta (Facebook) Pixel
|
||||||
|
|
||||||
|
| User Action | Event Name | Pixel Code | GTM Tag Name |
|
||||||
|
|-------------|-----------|------------|--------------|
|
||||||
|
| 회원가입 완료 | 등록완료 | CompleteRegistration | Marketing - meta pixel - join_complete |
|
||||||
|
| 이벤트 자세히 보기 | 콘텐츠 조회 | ViewContent | Marketing - meta Pixel - package detail |
|
||||||
|
| 관심상품 담기 | 위시리스트에 추가 | AddToWishlist | Marketing - meta Pixel - bookmark |
|
||||||
|
| 룸/패키지 예약 클릭 | 예약 | Schedule | Marketing - mata Pixel - bookmark |
|
||||||
|
| 잠재고객 | 잠재고객 | Lead | (not configured) |
|
||||||
|
|
||||||
|
### Kakao Pixel
|
||||||
|
|
||||||
|
| User Action | Event Name | Event Code | GTM Tag Name |
|
||||||
|
|-------------|-----------|------------|--------------|
|
||||||
|
| 회원가입 완료 | 회원가입 | CompleteRegistration | Marketing - Kakao Pixel - join member complete |
|
||||||
|
| 이벤트 자세히 보기 | 콘텐츠/상품 조회 | ViewContent | Marketing - Kakao Pixel - package detail |
|
||||||
|
| 관심상품 담기 | 관심상품추가 | AddToWishList | Marketing - Kakao Pixel - bookmark |
|
||||||
|
| 룸/패키지 예약 클릭 | 구매 | Purchase | Marketing - Kakao Pixel - click booking |
|
||||||
|
| 잠재고객 (멤버십) | 잠재고객 | Participation - Consulting | Marketing - Kakao Pixel - button - membership_detail_view |
|
||||||
|
| 잠재고객 (매스프로모션) | 잠재고객 | Participation - PreBooking | Marketing - Kakao pixel - button - massPromotion_detail |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Custom Channel Grouping (JHR)
|
||||||
|
|
||||||
|
| Order | Channel | Custom Rules |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| 1 | Direct | (default) |
|
||||||
|
| 2 | Cross-network | (default) |
|
||||||
|
| 3 | Paid Shopping | (default) |
|
||||||
|
| 4 | Paid Search | (default) |
|
||||||
|
| 5 | Paid Social | + kakao / sms |
|
||||||
|
| 6 | Paid Video | (default) |
|
||||||
|
| 7 | Paid Other | (default) |
|
||||||
|
| 8 | Display | (default) |
|
||||||
|
| 9 | Map Search | place.map.kakao.com, m.place.naver.com, pcmap.place.naver.com, map.kakao.com |
|
||||||
|
| 10 | Organic Shopping | (default) |
|
||||||
|
| 11 | Organic Social | (default) |
|
||||||
|
| 12 | Organic Video | (default) |
|
||||||
|
| 13 | Organic Search | (default) |
|
||||||
|
| 14 | Email | (default) |
|
||||||
|
| 15 | Affiliates | + event.ssg.com, marriott.com, blossom.shinsegae.com |
|
||||||
|
| 16 | Referral | (default) |
|
||||||
|
| 17-19 | Audio / SMS / Mobile Push | (default) |
|
||||||
|
| - | Unassigned | hotelQR / QRcoupon |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. eCommerce Product Categories
|
||||||
|
|
||||||
|
| Category | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| ROOM ONLY | 객실 상품만 제공 |
|
||||||
|
| 멤버십 전용 | CJM 회원 전용 상품 |
|
||||||
|
| 선결제 | 선결제 상품 (현재 미등록) |
|
||||||
|
| 얼리버드 | 조기 예약 상품 |
|
||||||
|
| 조식 | 조식 포함 패키지 |
|
||||||
|
| 패키지 | 프로모션 오퍼 포함 패키지 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Event Catalog (173 events)
|
||||||
|
|
||||||
|
### Booking Process Events
|
||||||
|
| Event Name | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| room_search | Room search initiated |
|
||||||
|
| room_search_home | Room search from homepage |
|
||||||
|
| room_tab_room_only | Room only tab selected |
|
||||||
|
| room_tab_package | Package tab selected |
|
||||||
|
| room_tab_members_only | Members only tab selected |
|
||||||
|
| room_booking_step4 | Booking step 4 (confirmation) |
|
||||||
|
| room_booking_promo | Promo code applied |
|
||||||
|
| book_room | Room booking completed |
|
||||||
|
| begin_checkout | Checkout started |
|
||||||
|
| purchase | Purchase completed |
|
||||||
|
| refund | Refund processed |
|
||||||
|
|
||||||
|
### Dining Events
|
||||||
|
| Event Name | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| dining_booking_start | Dining booking initiated |
|
||||||
|
| dining_hotel_select | Hotel selected for dining |
|
||||||
|
| dining_restaurant_select | Restaurant selected |
|
||||||
|
| dining_step0_view ~ dining_step4_view | Dining booking steps |
|
||||||
|
| dining_book | Dining reservation completed |
|
||||||
|
| dining_menu_view_click | Menu viewed |
|
||||||
|
| dining_search_home | Dining search from home |
|
||||||
|
|
||||||
|
### Package & Event Events
|
||||||
|
| Event Name | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| package_list_view | Package listing viewed |
|
||||||
|
| event_list_view | Event listing viewed |
|
||||||
|
| event_see_detail | Event detail clicked |
|
||||||
|
| theme_pkg_detail | Theme package detail clicked |
|
||||||
|
| pageview_theme_pkg | Theme package page viewed |
|
||||||
|
| bookmark_promotion | Package/event bookmarked |
|
||||||
|
| event_share | Event shared (Kakao/email/SMS/URL) |
|
||||||
|
| massPromotion_detail | Mass promotion detail viewed |
|
||||||
|
|
||||||
|
### Membership Events
|
||||||
|
| Event Name | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| memberED2_list_view | Membership list viewed |
|
||||||
|
| memberED2_join | Membership join started |
|
||||||
|
| memberED2_detail_{TIER} | Tier detail viewed (BLACK/GOLD/PLATINUM/PREMIER/GRANDJOSUN) |
|
||||||
|
| memberED2_detail_share | Membership shared |
|
||||||
|
| memberED2_inquiry | Membership inquiry |
|
||||||
|
|
||||||
|
### Navigation & Site Events
|
||||||
|
| Event Name | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| GNB_all_menu | All menu opened |
|
||||||
|
| GNB_sub_menu_click | Sub menu clicked |
|
||||||
|
| main_select_hotel | Hotel selected from main |
|
||||||
|
| main_room_booking | Room booking from main |
|
||||||
|
| main_dining_booking | Dining booking from main |
|
||||||
|
| hotel_layer_view | Hotel info layer viewed |
|
||||||
|
| login_click | Login clicked |
|
||||||
|
| join_click | Join clicked |
|
||||||
|
| lang_change | Language changed |
|
||||||
|
|
||||||
|
### E-commerce Events (GA4 Standard)
|
||||||
|
| Event Name | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| view_item_list | Item list viewed |
|
||||||
|
| view_item | Item detail viewed |
|
||||||
|
| add_to_cart | Added to cart |
|
||||||
|
| add_to_wishlist | Added to wishlist |
|
||||||
|
| remove_from_wishlist | Removed from wishlist |
|
||||||
|
| begin_checkout | Checkout started |
|
||||||
|
| purchase | Purchase completed |
|
||||||
|
| refund | Refund |
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
# JHR GA4 Tagging Reference
|
||||||
|
|
||||||
|
Source: [JHR - User Behaviour, Event Planning & GA setup](https://docs.google.com/spreadsheets/d/1rhzYApbZ4wbe1gE-E43E-mlRdLl_etkIOZnqs1g9X1U)
|
||||||
|
|
||||||
|
Container: GTM-NP6NJMP (JHR Website - 통합)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Tag Naming Convention
|
||||||
|
|
||||||
|
Format: `{Platform} {TriggerElement} - {Purpose} - {EventName}`
|
||||||
|
|
||||||
|
### Platform Categories
|
||||||
|
|
||||||
|
| Platform | Usage |
|
||||||
|
|----------|-------|
|
||||||
|
| GA4 | Google Analytics 4 events |
|
||||||
|
| Google Ads | Conversion tracking |
|
||||||
|
| Meta | Facebook/Instagram pixel |
|
||||||
|
| Kakao | Kakao pixel/moment |
|
||||||
|
| Marketing | Cross-platform marketing tags |
|
||||||
|
| Analytics | Analytics configuration tags |
|
||||||
|
| Site | Site-specific utility tags |
|
||||||
|
| SEO | SEO-related tags |
|
||||||
|
| Test | Testing/debug tags |
|
||||||
|
| {{Campaign Name}} | Campaign-specific tags (e.g., "LandersFesta") |
|
||||||
|
|
||||||
|
### Trigger Elements (what triggers the tag)
|
||||||
|
|
||||||
|
| Element | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| pageview | Page load / virtual pageview |
|
||||||
|
| click | Generic click |
|
||||||
|
| link | Link click |
|
||||||
|
| button | Button click |
|
||||||
|
| tab | Tab selection |
|
||||||
|
| form | Form submission |
|
||||||
|
| layer | Layer/modal/popup view |
|
||||||
|
| popup | Popup interaction |
|
||||||
|
| banner | Banner click/impression |
|
||||||
|
| dropdown | Dropdown selection |
|
||||||
|
| radio | Radio button selection |
|
||||||
|
| checkbox | Checkbox toggle |
|
||||||
|
| ajax | AJAX request complete |
|
||||||
|
| dataLayer | DataLayer push event |
|
||||||
|
|
||||||
|
### Tag Purposes (page/feature area)
|
||||||
|
|
||||||
|
| Purpose | Area |
|
||||||
|
|---------|------|
|
||||||
|
| booking | Room/package reservation process |
|
||||||
|
| dining | Dining reservation process |
|
||||||
|
| membership | Club Josun membership |
|
||||||
|
| package | Package/event listing & detail |
|
||||||
|
| theme | Theme package pages |
|
||||||
|
| campaign | Campaign/promotion pages |
|
||||||
|
| event | Event pages |
|
||||||
|
| timesale | Time-limited sale pages |
|
||||||
|
| survey | Survey/feedback |
|
||||||
|
| ecommerce | E-commerce tracking |
|
||||||
|
| SEO | SEO schema/config |
|
||||||
|
| site | Site-wide interactions |
|
||||||
|
| config | Configuration/settings |
|
||||||
|
| ux | UX tracking |
|
||||||
|
| test | Testing |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
GA4 pageview - booking - room_booking_step1
|
||||||
|
GA4 button - theme - theme_pkg_detail
|
||||||
|
GA4 click - campaign - kakao_channel_add
|
||||||
|
Kakao banner - timesale - {{Event Name}}
|
||||||
|
Meta form - event - {{Event Name}}
|
||||||
|
Marketing - meta pixel - join_complete
|
||||||
|
Marketing - Kakao Pixel - package detail
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Inherited Settings Variables
|
||||||
|
|
||||||
|
Tags inherit common parameters through shared settings variables.
|
||||||
|
This avoids duplicating parameters across hundreds of tags.
|
||||||
|
|
||||||
|
### GA4 Configuration Setting (shared across ALL GA4 tags)
|
||||||
|
|
||||||
|
| Parameter | Variable | Description |
|
||||||
|
|-----------|----------|-------------|
|
||||||
|
| login_status | {{cjs - login status Variable}} | Login state |
|
||||||
|
| user_lang | locale={{cjs - page language Variable}} | Page language |
|
||||||
|
| contents_group_hotel | {{Contents Group 1 - Hotel Site}} | Hotel brand grouping |
|
||||||
|
| contents_group_menu | {{Contents Group new - Menu Categories}} | Page section grouping |
|
||||||
|
| naver_query | {{URL referrer - naver query}} | Naver search query |
|
||||||
|
| naver_oquery | {{URL referrer - naver oquery}} | Naver original query |
|
||||||
|
|
||||||
|
### GA4 Event Settings (added to event tags)
|
||||||
|
|
||||||
|
| Parameter | Variable |
|
||||||
|
|-----------|----------|
|
||||||
|
| page_path | {{Page Path}} |
|
||||||
|
| login_status | {{cjs - login status Variable}} |
|
||||||
|
| user_lang | {{cjs - page language Variable}} |
|
||||||
|
|
||||||
|
### GA4 eCommerce Settings (added to ecommerce tags)
|
||||||
|
|
||||||
|
| Parameter | Variable |
|
||||||
|
|-----------|----------|
|
||||||
|
| pkg_name | {{dlv - eCommerce - item_name}} |
|
||||||
|
| pkg_hotel | {{dlv - eCommerce - item_brand}} |
|
||||||
|
| pkg_price | {{dlv - eCommerce - price}} |
|
||||||
|
| pkg_room_type | {{dlv - eCommerce - item_variant}} |
|
||||||
|
| pkg_id | {{dlv - eCommerce - item_id}} |
|
||||||
|
| page_path | {{Page Path}} |
|
||||||
|
|
||||||
|
### GA4 Promotion Settings (added to package/event detail tags)
|
||||||
|
|
||||||
|
| Parameter | Variable |
|
||||||
|
|-----------|----------|
|
||||||
|
| page_path | {{Page Path}} |
|
||||||
|
| login_status | {{cjs - login status Variable}} |
|
||||||
|
| user_lang | {{cjs - page language Variable}} |
|
||||||
|
| packageSn | {{URL query - packageSn Variable}} |
|
||||||
|
| themeSn | {{URL query - themeSn Variable}} |
|
||||||
|
| eventSn | {{URL query - eventSn Variable}} |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Standard Parameter Keys
|
||||||
|
|
||||||
|
| Parameter Name | Allowed Values / Variable |
|
||||||
|
|----------------|--------------------------|
|
||||||
|
| event_type | click |
|
||||||
|
| ui | GNB, modal, button, tab, link, banner, footer |
|
||||||
|
| page_path | {{Page Path}} |
|
||||||
|
| login_status | (from cjs variable) |
|
||||||
|
| user_lang | (from cjs variable) |
|
||||||
|
| pkg_name | (package/event title) |
|
||||||
|
| click_text | (clicked element text) |
|
||||||
|
| click_url | (clicked element URL) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Property Codes
|
||||||
|
|
||||||
|
| # | Hotel Name | URL | Website Code | Report Code |
|
||||||
|
|---|-----------|-----|-------------|-------------|
|
||||||
|
| 1 | 조선 팰리스 | jpg.josunhotel.com | JPY | JPG |
|
||||||
|
| 2 | 웨스틴 조선 서울 | josunhotel.com/hotel/westinSeoul.do | TWC | WJS |
|
||||||
|
| 3 | 웨스틴 조선 부산 | josunhotel.com/hotel/westinBusan.do | TWCB | WJB |
|
||||||
|
| 4 | 그랜드 조선 부산 | gjb.josunhotel.com | GJB | GJB |
|
||||||
|
| 5 | 그랜드 조선 제주 | gjj.josunhotel.com | GJJ | GJJ |
|
||||||
|
| 6 | 레스케이프 호텔 | les.josunhotel.com | LESCAPE | LES |
|
||||||
|
| 7 | 그래비티 조선 서울 판교 | grp.josunhotel.com | GRP | GVP |
|
||||||
|
| 8 | 포포인츠 서울역 | josunhotel.com/hotel/fpbsSeoul.do | FPBSS | FPS |
|
||||||
|
| 9 | 포포인츠 서울 명동 | josunhotel.com/hotel/fpbsMyeongdong.do | FPBSM | FPM |
|
||||||
|
| - | 전사 공통 | josunhotel.com | JOSUN HOTEL | JHR |
|
||||||
|
| 10 | 파라스파라 | paraspara.co.kr | - | - |
|
||||||
|
| 11 | 코랄로 바이 조선 | coralloyangyang.com | CBJ | YYH |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Meta / Kakao Pixel Event Mapping
|
||||||
|
|
||||||
|
### Meta (Facebook) Pixel
|
||||||
|
|
||||||
|
| User Action | Event Name | Pixel Code | GTM Tag Name |
|
||||||
|
|-------------|-----------|------------|--------------|
|
||||||
|
| 회원가입 완료 | 등록완료 | CompleteRegistration | Marketing - meta pixel - join_complete |
|
||||||
|
| 이벤트 자세히 보기 | 콘텐츠 조회 | ViewContent | Marketing - meta Pixel - package detail |
|
||||||
|
| 관심상품 담기 | 위시리스트에 추가 | AddToWishlist | Marketing - meta Pixel - bookmark |
|
||||||
|
| 룸/패키지 예약 클릭 | 예약 | Schedule | Marketing - mata Pixel - bookmark |
|
||||||
|
| 잠재고객 | 잠재고객 | Lead | (not configured) |
|
||||||
|
|
||||||
|
### Kakao Pixel
|
||||||
|
|
||||||
|
| User Action | Event Name | Event Code | GTM Tag Name |
|
||||||
|
|-------------|-----------|------------|--------------|
|
||||||
|
| 회원가입 완료 | 회원가입 | CompleteRegistration | Marketing - Kakao Pixel - join member complete |
|
||||||
|
| 이벤트 자세히 보기 | 콘텐츠/상품 조회 | ViewContent | Marketing - Kakao Pixel - package detail |
|
||||||
|
| 관심상품 담기 | 관심상품추가 | AddToWishList | Marketing - Kakao Pixel - bookmark |
|
||||||
|
| 룸/패키지 예약 클릭 | 구매 | Purchase | Marketing - Kakao Pixel - click booking |
|
||||||
|
| 잠재고객 (멤버십) | 잠재고객 | Participation - Consulting | Marketing - Kakao Pixel - button - membership_detail_view |
|
||||||
|
| 잠재고객 (매스프로모션) | 잠재고객 | Participation - PreBooking | Marketing - Kakao pixel - button - massPromotion_detail |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Custom Channel Grouping (JHR)
|
||||||
|
|
||||||
|
| Order | Channel | Custom Rules |
|
||||||
|
|-------|---------|-------------|
|
||||||
|
| 1 | Direct | (default) |
|
||||||
|
| 2 | Cross-network | (default) |
|
||||||
|
| 3 | Paid Shopping | (default) |
|
||||||
|
| 4 | Paid Search | (default) |
|
||||||
|
| 5 | Paid Social | + kakao / sms |
|
||||||
|
| 6 | Paid Video | (default) |
|
||||||
|
| 7 | Paid Other | (default) |
|
||||||
|
| 8 | Display | (default) |
|
||||||
|
| 9 | Map Search | place.map.kakao.com, m.place.naver.com, pcmap.place.naver.com, map.kakao.com |
|
||||||
|
| 10 | Organic Shopping | (default) |
|
||||||
|
| 11 | Organic Social | (default) |
|
||||||
|
| 12 | Organic Video | (default) |
|
||||||
|
| 13 | Organic Search | (default) |
|
||||||
|
| 14 | Email | (default) |
|
||||||
|
| 15 | Affiliates | + event.ssg.com, marriott.com, blossom.shinsegae.com |
|
||||||
|
| 16 | Referral | (default) |
|
||||||
|
| 17-19 | Audio / SMS / Mobile Push | (default) |
|
||||||
|
| - | Unassigned | hotelQR / QRcoupon |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. eCommerce Product Categories
|
||||||
|
|
||||||
|
| Category | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| ROOM ONLY | 객실 상품만 제공 |
|
||||||
|
| 멤버십 전용 | CJM 회원 전용 상품 |
|
||||||
|
| 선결제 | 선결제 상품 (현재 미등록) |
|
||||||
|
| 얼리버드 | 조기 예약 상품 |
|
||||||
|
| 조식 | 조식 포함 패키지 |
|
||||||
|
| 패키지 | 프로모션 오퍼 포함 패키지 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Event Catalog (173 events)
|
||||||
|
|
||||||
|
### Booking Process Events
|
||||||
|
| Event Name | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| room_search | Room search initiated |
|
||||||
|
| room_search_home | Room search from homepage |
|
||||||
|
| room_tab_room_only | Room only tab selected |
|
||||||
|
| room_tab_package | Package tab selected |
|
||||||
|
| room_tab_members_only | Members only tab selected |
|
||||||
|
| room_booking_step4 | Booking step 4 (confirmation) |
|
||||||
|
| room_booking_promo | Promo code applied |
|
||||||
|
| book_room | Room booking completed |
|
||||||
|
| begin_checkout | Checkout started |
|
||||||
|
| purchase | Purchase completed |
|
||||||
|
| refund | Refund processed |
|
||||||
|
|
||||||
|
### Dining Events
|
||||||
|
| Event Name | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| dining_booking_start | Dining booking initiated |
|
||||||
|
| dining_hotel_select | Hotel selected for dining |
|
||||||
|
| dining_restaurant_select | Restaurant selected |
|
||||||
|
| dining_step0_view ~ dining_step4_view | Dining booking steps |
|
||||||
|
| dining_book | Dining reservation completed |
|
||||||
|
| dining_menu_view_click | Menu viewed |
|
||||||
|
| dining_search_home | Dining search from home |
|
||||||
|
|
||||||
|
### Package & Event Events
|
||||||
|
| Event Name | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| package_list_view | Package listing viewed |
|
||||||
|
| event_list_view | Event listing viewed |
|
||||||
|
| event_see_detail | Event detail clicked |
|
||||||
|
| theme_pkg_detail | Theme package detail clicked |
|
||||||
|
| pageview_theme_pkg | Theme package page viewed |
|
||||||
|
| bookmark_promotion | Package/event bookmarked |
|
||||||
|
| event_share | Event shared (Kakao/email/SMS/URL) |
|
||||||
|
| massPromotion_detail | Mass promotion detail viewed |
|
||||||
|
|
||||||
|
### Membership Events
|
||||||
|
| Event Name | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| memberED2_list_view | Membership list viewed |
|
||||||
|
| memberED2_join | Membership join started |
|
||||||
|
| memberED2_detail_{TIER} | Tier detail viewed (BLACK/GOLD/PLATINUM/PREMIER/GRANDJOSUN) |
|
||||||
|
| memberED2_detail_share | Membership shared |
|
||||||
|
| memberED2_inquiry | Membership inquiry |
|
||||||
|
|
||||||
|
### Navigation & Site Events
|
||||||
|
| Event Name | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| GNB_all_menu | All menu opened |
|
||||||
|
| GNB_sub_menu_click | Sub menu clicked |
|
||||||
|
| main_select_hotel | Hotel selected from main |
|
||||||
|
| main_room_booking | Room booking from main |
|
||||||
|
| main_dining_booking | Dining booking from main |
|
||||||
|
| hotel_layer_view | Hotel info layer viewed |
|
||||||
|
| login_click | Login clicked |
|
||||||
|
| join_click | Join clicked |
|
||||||
|
| lang_change | Language changed |
|
||||||
|
|
||||||
|
### E-commerce Events (GA4 Standard)
|
||||||
|
| Event Name | Description |
|
||||||
|
|------------|-------------|
|
||||||
|
| view_item_list | Item list viewed |
|
||||||
|
| view_item | Item detail viewed |
|
||||||
|
| add_to_cart | Added to cart |
|
||||||
|
| add_to_wishlist | Added to wishlist |
|
||||||
|
| remove_from_wishlist | Removed from wishlist |
|
||||||
|
| begin_checkout | Checkout started |
|
||||||
|
| purchase | Purchase completed |
|
||||||
|
| refund | Refund |
|
||||||
225
custom-skills/62-gtm-validator/code/SKILL.md
Normal file
225
custom-skills/62-gtm-validator/code/SKILL.md
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
---
|
||||||
|
name: gtm-validator
|
||||||
|
description: >
|
||||||
|
GTM QA and validation toolkit. Verifies tags fire correctly on live pages,
|
||||||
|
tests trigger conditions against actual DOM, validates dataLayer schemas,
|
||||||
|
checks naming conventions, compares cross-platform events, and runs QA checklists.
|
||||||
|
Uses Chrome DevTools MCP for live page testing.
|
||||||
|
Triggers on "validate tags", "check triggers", "verify tracking", "QA GTM",
|
||||||
|
"test events", "debug GTM", "troubleshoot", "naming conventions",
|
||||||
|
"GTM question", "GTM best practice", "compare versions".
|
||||||
|
NOT for auditing (use gtm-audit) or creating/modifying (use gtm-editor).
|
||||||
|
---
|
||||||
|
|
||||||
|
# GTM Validator Skill
|
||||||
|
|
||||||
|
Verify GTM implementations work correctly on live pages. Test triggers, validate dataLayer, check naming conventions.
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
### Browser Testing (Chrome DevTools MCP)
|
||||||
|
- `navigate_page` — Load page to test
|
||||||
|
- `evaluate_script` — Check dataLayer state, test selectors, verify globals
|
||||||
|
- `list_network_requests` — Verify tag firing after interactions
|
||||||
|
- `click` / `hover` — Simulate user actions to trigger events
|
||||||
|
- `list_console_messages` — Check for JS errors
|
||||||
|
- `take_screenshot` — Document test results
|
||||||
|
|
||||||
|
### GTM Container (DTM Agent MCP)
|
||||||
|
- `dtm_status` — Check active container
|
||||||
|
- `dtm_list_tags` / `dtm_get_tag` — Get tag configurations
|
||||||
|
- `dtm_list_triggers` / `dtm_get_trigger` — Get trigger conditions
|
||||||
|
- `dtm_list_variables` / `dtm_get_variable` — Get variable definitions
|
||||||
|
- `dtm_list_versions` / `dtm_get_live_version` — Compare versions
|
||||||
|
|
||||||
|
## Validation Modes
|
||||||
|
|
||||||
|
### 1. Tag Firing Verification
|
||||||
|
|
||||||
|
Test that specific tags fire on the correct pages and interactions.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. `dtm_list_tags` — get all active tags with their triggers
|
||||||
|
2. `navigate_page` to target URL
|
||||||
|
3. `list_network_requests` — capture initial tag firings
|
||||||
|
4. For click triggers: `click` the element, then check `list_network_requests` again
|
||||||
|
5. Compare: expected tags (from GTM config) vs actual network requests
|
||||||
|
|
||||||
|
**Verification script for specific tag:**
|
||||||
|
```javascript
|
||||||
|
// Check if GA4 event fired
|
||||||
|
(function(){
|
||||||
|
var dl = window.dataLayer || [];
|
||||||
|
var events = dl.filter(function(e){return e.event && e.event !== 'gtm.js' && e.event !== 'gtm.dom' && e.event !== 'gtm.load';});
|
||||||
|
return JSON.stringify(events.map(function(e){return {event: e.event, params: Object.keys(e).filter(function(k){return k !== 'event' && !k.startsWith('gtm.')})}}));
|
||||||
|
})()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Trigger Condition Testing
|
||||||
|
|
||||||
|
Verify trigger CSS selectors and conditions match the actual page DOM.
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. `dtm_get_trigger` — get trigger filter/autoEventFilter
|
||||||
|
2. Extract CSS selectors from trigger config
|
||||||
|
3. `evaluate_script` — test if selectors match elements on page:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Test CSS selector
|
||||||
|
(function(selector){
|
||||||
|
var els = document.querySelectorAll(selector);
|
||||||
|
return JSON.stringify({
|
||||||
|
selector: selector,
|
||||||
|
matchCount: els.length,
|
||||||
|
elements: Array.from(els).slice(0,5).map(function(el){
|
||||||
|
return {tag: el.tagName, id: el.id, classes: el.className, text: (el.textContent||'').trim().substring(0,30)};
|
||||||
|
})
|
||||||
|
});
|
||||||
|
})('.hero a.btn')
|
||||||
|
```
|
||||||
|
|
||||||
|
4. If selector matches 0 elements → trigger will NEVER fire → **CRITICAL issue**
|
||||||
|
5. If selector matches too many elements → trigger may fire incorrectly → **WARNING**
|
||||||
|
|
||||||
|
### 3. DataLayer Schema Validation
|
||||||
|
|
||||||
|
Verify dataLayer pushes contain required fields with correct types.
|
||||||
|
|
||||||
|
**GA4 Recommended Event Schemas:**
|
||||||
|
|
||||||
|
| Event | Required Fields |
|
||||||
|
|-------|----------------|
|
||||||
|
| `page_view` | (automatic) |
|
||||||
|
| `generate_lead` | — (value, currency optional) |
|
||||||
|
| `purchase` | transaction_id, value, currency, items[] |
|
||||||
|
| `add_to_cart` | items[] (item_id, item_name, price, quantity) |
|
||||||
|
| `form_start` | form_id, form_name |
|
||||||
|
| `form_submit` | form_id, form_name |
|
||||||
|
|
||||||
|
**Validation script:**
|
||||||
|
```javascript
|
||||||
|
// Check dataLayer for specific event schema
|
||||||
|
(function(eventName){
|
||||||
|
var dl = window.dataLayer || [];
|
||||||
|
var events = dl.filter(function(e){return e.event === eventName;});
|
||||||
|
if(events.length === 0) return JSON.stringify({found: false, event: eventName});
|
||||||
|
var e = events[events.length-1];
|
||||||
|
return JSON.stringify({found: true, event: eventName, keys: Object.keys(e), data: e});
|
||||||
|
})('generate_lead')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Naming Convention Check
|
||||||
|
|
||||||
|
Verify all GTM resources follow D.intelligence naming standards.
|
||||||
|
|
||||||
|
**Tag naming:**
|
||||||
|
```
|
||||||
|
[Platform] - [event_name] [context]
|
||||||
|
Examples:
|
||||||
|
GA4 - generate_lead (contact form)
|
||||||
|
sGTM - purchase
|
||||||
|
cHTML - Contact Form dataLayer Events
|
||||||
|
FB_CONVERSIONS_API-PIXEL_ID-Web-Tag-GA4_Event
|
||||||
|
```
|
||||||
|
|
||||||
|
**Trigger naming:**
|
||||||
|
```
|
||||||
|
[Type] - [description] Trigger
|
||||||
|
Examples:
|
||||||
|
Click - Hero CTA (consult)
|
||||||
|
CE - contact_form_submit
|
||||||
|
Form Submit - contact-form
|
||||||
|
Scroll - 50pct Depth
|
||||||
|
PV - consult booking Trigger
|
||||||
|
```
|
||||||
|
|
||||||
|
**Variable naming:**
|
||||||
|
```
|
||||||
|
[prefix] - [description]
|
||||||
|
Prefixes: dlv (dataLayer), cjs (Custom JS), aev (Auto-Event),
|
||||||
|
jsv (JS Variable), URL query, cookie, c (Constant)
|
||||||
|
Examples:
|
||||||
|
dlv - contact_form.request_type
|
||||||
|
cjs - Social platform
|
||||||
|
aev - click url hostname
|
||||||
|
```
|
||||||
|
|
||||||
|
**Validation steps:**
|
||||||
|
1. `dtm_list_tags` / `dtm_list_triggers` / `dtm_list_variables`
|
||||||
|
2. Check each name against patterns above
|
||||||
|
3. Flag violations: missing prefix, wrong case, unclear description
|
||||||
|
|
||||||
|
### 5. Cross-Platform Event Mapping Verification
|
||||||
|
|
||||||
|
Verify the same user action sends consistent events to all platforms.
|
||||||
|
|
||||||
|
| User Action | GA4 | Meta (FB) | Google Ads | Kakao | Naver |
|
||||||
|
|------------|-----|-----------|------------|-------|-------|
|
||||||
|
| Page view | page_view | PageView | — | pageView | — |
|
||||||
|
| Product view | view_item | ViewContent | — | viewContent | — |
|
||||||
|
| Add to cart | add_to_cart | AddToCart | — | addToCart | — |
|
||||||
|
| Purchase | purchase | Purchase | purchase | purchase | — |
|
||||||
|
| Lead form | generate_lead | Lead | submit_lead_form | participation | — |
|
||||||
|
|
||||||
|
**Steps:**
|
||||||
|
1. Trigger a user action (click, form submit)
|
||||||
|
2. `list_network_requests` — capture all resulting requests
|
||||||
|
3. Check each platform received the correct event:
|
||||||
|
- GA4: `google-analytics.com/g/collect?en=...`
|
||||||
|
- Meta: `facebook.com/tr/?ev=...`
|
||||||
|
- Google Ads: `googleads.g.doubleclick.net/pagead/...`
|
||||||
|
|
||||||
|
### 6. QA Checklist Execution
|
||||||
|
|
||||||
|
Run through a standard QA checklist for GTM implementation:
|
||||||
|
|
||||||
|
- [ ] GTM container snippet in `<head>` (not `<body>`)
|
||||||
|
- [ ] GTM noscript fallback in `<body>`
|
||||||
|
- [ ] No duplicate GTM containers on same page
|
||||||
|
- [ ] dataLayer initialized before GTM snippet
|
||||||
|
- [ ] All P1 tags fire on page load (GA4 config, Consent Mode)
|
||||||
|
- [ ] Click triggers use specific selectors (not generic classes)
|
||||||
|
- [ ] Form triggers match form IDs that exist on page
|
||||||
|
- [ ] Custom HTML tags use ES5 syntax only
|
||||||
|
- [ ] No console errors related to GTM/tags
|
||||||
|
- [ ] Consent Mode properly blocks tags before consent
|
||||||
|
- [ ] sGTM endpoint responding (no SSL errors)
|
||||||
|
- [ ] Cross-domain tracking configured if needed
|
||||||
|
|
||||||
|
### 7. Version Comparison
|
||||||
|
|
||||||
|
Compare tags between two container versions to identify changes.
|
||||||
|
|
||||||
|
1. `dtm_list_versions` — get version list
|
||||||
|
2. `dtm_get_live_version` — get current live version with all tags
|
||||||
|
3. Compare tag counts, trigger counts, new/modified/deleted items
|
||||||
|
4. Report changes since last publish
|
||||||
|
|
||||||
|
## Reference Files
|
||||||
|
|
||||||
|
| File | Content |
|
||||||
|
|------|---------|
|
||||||
|
| `references/datalayer-schema.md` | GA4 recommended event schemas |
|
||||||
|
| `references/platform-mapping.md` | GA4 ↔ Meta ↔ Kakao ↔ Ads event mapping |
|
||||||
|
| `references/naming-conventions.md` | Tag/trigger/variable naming rules |
|
||||||
|
| `references/qa-checklist.md` | Full QA checklist |
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. **Receive** tag list from gtm-editor (or run `dtm_list_tags`)
|
||||||
|
2. **Navigate** to the target page via Chrome DevTools
|
||||||
|
3. **Test** each trigger's CSS selector matches actual DOM elements
|
||||||
|
4. **Simulate** user actions (clicks, form submits, scrolls)
|
||||||
|
5. **Capture** network requests after each action
|
||||||
|
6. **Verify** correct events fired to correct platforms
|
||||||
|
7. **Report** pass/fail with evidence (screenshots, network logs)
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Always test on the LIVE published version, not preview (unless debugging)
|
||||||
|
- Test on both desktop and mobile viewports when possible
|
||||||
|
- Check consent mode state before flagging missing tags
|
||||||
|
- Document every test result with screenshots or network request evidence
|
||||||
|
- **When a trigger uses CSS selectors instead of IDs, flag it as a fragility risk** — recommend adding `id` attributes to the HTML
|
||||||
|
- If a trigger selector doesn't match, suggest adding an ID to the element first (not a different CSS selector) and hand off to gtm-editor
|
||||||
|
- GTM questions, best practices, and architecture advice are also in scope
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
# DataLayer Schema Reference
|
||||||
|
|
||||||
|
GA4 권장 이벤트 기반 DataLayer 스키마 정의.
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
1. GTM 스니펫 이전에 dataLayer 초기화
|
||||||
|
2. 이벤트 발생 시점에 push (사전 아님)
|
||||||
|
3. ecommerce 객체는 push 전 clear 필수
|
||||||
|
4. PII 데이터 직접 수집 금지
|
||||||
|
|
||||||
|
## Base Structure
|
||||||
|
|
||||||
|
### Page Load
|
||||||
|
```javascript
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
dataLayer.push({
|
||||||
|
'event': 'page_data',
|
||||||
|
'page': {
|
||||||
|
'type': 'home|category|product|cart|checkout|thankyou|content|search',
|
||||||
|
'name': 'Page Name',
|
||||||
|
'category': 'Category/Subcategory',
|
||||||
|
'language': 'ko'
|
||||||
|
},
|
||||||
|
'user': {
|
||||||
|
'status': 'logged_in|logged_out',
|
||||||
|
'id': 'hashed_user_id', // SHA256, 로그인 시에만
|
||||||
|
'type': 'new|returning|member|guest',
|
||||||
|
'loginMethod': 'email|kakao|naver|google'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## E-commerce Events
|
||||||
|
|
||||||
|
### view_item
|
||||||
|
```javascript
|
||||||
|
dataLayer.push({ ecommerce: null });
|
||||||
|
dataLayer.push({
|
||||||
|
'event': 'view_item',
|
||||||
|
'ecommerce': {
|
||||||
|
'currency': 'KRW',
|
||||||
|
'value': 29000,
|
||||||
|
'items': [{
|
||||||
|
'item_id': 'SKU_12345',
|
||||||
|
'item_name': 'Product Name',
|
||||||
|
'item_brand': 'Brand Name',
|
||||||
|
'item_category': 'Category',
|
||||||
|
'item_variant': 'Blue / Large',
|
||||||
|
'price': 29000,
|
||||||
|
'quantity': 1
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### add_to_cart
|
||||||
|
```javascript
|
||||||
|
dataLayer.push({ ecommerce: null });
|
||||||
|
dataLayer.push({
|
||||||
|
'event': 'add_to_cart',
|
||||||
|
'ecommerce': {
|
||||||
|
'currency': 'KRW',
|
||||||
|
'value': 29000,
|
||||||
|
'items': [{
|
||||||
|
'item_id': 'SKU_12345',
|
||||||
|
'item_name': 'Product Name',
|
||||||
|
'price': 29000,
|
||||||
|
'quantity': 1
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### begin_checkout
|
||||||
|
```javascript
|
||||||
|
dataLayer.push({ ecommerce: null });
|
||||||
|
dataLayer.push({
|
||||||
|
'event': 'begin_checkout',
|
||||||
|
'ecommerce': {
|
||||||
|
'currency': 'KRW',
|
||||||
|
'value': 58000,
|
||||||
|
'coupon': 'WELCOME10',
|
||||||
|
'items': [/* cart items */]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### purchase
|
||||||
|
```javascript
|
||||||
|
dataLayer.push({ ecommerce: null });
|
||||||
|
dataLayer.push({
|
||||||
|
'event': 'purchase',
|
||||||
|
'ecommerce': {
|
||||||
|
'transaction_id': 'T_20250115_001',
|
||||||
|
'affiliation': 'Store Name',
|
||||||
|
'value': 53000,
|
||||||
|
'tax': 4818,
|
||||||
|
'shipping': 3000,
|
||||||
|
'currency': 'KRW',
|
||||||
|
'coupon': 'WELCOME10',
|
||||||
|
'items': [{
|
||||||
|
'item_id': 'SKU_12345',
|
||||||
|
'item_name': 'Product Name',
|
||||||
|
'price': 29000,
|
||||||
|
'quantity': 1
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lead Gen Events
|
||||||
|
|
||||||
|
### generate_lead
|
||||||
|
```javascript
|
||||||
|
dataLayer.push({
|
||||||
|
'event': 'generate_lead',
|
||||||
|
'currency': 'KRW',
|
||||||
|
'value': 50000,
|
||||||
|
'lead': {
|
||||||
|
'source': 'contact_form|landing_page',
|
||||||
|
'type': 'inquiry|quote_request'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### form_submit
|
||||||
|
```javascript
|
||||||
|
dataLayer.push({
|
||||||
|
'event': 'form_submit',
|
||||||
|
'form': {
|
||||||
|
'id': 'contact_form',
|
||||||
|
'name': 'Contact Us Form',
|
||||||
|
'type': 'contact',
|
||||||
|
'success': true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Engagement Events
|
||||||
|
|
||||||
|
### login / sign_up
|
||||||
|
```javascript
|
||||||
|
dataLayer.push({
|
||||||
|
'event': 'login', // or 'sign_up'
|
||||||
|
'method': 'email|kakao|naver|google'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### search
|
||||||
|
```javascript
|
||||||
|
dataLayer.push({
|
||||||
|
'event': 'search',
|
||||||
|
'search_term': 'user search query',
|
||||||
|
'search': {
|
||||||
|
'results_count': 42,
|
||||||
|
'category': 'product|content'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Item Object Reference
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|-----------|------|----------|-------------|
|
||||||
|
| item_id | string | Yes | SKU |
|
||||||
|
| item_name | string | Yes | 상품명 |
|
||||||
|
| price | number | Yes | 단가 |
|
||||||
|
| quantity | number | Yes | 수량 |
|
||||||
|
| item_brand | string | No | 브랜드명 |
|
||||||
|
| item_category | string | No | 카테고리 |
|
||||||
|
| item_category2-5 | string | No | 하위 카테고리 |
|
||||||
|
| item_variant | string | No | 옵션 |
|
||||||
|
| coupon | string | No | 쿠폰 코드 |
|
||||||
|
| discount | number | No | 할인 금액 |
|
||||||
|
| index | number | No | 목록 위치 |
|
||||||
|
|
||||||
|
## Validation Rules
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ✅ Correct
|
||||||
|
'price': 29000 // number
|
||||||
|
'quantity': 1 // number
|
||||||
|
'currency': 'KRW' // uppercase
|
||||||
|
|
||||||
|
// ❌ Wrong
|
||||||
|
'price': '29000' // string
|
||||||
|
'currency': 'krw' // lowercase
|
||||||
|
```
|
||||||
|
|
||||||
|
### Array Limits
|
||||||
|
- items[]: 최대 200개
|
||||||
|
- 초과 시 분할 전송
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# GTM Naming Conventions
|
||||||
|
|
||||||
|
일관된 GTM 컨테이너 관리를 위한 네이밍 규칙.
|
||||||
|
|
||||||
|
## Tag Naming
|
||||||
|
|
||||||
|
### Pattern
|
||||||
|
```
|
||||||
|
[Platform] - [Type] - [Description]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
| Platform | Type | Description | Tag Name |
|
||||||
|
|----------|------|-------------|----------|
|
||||||
|
| GA4 | Config | Main | GA4 - Config - Main |
|
||||||
|
| GA4 | Event | purchase | GA4 - Event - purchase |
|
||||||
|
| GAds | Conversion | Purchase | GAds - Conversion - Purchase |
|
||||||
|
| Meta | Event | Purchase | Meta - Event - Purchase |
|
||||||
|
| Kakao | Event | purchase | Kakao - Event - purchase |
|
||||||
|
|
||||||
|
## Trigger Naming
|
||||||
|
|
||||||
|
### Pattern
|
||||||
|
```
|
||||||
|
[Type] - [Condition] - [Context]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Codes
|
||||||
|
| Code | Type | Example |
|
||||||
|
|------|------|---------|
|
||||||
|
| PV | Page View | PV - All Pages |
|
||||||
|
| DL | DataLayer Event | DL - purchase |
|
||||||
|
| Click | Click | Click - CTA Button |
|
||||||
|
| Form | Form Submit | Form - Contact |
|
||||||
|
| Scroll | Scroll Depth | Scroll - 75% |
|
||||||
|
| Vis | Element Visibility | Vis - Video Player |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
```
|
||||||
|
PV - All Pages
|
||||||
|
PV - Thank You Page
|
||||||
|
DL - purchase
|
||||||
|
DL - add_to_cart
|
||||||
|
Click - CTA Button - Hero
|
||||||
|
Form - Contact Form
|
||||||
|
Scroll - 90%
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variable Naming
|
||||||
|
|
||||||
|
### Pattern
|
||||||
|
```
|
||||||
|
[Type] - [Name/Path]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Codes
|
||||||
|
| Code | Type | Example |
|
||||||
|
|------|------|---------|
|
||||||
|
| DLV | DataLayer Variable | DLV - ecommerce.value |
|
||||||
|
| CJS | Custom JavaScript | CJS - Item IDs Array |
|
||||||
|
| Const | Constant | Const - GA4 Measurement ID |
|
||||||
|
| LT | Lookup Table | LT - Page Type Mapping |
|
||||||
|
| URL | URL Variable | URL - Path |
|
||||||
|
| DOM | DOM Element | DOM - Product Title |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
```
|
||||||
|
DLV - event
|
||||||
|
DLV - ecommerce.transaction_id
|
||||||
|
DLV - ecommerce.items
|
||||||
|
CJS - Items for Meta
|
||||||
|
CJS - Formatted Price
|
||||||
|
Const - GA4 Measurement ID
|
||||||
|
Const - Meta Pixel ID
|
||||||
|
LT - Payment Method Mapping
|
||||||
|
```
|
||||||
|
|
||||||
|
## Folder Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
📁 01. Configuration
|
||||||
|
📁 02. GA4 Events
|
||||||
|
📁 03. Google Ads
|
||||||
|
📁 04. Meta Pixel
|
||||||
|
📁 05. Kakao Pixel
|
||||||
|
📁 06. Other Platforms
|
||||||
|
📁 07. Utilities
|
||||||
|
📁 99. Deprecated
|
||||||
|
```
|
||||||
|
|
||||||
|
## Built-in Variables to Enable
|
||||||
|
|
||||||
|
```
|
||||||
|
Pages: ✅ Page URL, Path, Hostname, Referrer
|
||||||
|
Utilities: ✅ Event, Container ID, Random Number
|
||||||
|
Clicks: ✅ Element, Classes, ID, URL, Text
|
||||||
|
Forms: ✅ Element, Classes, ID, URL, Text
|
||||||
|
History: ✅ New/Old History Fragment/State
|
||||||
|
Videos: ✅ All YouTube variables
|
||||||
|
Scrolling: ✅ Depth Threshold, Units, Direction
|
||||||
|
Visibility: ✅ Percent Visible, On-Screen Duration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
```
|
||||||
|
❌ Tag 1, New Tag, Test
|
||||||
|
❌ GA4 purchase event (일관성 없음)
|
||||||
|
❌ G4-E-Pch (과도한 약어)
|
||||||
|
❌ Very Long Tag Name for Purchase Complete Event (너무 김)
|
||||||
|
|
||||||
|
✅ GA4 - Event - purchase
|
||||||
|
✅ DL - purchase
|
||||||
|
✅ CJS - Items for Meta
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version Notes
|
||||||
|
|
||||||
|
```
|
||||||
|
v[Major].[Minor] - [Description]
|
||||||
|
예: v12.1 - Added form_submit event
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
- Added:
|
||||||
|
- Modified:
|
||||||
|
- Removed:
|
||||||
|
|
||||||
|
## Reason
|
||||||
|
[변경 이유]
|
||||||
|
```
|
||||||
215
custom-skills/62-gtm-validator/code/references/phase6-audit.md
Normal file
215
custom-skills/62-gtm-validator/code/references/phase6-audit.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# Phase 6: Progressive Audit & Update
|
||||||
|
|
||||||
|
설정된 GTM Container에 대한 주기적 진단, 유효성 검증, 마이너 업데이트 및 태그 추가.
|
||||||
|
|
||||||
|
## Objectives
|
||||||
|
|
||||||
|
1. GTM 컨테이너 건강 상태 점검
|
||||||
|
2. 태그 발화 및 데이터 정합성 검증
|
||||||
|
3. 최신 요구사항 반영 업데이트
|
||||||
|
4. D.intelligence GTM Toolkit 연계 자동화
|
||||||
|
|
||||||
|
## Audit Schedule
|
||||||
|
|
||||||
|
| Frequency | Scope | Trigger |
|
||||||
|
|-----------|-------|---------|
|
||||||
|
| Weekly | Tag firing validation | 자동 스케줄 |
|
||||||
|
| Monthly | Full container review | 월간 리포트 |
|
||||||
|
| Quarterly | Architecture review | 분기 회의 |
|
||||||
|
| Ad-hoc | Issue investigation | 이슈 발생 시 |
|
||||||
|
|
||||||
|
## Audit Checklist
|
||||||
|
|
||||||
|
### 1. Container Health Check
|
||||||
|
- [ ] 버전 히스토리 확인 (최근 변경사항)
|
||||||
|
- [ ] 미사용 태그/트리거/변수 식별
|
||||||
|
- [ ] 명명 규칙 준수 여부
|
||||||
|
- [ ] 폴더 구조 정리 상태
|
||||||
|
|
||||||
|
### 2. Tag Firing Validation
|
||||||
|
- [ ] 모든 P1 태그 정상 발화
|
||||||
|
- [ ] 트리거 조건 정확성
|
||||||
|
- [ ] 변수 값 정확성
|
||||||
|
- [ ] 차단 트리거 작동
|
||||||
|
|
||||||
|
### 3. Data Quality
|
||||||
|
- [ ] GA4 Realtime 데이터 확인
|
||||||
|
- [ ] 전환 데이터 정합성
|
||||||
|
- [ ] 파라미터 값 검증
|
||||||
|
- [ ] 이상치 탐지
|
||||||
|
|
||||||
|
### 4. Platform Sync
|
||||||
|
- [ ] GA4 ↔ GTM 동기화
|
||||||
|
- [ ] Google Ads 전환 데이터
|
||||||
|
- [ ] Meta Events Manager 상태
|
||||||
|
- [ ] Kakao Pixel 상태
|
||||||
|
|
||||||
|
## D.intelligence GTM Toolkit Integration
|
||||||
|
|
||||||
|
### Repository
|
||||||
|
```
|
||||||
|
https://github.com/ourdigital/dintel-gtm-toolkit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Capabilities
|
||||||
|
|
||||||
|
| Feature | Description | Use Case |
|
||||||
|
|---------|-------------|----------|
|
||||||
|
| Container Analysis | JSON 파싱, 구조 분석 | 컨테이너 리뷰 |
|
||||||
|
| Dependency Mapping | 태그↔트리거↔변수 관계 | 영향도 분석 |
|
||||||
|
| Version Diff | 버전 간 변경사항 비교 | 변경 추적 |
|
||||||
|
| Tag Validation | 발화 상태 자동 검증 | 정기 감사 |
|
||||||
|
|
||||||
|
### Integration Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. GTM Container Export
|
||||||
|
└── JSON 파일 다운로드
|
||||||
|
|
||||||
|
2. D.intel Toolkit Analysis
|
||||||
|
├── python analyze_container.py container.json
|
||||||
|
├── Dependency graph 생성
|
||||||
|
└── Issue detection
|
||||||
|
|
||||||
|
3. Report Generation
|
||||||
|
└── Markdown/HTML 리포트
|
||||||
|
|
||||||
|
4. GTM Guardian Update
|
||||||
|
└── Notion에 리포트 저장
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sample Toolkit Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Container 분석
|
||||||
|
python analyze_container.py GTM-XXXXXX.json --output report.md
|
||||||
|
|
||||||
|
# 버전 비교
|
||||||
|
python diff_versions.py v1.json v2.json --output diff.md
|
||||||
|
|
||||||
|
# 태그 검증
|
||||||
|
python validate_tags.py container.json --events events.csv
|
||||||
|
|
||||||
|
# 미사용 요소 탐지
|
||||||
|
python find_unused.py container.json --type all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Audit Report Template
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# GTM Audit Report
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
- **Container**: GTM-XXXXXX
|
||||||
|
- **Audit Date**: YYYY-MM-DD
|
||||||
|
- **Version Reviewed**: XX
|
||||||
|
- **Overall Status**: ✅ Healthy / ⚠️ Needs Attention / ❌ Critical
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
### ✅ Passing
|
||||||
|
- [항목]
|
||||||
|
|
||||||
|
### ⚠️ Warnings
|
||||||
|
| Issue | Impact | Recommendation |
|
||||||
|
|-------|--------|----------------|
|
||||||
|
|
||||||
|
### ❌ Critical
|
||||||
|
| Issue | Impact | Action Required |
|
||||||
|
|-------|--------|-----------------|
|
||||||
|
|
||||||
|
## Tag Firing Status
|
||||||
|
|
||||||
|
| Tag | Expected | Actual | Status |
|
||||||
|
|-----|----------|--------|--------|
|
||||||
|
| GA4 - Event - purchase | Thank You | Thank You | ✅ |
|
||||||
|
|
||||||
|
## Data Quality Check
|
||||||
|
|
||||||
|
| Metric | Expected | Actual | Variance |
|
||||||
|
|--------|----------|--------|----------|
|
||||||
|
| Daily Purchases | ~50 | 48 | -4% |
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
- [ ]
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
## Appendix
|
||||||
|
- Container JSON snapshot
|
||||||
|
- D.intel Toolkit output
|
||||||
|
```
|
||||||
|
|
||||||
|
## Update Procedures
|
||||||
|
|
||||||
|
### Minor Update (태그 수정)
|
||||||
|
```
|
||||||
|
1. 변경 필요 태그 식별
|
||||||
|
2. Preview 모드에서 수정
|
||||||
|
3. 테스트 완료
|
||||||
|
4. Publish with version note
|
||||||
|
5. Notion 문서 업데이트
|
||||||
|
```
|
||||||
|
|
||||||
|
### Major Update (신규 태그 추가)
|
||||||
|
```
|
||||||
|
1. Phase 3-4 문서 업데이트
|
||||||
|
2. DataLayer 요구사항 개발팀 전달
|
||||||
|
3. GTM 태그/트리거/변수 설정
|
||||||
|
4. QA 체크리스트 수행
|
||||||
|
5. 단계적 배포
|
||||||
|
6. Notion 문서 업데이트
|
||||||
|
```
|
||||||
|
|
||||||
|
### Emergency Fix
|
||||||
|
```
|
||||||
|
1. 이슈 원인 파악
|
||||||
|
2. 최소 변경으로 수정
|
||||||
|
3. Publish (emergency version note)
|
||||||
|
4. 근본 원인 분석
|
||||||
|
5. 영구 수정 계획
|
||||||
|
```
|
||||||
|
|
||||||
|
## Versioning Best Practices
|
||||||
|
|
||||||
|
### Version Naming
|
||||||
|
```
|
||||||
|
v[Major].[Minor] - [Description]
|
||||||
|
예: v12.1 - Added form_submit event
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version Notes Template
|
||||||
|
```
|
||||||
|
## Changes
|
||||||
|
- Added: [새로 추가된 항목]
|
||||||
|
- Modified: [수정된 항목]
|
||||||
|
- Removed: [삭제된 항목]
|
||||||
|
|
||||||
|
## Reason
|
||||||
|
[변경 이유]
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
- [테스트 항목 및 결과]
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
[롤백 필요 시 절차]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring Setup
|
||||||
|
|
||||||
|
### GA4 Alerts
|
||||||
|
- 일일 전환 수 급감 (>30%)
|
||||||
|
- 세션 수 급감 (>50%)
|
||||||
|
- 이벤트 수집 오류
|
||||||
|
|
||||||
|
### Custom Monitoring
|
||||||
|
- DataLayer 오류 로깅
|
||||||
|
- Tag Manager 오류 추적
|
||||||
|
- 서버 사이드 태그 상태 (sGTM 시)
|
||||||
|
|
||||||
|
## Next Phase
|
||||||
|
|
||||||
|
주기적 감사 완료 후 → Phase 7: Lookup App (필요 시)
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
# Phase 7: Event Lookup & Parameter Library Web App
|
||||||
|
|
||||||
|
Google Apps Script를 활용한 Event Taxonomy Sheet 기반 업무 지원용 앱 배포.
|
||||||
|
|
||||||
|
## Objectives
|
||||||
|
|
||||||
|
1. Event Taxonomy 데이터 조회 앱
|
||||||
|
2. 실시간 파라미터 참조 도구
|
||||||
|
3. 비개발자용 Self-service 도구
|
||||||
|
4. 팀 간 일관된 이벤트 정보 공유
|
||||||
|
|
||||||
|
## App Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
|
│ Google Sheets │────▶│ Apps Script │────▶│ Web App │
|
||||||
|
│ (Taxonomy DB) │ │ (Backend) │ │ (Frontend) │
|
||||||
|
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Google Sheets Data Source
|
||||||
|
|
||||||
|
### Required Sheets
|
||||||
|
- Events Master
|
||||||
|
- Parameters Reference
|
||||||
|
- Custom Definitions
|
||||||
|
- Platform Mapping
|
||||||
|
|
||||||
|
### Column Requirements
|
||||||
|
→ Phase 4 참조: [phase4-taxonomy.md](phase4-taxonomy.md)
|
||||||
|
|
||||||
|
## Apps Script Setup
|
||||||
|
|
||||||
|
### 1. Project Creation
|
||||||
|
```
|
||||||
|
1. Google Sheets 열기
|
||||||
|
2. Extensions > Apps Script
|
||||||
|
3. 프로젝트 이름 설정
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Core Functions
|
||||||
|
|
||||||
|
#### Data Retrieval
|
||||||
|
```javascript
|
||||||
|
function getEventsData() {
|
||||||
|
const sheet = SpreadsheetApp.getActiveSpreadsheet()
|
||||||
|
.getSheetByName('Events Master');
|
||||||
|
const data = sheet.getDataRange().getValues();
|
||||||
|
const headers = data[0];
|
||||||
|
|
||||||
|
return data.slice(1).map(row => {
|
||||||
|
const obj = {};
|
||||||
|
headers.forEach((header, i) => {
|
||||||
|
obj[header] = row[i];
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParametersData() {
|
||||||
|
const sheet = SpreadsheetApp.getActiveSpreadsheet()
|
||||||
|
.getSheetByName('Parameters Reference');
|
||||||
|
const data = sheet.getDataRange().getValues();
|
||||||
|
const headers = data[0];
|
||||||
|
|
||||||
|
return data.slice(1).map(row => {
|
||||||
|
const obj = {};
|
||||||
|
headers.forEach((header, i) => {
|
||||||
|
obj[header] = row[i];
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Search Function
|
||||||
|
```javascript
|
||||||
|
function searchEvents(query) {
|
||||||
|
const events = getEventsData();
|
||||||
|
const searchTerm = query.toLowerCase();
|
||||||
|
|
||||||
|
return events.filter(event =>
|
||||||
|
event.event_name.toLowerCase().includes(searchTerm) ||
|
||||||
|
event.event_category.toLowerCase().includes(searchTerm) ||
|
||||||
|
event.notes.toLowerCase().includes(searchTerm)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventDetails(eventName) {
|
||||||
|
const events = getEventsData();
|
||||||
|
const params = getParametersData();
|
||||||
|
|
||||||
|
const event = events.find(e => e.event_name === eventName);
|
||||||
|
if (!event) return null;
|
||||||
|
|
||||||
|
const eventParams = event.parameters.split(',').map(p => p.trim());
|
||||||
|
const paramDetails = params.filter(p =>
|
||||||
|
eventParams.includes(p.parameter_name)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
event: event,
|
||||||
|
parameters: paramDetails
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Web App Handler
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function doGet(e) {
|
||||||
|
return HtmlService.createHtmlOutputFromFile('index')
|
||||||
|
.setTitle('GTM Event Lookup')
|
||||||
|
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doPost(e) {
|
||||||
|
const action = e.parameter.action;
|
||||||
|
|
||||||
|
switch(action) {
|
||||||
|
case 'search':
|
||||||
|
return ContentService.createTextOutput(
|
||||||
|
JSON.stringify(searchEvents(e.parameter.query))
|
||||||
|
).setMimeType(ContentService.MimeType.JSON);
|
||||||
|
|
||||||
|
case 'details':
|
||||||
|
return ContentService.createTextOutput(
|
||||||
|
JSON.stringify(getEventDetails(e.parameter.eventName))
|
||||||
|
).setMimeType(ContentService.MimeType.JSON);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ContentService.createTextOutput(
|
||||||
|
JSON.stringify({error: 'Invalid action'})
|
||||||
|
).setMimeType(ContentService.MimeType.JSON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. HTML Frontend (index.html)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<base target="_top">
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Roboto', sans-serif; padding: 20px; max-width: 800px; margin: 0 auto; }
|
||||||
|
.search-box { width: 100%; padding: 12px; font-size: 16px; margin-bottom: 20px; }
|
||||||
|
.event-card { border: 1px solid #ddd; padding: 16px; margin: 10px 0; border-radius: 8px; }
|
||||||
|
.event-name { font-size: 18px; font-weight: bold; color: #1a73e8; }
|
||||||
|
.param-badge { background: #e8f0fe; color: #1967d2; padding: 4px 8px; border-radius: 4px; margin: 2px; display: inline-block; }
|
||||||
|
.category { color: #5f6368; font-size: 14px; }
|
||||||
|
.priority-p1 { border-left: 4px solid #ea4335; }
|
||||||
|
.priority-p2 { border-left: 4px solid #fbbc04; }
|
||||||
|
.priority-p3 { border-left: 4px solid #34a853; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>🏷️ GTM Event Lookup</h1>
|
||||||
|
<input type="text" class="search-box" id="searchInput" placeholder="이벤트명 또는 카테고리 검색...">
|
||||||
|
<div id="results"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
const results = document.getElementById('results');
|
||||||
|
|
||||||
|
searchInput.addEventListener('input', debounce(function() {
|
||||||
|
const query = this.value;
|
||||||
|
if (query.length < 2) {
|
||||||
|
results.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
google.script.run
|
||||||
|
.withSuccessHandler(displayResults)
|
||||||
|
.searchEvents(query);
|
||||||
|
}, 300));
|
||||||
|
|
||||||
|
function displayResults(events) {
|
||||||
|
results.innerHTML = events.map(event => `
|
||||||
|
<div class="event-card priority-${event.priority.toLowerCase()}">
|
||||||
|
<div class="event-name">${event.event_name}</div>
|
||||||
|
<div class="category">${event.event_category} | ${event.priority}</div>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
${event.parameters.split(',').map(p =>
|
||||||
|
`<span class="param-badge">${p.trim()}</span>`
|
||||||
|
).join('')}
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 10px; color: #5f6368;">${event.notes}</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function(...args) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### 1. Deploy as Web App
|
||||||
|
```
|
||||||
|
1. Deploy > New deployment
|
||||||
|
2. Type: Web app
|
||||||
|
3. Execute as: Me
|
||||||
|
4. Access: Anyone within organization (또는 Anyone)
|
||||||
|
5. Deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Get Web App URL
|
||||||
|
```
|
||||||
|
배포 후 제공되는 URL 복사
|
||||||
|
https://script.google.com/macros/s/[ID]/exec
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Share with Team
|
||||||
|
- URL을 Notion/Slack에 공유
|
||||||
|
- 북마크 권장
|
||||||
|
|
||||||
|
## Features Roadmap
|
||||||
|
|
||||||
|
### Phase 1 (MVP)
|
||||||
|
- [x] 이벤트 검색
|
||||||
|
- [x] 파라미터 조회
|
||||||
|
- [x] 우선순위 필터
|
||||||
|
|
||||||
|
### Phase 2
|
||||||
|
- [ ] DataLayer 코드 스니펫 복사
|
||||||
|
- [ ] 플랫폼별 코드 예시
|
||||||
|
- [ ] 최근 조회 히스토리
|
||||||
|
|
||||||
|
### Phase 3
|
||||||
|
- [ ] 이벤트 추가/수정 폼
|
||||||
|
- [ ] 변경 이력 추적
|
||||||
|
- [ ] Slack 알림 연동
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### Data Update
|
||||||
|
1. Google Sheets 원본 데이터 수정
|
||||||
|
2. Web App 자동 반영 (실시간)
|
||||||
|
|
||||||
|
### Code Update
|
||||||
|
1. Apps Script 수정
|
||||||
|
2. Deploy > Manage deployments
|
||||||
|
3. New version 배포
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
- Google Sheets 자동 버전 히스토리
|
||||||
|
- 주기적 수동 백업 권장
|
||||||
|
|
||||||
|
## Access Control
|
||||||
|
|
||||||
|
| Role | Access Level |
|
||||||
|
|------|--------------|
|
||||||
|
| Admin | 시트 편집 + 스크립트 수정 |
|
||||||
|
| Editor | 시트 편집 |
|
||||||
|
| Viewer | Web App 조회만 |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Issue | Solution |
|
||||||
|
|-------|----------|
|
||||||
|
| 앱 로딩 느림 | 데이터 캐싱 구현 |
|
||||||
|
| 검색 결과 없음 | 검색어 확인, 데이터 존재 확인 |
|
||||||
|
| 권한 오류 | 배포 설정에서 접근 권한 확인 |
|
||||||
|
| 데이터 미반영 | 시트 새로고침, 캐시 클리어 |
|
||||||
|
|
||||||
|
## Integration with Notion
|
||||||
|
|
||||||
|
Web App URL을 Notion에 embed:
|
||||||
|
```
|
||||||
|
/embed [Web App URL]
|
||||||
|
```
|
||||||
|
|
||||||
|
또는 iframe으로 직접 삽입.
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# Platform Event Mapping Reference
|
||||||
|
|
||||||
|
GA4, Google Ads, Meta Pixel, Kakao Pixel 간 이벤트 매핑.
|
||||||
|
|
||||||
|
## E-commerce Events
|
||||||
|
|
||||||
|
| User Action | GA4 | Meta | Kakao | Google Ads |
|
||||||
|
|-------------|-----|------|-------|------------|
|
||||||
|
| 상품 목록 조회 | view_item_list | ViewContent | viewContent | - |
|
||||||
|
| 상품 상세 조회 | view_item | ViewContent | viewContent | - |
|
||||||
|
| 상품 검색 | search | Search | search | - |
|
||||||
|
| 장바구니 담기 | add_to_cart | AddToCart | addToCart | - |
|
||||||
|
| 위시리스트 | add_to_wishlist | AddToWishlist | addToWishlist | - |
|
||||||
|
| 결제 시작 | begin_checkout | InitiateCheckout | beginCheckout | - |
|
||||||
|
| 배송정보 입력 | add_shipping_info | - | - | - |
|
||||||
|
| 결제정보 입력 | add_payment_info | AddPaymentInfo | addPaymentInfo | - |
|
||||||
|
| 구매 완료 | purchase | Purchase | purchase | purchase |
|
||||||
|
|
||||||
|
## Lead Gen Events
|
||||||
|
|
||||||
|
| User Action | GA4 | Meta | Kakao | Google Ads |
|
||||||
|
|-------------|-----|------|-------|------------|
|
||||||
|
| 회원가입 | sign_up | CompleteRegistration | completeRegistration | sign_up |
|
||||||
|
| 로그인 | login | - | login | - |
|
||||||
|
| 리드 생성 | generate_lead | Lead | participation | submit_lead_form |
|
||||||
|
|
||||||
|
## Engagement Events
|
||||||
|
|
||||||
|
| User Action | GA4 | Meta | Kakao | Google Ads |
|
||||||
|
|-------------|-----|------|-------|------------|
|
||||||
|
| 페이지 조회 | page_view | PageView | pageView | page_view |
|
||||||
|
| 검색 | search | Search | search | - |
|
||||||
|
| 공유 | share | - | - | - |
|
||||||
|
|
||||||
|
## Parameter Transformation
|
||||||
|
|
||||||
|
### GA4 → Meta
|
||||||
|
```javascript
|
||||||
|
// GA4 items[] → Meta content_ids[]
|
||||||
|
const contentIds = items.map(item => item.item_id);
|
||||||
|
|
||||||
|
// Meta Purchase
|
||||||
|
fbq('track', 'Purchase', {
|
||||||
|
content_ids: contentIds,
|
||||||
|
content_type: 'product',
|
||||||
|
value: ecommerce.value,
|
||||||
|
currency: 'KRW',
|
||||||
|
num_items: items.length
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### GA4 → Kakao
|
||||||
|
```javascript
|
||||||
|
// GA4 items[] → Kakao products[]
|
||||||
|
const products = items.map(item => ({
|
||||||
|
id: item.item_id,
|
||||||
|
name: item.item_name,
|
||||||
|
price: item.price,
|
||||||
|
quantity: item.quantity
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Kakao purchase
|
||||||
|
kakaoPixel('PIXEL_ID').purchase({
|
||||||
|
total_price: ecommerce.value,
|
||||||
|
currency: 'KRW',
|
||||||
|
products: products
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## GTM Variables
|
||||||
|
|
||||||
|
### CJS - Item IDs for Meta
|
||||||
|
```javascript
|
||||||
|
function() {
|
||||||
|
var items = {{DLV - ecommerce.items}} || [];
|
||||||
|
return items.map(function(item) {
|
||||||
|
return item.item_id;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CJS - Products for Kakao
|
||||||
|
```javascript
|
||||||
|
function() {
|
||||||
|
var items = {{DLV - ecommerce.items}} || [];
|
||||||
|
return items.map(function(item) {
|
||||||
|
return {
|
||||||
|
id: item.item_id,
|
||||||
|
name: item.item_name,
|
||||||
|
price: item.price,
|
||||||
|
quantity: item.quantity
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deduplication
|
||||||
|
|
||||||
|
### Event ID Pattern
|
||||||
|
```javascript
|
||||||
|
var eventId = 'evt_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||||
|
|
||||||
|
// Client-side
|
||||||
|
fbq('track', 'Purchase', {...}, {eventID: eventId});
|
||||||
|
|
||||||
|
// Server-side (CAPI)
|
||||||
|
// 동일한 eventId 사용 → Meta 자동 중복 제거
|
||||||
|
```
|
||||||
|
|
||||||
|
## Platform-specific Notes
|
||||||
|
|
||||||
|
### Meta Pixel
|
||||||
|
- content_type: 'product' (상품) / 'product_group' (변형)
|
||||||
|
- currency: ISO 4217 필수
|
||||||
|
- value: 숫자 (소수점 가능)
|
||||||
|
|
||||||
|
### Kakao Pixel
|
||||||
|
- Server-side 미지원 (Client-only)
|
||||||
|
- products[]: 최대 10개 권장
|
||||||
|
- total_price: 숫자
|
||||||
|
|
||||||
|
### Google Ads
|
||||||
|
- Enhanced Conversions 권장
|
||||||
|
- transaction_id 필수 (중복 제거용)
|
||||||
|
- value: KRW 정수
|
||||||
149
custom-skills/62-gtm-validator/code/references/qa-checklist.md
Normal file
149
custom-skills/62-gtm-validator/code/references/qa-checklist.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# GTM Implementation QA Checklist
|
||||||
|
|
||||||
|
태깅 구현 품질 검증 체크리스트.
|
||||||
|
|
||||||
|
## 1. Pre-Implementation
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- [ ] Tagging Plan 승인 완료
|
||||||
|
- [ ] DataLayer 스키마 개발팀 공유
|
||||||
|
- [ ] 이벤트 택소노미 합의 완료
|
||||||
|
|
||||||
|
### Access
|
||||||
|
- [ ] GTM 컨테이너 접근 권한
|
||||||
|
- [ ] GA4 속성 접근 권한
|
||||||
|
- [ ] 광고 플랫폼 접근 권한
|
||||||
|
|
||||||
|
### Tools
|
||||||
|
- [ ] GTM Preview 모드 정상
|
||||||
|
- [ ] GA4 DebugView 활성화
|
||||||
|
- [ ] Chrome DevTools 준비
|
||||||
|
|
||||||
|
## 2. DataLayer Validation
|
||||||
|
|
||||||
|
### Base Setup
|
||||||
|
- [ ] GTM 스니펫 이전 dataLayer 초기화
|
||||||
|
- [ ] 페이지 로드 시 기본 데이터 push
|
||||||
|
- [ ] pageType, userId 등 기본 변수 존재
|
||||||
|
|
||||||
|
### Data Types
|
||||||
|
- [ ] 숫자 값이 Number 타입
|
||||||
|
- [ ] 금액에 통화 코드 포함 (KRW)
|
||||||
|
- [ ] transaction_id 유니크
|
||||||
|
|
||||||
|
### E-commerce
|
||||||
|
- [ ] ecommerce: null 선행 push
|
||||||
|
- [ ] items[] 배열 구조 정확
|
||||||
|
- [ ] 필수 파라미터 완비
|
||||||
|
|
||||||
|
### Console Check
|
||||||
|
```javascript
|
||||||
|
console.log(window.dataLayer);
|
||||||
|
dataLayer.filter(e => e.event === 'purchase');
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Tag Firing
|
||||||
|
|
||||||
|
### GA4
|
||||||
|
| Tag | Trigger | Status |
|
||||||
|
|-----|---------|--------|
|
||||||
|
| Config | All Pages | [ ] |
|
||||||
|
| view_item | Product page | [ ] |
|
||||||
|
| add_to_cart | Add button | [ ] |
|
||||||
|
| begin_checkout | Checkout start | [ ] |
|
||||||
|
| purchase | Thank you | [ ] |
|
||||||
|
|
||||||
|
### Google Ads
|
||||||
|
| Conversion | Trigger | Status |
|
||||||
|
|------------|---------|--------|
|
||||||
|
| Purchase | DL - purchase | [ ] |
|
||||||
|
| Lead | DL - generate_lead | [ ] |
|
||||||
|
|
||||||
|
### Meta Pixel
|
||||||
|
| Event | Trigger | Status |
|
||||||
|
|-------|---------|--------|
|
||||||
|
| PageView | All Pages | [ ] |
|
||||||
|
| ViewContent | Product | [ ] |
|
||||||
|
| AddToCart | Add | [ ] |
|
||||||
|
| Purchase | Thank you | [ ] |
|
||||||
|
|
||||||
|
### Kakao Pixel
|
||||||
|
| Event | Trigger | Status |
|
||||||
|
|-------|---------|--------|
|
||||||
|
| pageView | All Pages | [ ] |
|
||||||
|
| purchase | Thank you | [ ] |
|
||||||
|
|
||||||
|
## 4. Network Validation
|
||||||
|
|
||||||
|
### Endpoints
|
||||||
|
```
|
||||||
|
GA4: google-analytics.com/g/collect
|
||||||
|
GAds: googleads.g.doubleclick.net/pagead/conversion/
|
||||||
|
Meta: facebook.com/tr/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Points
|
||||||
|
- [ ] 요청 전송됨
|
||||||
|
- [ ] 파라미터 정확
|
||||||
|
- [ ] 응답 성공 (200/204)
|
||||||
|
|
||||||
|
## 5. Cross-browser Testing
|
||||||
|
|
||||||
|
### Desktop
|
||||||
|
- [ ] Chrome
|
||||||
|
- [ ] Safari
|
||||||
|
- [ ] Firefox
|
||||||
|
- [ ] Edge
|
||||||
|
|
||||||
|
### Mobile
|
||||||
|
- [ ] iOS Safari
|
||||||
|
- [ ] iOS Chrome
|
||||||
|
- [ ] Android Chrome
|
||||||
|
|
||||||
|
## 6. Data Validation (Platform)
|
||||||
|
|
||||||
|
### GA4
|
||||||
|
- [ ] Realtime 보고서 이벤트 표시
|
||||||
|
- [ ] 파라미터 정확히 수집
|
||||||
|
- [ ] E-commerce 보고서 정확
|
||||||
|
|
||||||
|
### Google Ads
|
||||||
|
- [ ] 전환 액션 데이터 유입
|
||||||
|
- [ ] 전환 값 정확
|
||||||
|
|
||||||
|
### Meta
|
||||||
|
- [ ] Events Manager 수신 확인
|
||||||
|
- [ ] Event match quality 확인
|
||||||
|
|
||||||
|
## 7. Edge Cases
|
||||||
|
|
||||||
|
- [ ] 빈 장바구니 결제 시작 처리
|
||||||
|
- [ ] 0개 구매 처리
|
||||||
|
- [ ] 쿠폰 없는 구매
|
||||||
|
- [ ] 게스트 체크아웃
|
||||||
|
- [ ] 새로고침 시 중복 방지
|
||||||
|
- [ ] 뒤로가기 시 처리
|
||||||
|
|
||||||
|
## 8. Performance & Privacy
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- [ ] 태그 로딩이 렌더링 차단 안 함
|
||||||
|
- [ ] 비동기 로딩
|
||||||
|
|
||||||
|
### Privacy
|
||||||
|
- [ ] 동의 없이 태그 미발화 (CMP 시)
|
||||||
|
- [ ] PII 직접 수집 없음
|
||||||
|
|
||||||
|
## Sign-off
|
||||||
|
|
||||||
|
| Role | Name | Date | ✓ |
|
||||||
|
|------|------|------|---|
|
||||||
|
| QA Lead | | | |
|
||||||
|
| Analytics Lead | | | |
|
||||||
|
| Dev Lead | | | |
|
||||||
|
|
||||||
|
## Issues Log
|
||||||
|
|
||||||
|
| # | Issue | Severity | Status |
|
||||||
|
|---|-------|----------|--------|
|
||||||
|
| 1 | | High/Med/Low | Open/Resolved |
|
||||||
45
custom-skills/92-mac-optimizer/.claude-plugin/plugin.json
Normal file
45
custom-skills/92-mac-optimizer/.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "mac-optimizer",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Modular macOS system health toolkit with read-only audits, cleanup, and security checks",
|
||||||
|
"author": {
|
||||||
|
"name": "OurDigital"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"macos",
|
||||||
|
"system",
|
||||||
|
"optimization",
|
||||||
|
"cleanup",
|
||||||
|
"security",
|
||||||
|
"homebrew",
|
||||||
|
"packages",
|
||||||
|
"disk-space"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name": "mac-doctor",
|
||||||
|
"description": "Full macOS system health check — runs all 5 audit modules and presents unified findings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mac-packages",
|
||||||
|
"description": "Audit package managers (Homebrew, npm, pip, pyenv) for outdated packages and issues"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mac-environment",
|
||||||
|
"description": "Audit shell environment — PATH, symlinks, shell configs, and startup time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mac-security",
|
||||||
|
"description": "Security posture assessment — SIP, Gatekeeper, Firewall, FileVault, SSH, ports"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mac-cleanup",
|
||||||
|
"description": "Scan and clean caches, logs, and clutter — shows sizes first, cleans only with consent"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mac-resources",
|
||||||
|
"description": "Monitor CPU, memory, disk, battery, and identify resource-hungry processes"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
53
custom-skills/92-mac-optimizer/commands/mac-cleanup.md
Normal file
53
custom-skills/92-mac-optimizer/commands/mac-cleanup.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
allowed-tools: Bash(bash *audit_cleanup.sh*), Bash(bash *cleanup_execute.sh*), Bash(du *), Bash(brew *), Bash(npm *), Bash(pip *), Bash(docker *), Bash(rm *)
|
||||||
|
description: Scan and clean caches, logs, and clutter — shows sizes first, cleans only with consent
|
||||||
|
---
|
||||||
|
|
||||||
|
## Your task
|
||||||
|
|
||||||
|
Scan for reclaimable disk space and offer cleanup.
|
||||||
|
|
||||||
|
### Step 1 — Run the audit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash $CLAUDE_PLUGIN_ROOT/skills/mac-optimizer/scripts/audit_cleanup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2 — Report
|
||||||
|
|
||||||
|
Parse JSON output and present as a table sorted by size (largest first):
|
||||||
|
|
||||||
|
| Category | Size | Risk | Target |
|
||||||
|
|---|---|---|---|
|
||||||
|
|
||||||
|
Show the total reclaimable space.
|
||||||
|
|
||||||
|
### Step 3 — Ask for consent
|
||||||
|
|
||||||
|
Ask which categories to clean. Group by risk level (Safe / Moderate / Risky).
|
||||||
|
|
||||||
|
### Step 4 — Dry run first
|
||||||
|
|
||||||
|
For approved targets, always run dry-run first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash $CLAUDE_PLUGIN_ROOT/skills/mac-optimizer/scripts/cleanup_execute.sh --dry-run <targets>
|
||||||
|
```
|
||||||
|
|
||||||
|
Show the output and ask for final confirmation.
|
||||||
|
|
||||||
|
### Step 5 — Execute
|
||||||
|
|
||||||
|
Only after explicit confirmation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash $CLAUDE_PLUGIN_ROOT/skills/mac-optimizer/scripts/cleanup_execute.sh --execute <targets>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Safety rules
|
||||||
|
|
||||||
|
- Never execute cleanup without explicit user approval
|
||||||
|
- Always show sizes before deleting anything
|
||||||
|
- Always run --dry-run before --execute
|
||||||
|
- Docker cleanup uses `docker system prune`, never direct file deletion
|
||||||
|
- No sudo by default — if an action needs sudo, state why and ask first
|
||||||
60
custom-skills/92-mac-optimizer/commands/mac-doctor.md
Normal file
60
custom-skills/92-mac-optimizer/commands/mac-doctor.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
allowed-tools: Bash(bash *audit_*.sh*), Bash(bash *cleanup_execute.sh*), Bash(du *), Bash(df *), Bash(brew *), Bash(npm *), Bash(pip *), Bash(docker *), Bash(cp *)
|
||||||
|
description: Full macOS system health check — runs all 5 audit modules and presents unified findings
|
||||||
|
---
|
||||||
|
|
||||||
|
## Your task
|
||||||
|
|
||||||
|
Run a full macOS system health check using all 5 audit modules. Follow this workflow exactly:
|
||||||
|
|
||||||
|
### Step 1 — Run all audits
|
||||||
|
|
||||||
|
Run these scripts sequentially, collecting all JSON output:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash $CLAUDE_PLUGIN_ROOT/skills/mac-optimizer/scripts/audit_packages.sh
|
||||||
|
bash $CLAUDE_PLUGIN_ROOT/skills/mac-optimizer/scripts/audit_environment.sh
|
||||||
|
bash $CLAUDE_PLUGIN_ROOT/skills/mac-optimizer/scripts/audit_security.sh
|
||||||
|
bash $CLAUDE_PLUGIN_ROOT/skills/mac-optimizer/scripts/audit_cleanup.sh
|
||||||
|
bash $CLAUDE_PLUGIN_ROOT/skills/mac-optimizer/scripts/audit_resources.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
If a script exits non-zero, report the error and continue with other modules.
|
||||||
|
|
||||||
|
### Step 2 — Parse and report
|
||||||
|
|
||||||
|
Parse all JSON lines. Each has: `{"module":"...","severity":"...","finding":"...","action":"...","details":"..."}`
|
||||||
|
|
||||||
|
Present findings grouped by severity (critical first, then warning, then info) as markdown tables:
|
||||||
|
|
||||||
|
```
|
||||||
|
### Critical
|
||||||
|
| Module | Finding | Recommended Action |
|
||||||
|
|---|---|---|
|
||||||
|
|
||||||
|
### Warning
|
||||||
|
| Module | Finding | Recommended Action |
|
||||||
|
|---|---|---|
|
||||||
|
|
||||||
|
### Info
|
||||||
|
| Module | Finding | Recommended Action |
|
||||||
|
|---|---|---|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3 — Ask for consent
|
||||||
|
|
||||||
|
STOP and ask: "Which actions would you like me to perform? You can approve by category (e.g., 'update packages and clean caches') or review each item."
|
||||||
|
|
||||||
|
### Step 4 — Execute approved actions only
|
||||||
|
|
||||||
|
For cleanup actions, always run `--dry-run` first and show what will happen before running `--execute`.
|
||||||
|
|
||||||
|
### Safety rules
|
||||||
|
|
||||||
|
- Never execute cleanup without explicit user approval
|
||||||
|
- Always show sizes before deleting anything
|
||||||
|
- Security module is read-only — present findings and remediation guidance only
|
||||||
|
- Back up shell configs before modifying: `cp ~/.zshrc ~/.config/mac-optimizer-backups/.zshrc.$(date +%s)`
|
||||||
|
- Process deny-list: never suggest killing kernel_task, launchd, WindowServer, loginwindow, mds, mds_stores, opendirectoryd, coreaudiod, SystemUIServer, Finder, Dock
|
||||||
|
- No sudo by default — if an action needs sudo, state why and ask first
|
||||||
|
- Docker cleanup uses `docker system prune`, never direct file deletion
|
||||||
30
custom-skills/92-mac-optimizer/commands/mac-environment.md
Normal file
30
custom-skills/92-mac-optimizer/commands/mac-environment.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
allowed-tools: Bash(bash *audit_environment.sh*), Bash(echo *), Bash(cp *)
|
||||||
|
description: Audit shell environment — PATH, symlinks, shell configs, and startup time
|
||||||
|
---
|
||||||
|
|
||||||
|
## Your task
|
||||||
|
|
||||||
|
Audit the shell environment on this Mac.
|
||||||
|
|
||||||
|
### Step 1 — Run the audit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash $CLAUDE_PLUGIN_ROOT/skills/mac-optimizer/scripts/audit_environment.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2 — Report
|
||||||
|
|
||||||
|
Parse JSON output and present as a severity-ranked markdown table:
|
||||||
|
|
||||||
|
| Severity | Finding | Recommended Action | Details |
|
||||||
|
|---|---|---|---|
|
||||||
|
|
||||||
|
### Step 3 — Ask for consent
|
||||||
|
|
||||||
|
If there are fixable issues (duplicate PATH entries, broken symlinks, non-existent directories), list them and ask which to fix.
|
||||||
|
|
||||||
|
### Safety rules
|
||||||
|
|
||||||
|
- Back up shell configs before modifying: `cp ~/.zshrc ~/.config/mac-optimizer-backups/.zshrc.$(date +%s)`
|
||||||
|
- Present PATH consolidation suggestions but don't auto-modify
|
||||||
32
custom-skills/92-mac-optimizer/commands/mac-packages.md
Normal file
32
custom-skills/92-mac-optimizer/commands/mac-packages.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
allowed-tools: Bash(bash *audit_packages.sh*), Bash(brew *), Bash(npm *), Bash(pip *), Bash(nvm *), Bash(pyenv *)
|
||||||
|
description: Audit package managers (Homebrew, npm, pip, pyenv) for outdated packages and issues
|
||||||
|
---
|
||||||
|
|
||||||
|
## Your task
|
||||||
|
|
||||||
|
Audit all package managers on this Mac.
|
||||||
|
|
||||||
|
### Step 1 — Run the audit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash $CLAUDE_PLUGIN_ROOT/skills/mac-optimizer/scripts/audit_packages.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2 — Report
|
||||||
|
|
||||||
|
Parse JSON output and present as a severity-ranked markdown table:
|
||||||
|
|
||||||
|
| Severity | Finding | Recommended Action | Details |
|
||||||
|
|---|---|---|---|
|
||||||
|
|
||||||
|
### Step 3 — Ask for consent
|
||||||
|
|
||||||
|
List available update actions grouped by risk. Ask which actions to perform.
|
||||||
|
|
||||||
|
### Safety notes
|
||||||
|
|
||||||
|
- `brew upgrade` is generally safe but can break projects pinned to specific versions
|
||||||
|
- Global npm packages are version-independent from project dependencies
|
||||||
|
- pip upgrades can break dependency chains — suggest `--dry-run` first
|
||||||
|
- Only audit pyenv-managed environments (not system Python)
|
||||||
29
custom-skills/92-mac-optimizer/commands/mac-resources.md
Normal file
29
custom-skills/92-mac-optimizer/commands/mac-resources.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
allowed-tools: Bash(bash *audit_resources.sh*), Bash(ps *), Bash(kill *), Bash(df *)
|
||||||
|
description: Monitor CPU, memory, disk, battery, and identify resource-hungry processes
|
||||||
|
---
|
||||||
|
|
||||||
|
## Your task
|
||||||
|
|
||||||
|
Check system resource usage on this Mac.
|
||||||
|
|
||||||
|
### Step 1 — Run the audit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash $CLAUDE_PLUGIN_ROOT/skills/mac-optimizer/scripts/audit_resources.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2 — Report
|
||||||
|
|
||||||
|
Parse JSON output and present as a severity-ranked markdown table:
|
||||||
|
|
||||||
|
| Severity | Finding | Recommended Action | Details |
|
||||||
|
|---|---|---|---|
|
||||||
|
|
||||||
|
### Step 3 — Offer actions
|
||||||
|
|
||||||
|
If high-resource processes are found, offer to help. But respect the deny-list:
|
||||||
|
|
||||||
|
**Never suggest killing**: kernel_task, launchd, WindowServer, loginwindow, mds, mds_stores, opendirectoryd, coreaudiod, SystemUIServer, Finder, Dock
|
||||||
|
|
||||||
|
For user processes consuming excessive resources, ask before terminating.
|
||||||
25
custom-skills/92-mac-optimizer/commands/mac-security.md
Normal file
25
custom-skills/92-mac-optimizer/commands/mac-security.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
allowed-tools: Bash(bash *audit_security.sh*)
|
||||||
|
description: Security posture assessment — SIP, Gatekeeper, Firewall, FileVault, SSH, ports
|
||||||
|
---
|
||||||
|
|
||||||
|
## Your task
|
||||||
|
|
||||||
|
Run a read-only security posture assessment.
|
||||||
|
|
||||||
|
### Step 1 — Run the audit
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash $CLAUDE_PLUGIN_ROOT/skills/mac-optimizer/scripts/audit_security.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2 — Report
|
||||||
|
|
||||||
|
Parse JSON output and present as a severity-ranked markdown table:
|
||||||
|
|
||||||
|
| Severity | Finding | Recommended Action | Details |
|
||||||
|
|---|---|---|---|
|
||||||
|
|
||||||
|
### Step 3 — Guidance only
|
||||||
|
|
||||||
|
The security module is **read-only**. Present findings and remediation guidance. Do NOT attempt to change security settings. Explain how the user can fix issues themselves via System Settings or Terminal commands.
|
||||||
44
custom-skills/92-mac-optimizer/skills/mac-optimizer/SKILL.md
Normal file
44
custom-skills/92-mac-optimizer/skills/mac-optimizer/SKILL.md
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
name: mac-optimizer
|
||||||
|
description: macOS system health toolkit — use when user mentions system optimization, cleanup, packages, security audit, disk space, performance, brew updates, cache clearing, or system health on Mac
|
||||||
|
version: 1.0.0
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mac Optimizer
|
||||||
|
|
||||||
|
Modular macOS system health toolkit. Runs read-only audits first, then recommends actions with user consent.
|
||||||
|
|
||||||
|
## Module Routing
|
||||||
|
|
||||||
|
| Keywords | Command |
|
||||||
|
|---|---|
|
||||||
|
| brew, homebrew, npm, nvm, pip, pyenv, packages, update, outdated | /mac-packages |
|
||||||
|
| path, shell, zshrc, environment, env, config, symlink | /mac-environment |
|
||||||
|
| security, firewall, sip, gatekeeper, filevault, ports, ssh | /mac-security |
|
||||||
|
| cache, cleanup, clean, logs, clutter, disk space, free space, trash | /mac-cleanup |
|
||||||
|
| cpu, memory, ram, disk, battery, processes, resources, slow | /mac-resources |
|
||||||
|
| doctor, audit, health, full check, everything, system check | /mac-doctor (all) |
|
||||||
|
|
||||||
|
Default to **/mac-doctor** when the request is ambiguous.
|
||||||
|
|
||||||
|
When the user's request matches a specific module, suggest the appropriate slash command. When the request is broad (e.g., "check my system", "run a health check"), use /mac-doctor.
|
||||||
|
|
||||||
|
## Execution Model
|
||||||
|
|
||||||
|
Every module follows this flow:
|
||||||
|
|
||||||
|
1. **Audit** — run the module's script (read-only)
|
||||||
|
2. **Report** — parse JSON output, present findings as a severity-ranked table
|
||||||
|
3. **Recommend** — list available actions grouped by risk
|
||||||
|
4. **Consent** — ask user which actions to approve
|
||||||
|
5. **Act** — execute only approved actions
|
||||||
|
|
||||||
|
## Safety Rules
|
||||||
|
|
||||||
|
- **Never execute cleanup without explicit user approval**
|
||||||
|
- **Always show sizes before deleting anything**
|
||||||
|
- **Security module is read-only** — present findings and remediation guidance only
|
||||||
|
- **Back up shell configs** before modifying
|
||||||
|
- **Process deny-list**: never suggest killing kernel_task, launchd, WindowServer, loginwindow, mds, mds_stores, opendirectoryd, coreaudiod, SystemUIServer, Finder, Dock
|
||||||
|
- **No sudo by default** — if an action needs sudo, state why and ask first
|
||||||
|
- Docker cleanup uses `docker system prune`, never direct file deletion
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Cleanup Targets Reference
|
||||||
|
|
||||||
|
## Risk Ratings
|
||||||
|
|
||||||
|
| Risk | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| Safe | Can be deleted without consequence; regenerated automatically |
|
||||||
|
| Moderate | Review before deleting; may lose useful data |
|
||||||
|
| Risky | May break applications; requires careful inspection |
|
||||||
|
| Info only | Report size but do not offer deletion |
|
||||||
|
|
||||||
|
## Target Details
|
||||||
|
|
||||||
|
### User Logs (Safe)
|
||||||
|
- **Path**: `~/Library/Logs/`
|
||||||
|
- **Cleanup target**: `user-logs`
|
||||||
|
- **Notes**: Application crash reports and debug logs. Regenerated as needed.
|
||||||
|
|
||||||
|
### System Logs (Moderate, requires sudo)
|
||||||
|
- **Path**: `/var/log/`
|
||||||
|
- **Cleanup target**: not available via cleanup_execute.sh (requires sudo)
|
||||||
|
- **Notes**: System-level logs. Only clean old/rotated files. Active logs should not be deleted.
|
||||||
|
|
||||||
|
### User Caches (Safe, per-app)
|
||||||
|
- **Path**: `~/Library/Caches/`
|
||||||
|
- **Cleanup target**: `user-caches`
|
||||||
|
- **Notes**: App caches regenerated on next use. Some apps may need to re-download data. Browser caches can be large.
|
||||||
|
|
||||||
|
### Homebrew Cache (Safe)
|
||||||
|
- **Path**: `$(brew --cache)` (typically `~/Library/Caches/Homebrew/`)
|
||||||
|
- **Cleanup target**: `brew-cache`
|
||||||
|
- **Notes**: Downloaded bottles and source archives. `brew cleanup` handles this properly.
|
||||||
|
|
||||||
|
### npm Cache (Safe)
|
||||||
|
- **Path**: `~/.npm/_cacache/`
|
||||||
|
- **Cleanup target**: `npm-cache`
|
||||||
|
- **Notes**: Package download cache. `npm cache clean --force` is the proper way to clear.
|
||||||
|
|
||||||
|
### pip Cache (Safe)
|
||||||
|
- **Path**: `~/Library/Caches/pip/`
|
||||||
|
- **Cleanup target**: `pip-cache`
|
||||||
|
- **Notes**: Downloaded wheel/sdist cache. `pip cache purge` is the proper way to clear.
|
||||||
|
|
||||||
|
### Xcode DerivedData (Safe)
|
||||||
|
- **Path**: `~/Library/Developer/Xcode/DerivedData/`
|
||||||
|
- **Cleanup target**: `xcode-derived`
|
||||||
|
- **Notes**: Build artifacts and indexes. Rebuilt on next Xcode build. Can grow very large.
|
||||||
|
|
||||||
|
### Xcode Archives (Moderate)
|
||||||
|
- **Path**: `~/Library/Developer/Xcode/Archives/`
|
||||||
|
- **Cleanup target**: `xcode-archives`
|
||||||
|
- **Notes**: App Store submission archives. May be needed for symbolication of crash reports. Review before deleting.
|
||||||
|
|
||||||
|
### iOS DeviceSupport (Safe)
|
||||||
|
- **Path**: `~/Library/Developer/Xcode/iOS DeviceSupport/`
|
||||||
|
- **Cleanup target**: `ios-support`
|
||||||
|
- **Notes**: Debug symbols for connected iOS devices. Re-downloaded when device connects again.
|
||||||
|
|
||||||
|
### Docker (Moderate)
|
||||||
|
- **Cleanup target**: `docker`
|
||||||
|
- **Notes**: Uses `docker system prune -f`. Removes stopped containers, unused networks, dangling images. Does NOT remove named volumes. Never delete Docker's filesystem directly.
|
||||||
|
|
||||||
|
### Trash (Safe)
|
||||||
|
- **Path**: `~/.Trash/`
|
||||||
|
- **Cleanup target**: `trash`
|
||||||
|
- **Notes**: Items in Trash. User has already "deleted" these.
|
||||||
|
|
||||||
|
### Old Downloads (Info only)
|
||||||
|
- **Path**: `~/Downloads/` (files >90 days old)
|
||||||
|
- **Notes**: Report only. User should review manually. Never auto-delete downloads.
|
||||||
|
|
||||||
|
### Application Support Remnants (Risky)
|
||||||
|
- **Path**: `~/Library/Application Support/` orphaned directories
|
||||||
|
- **Notes**: Report only. Directories from uninstalled apps. Difficult to determine automatically which are truly orphaned. Manual review required.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Package Manager Reference
|
||||||
|
|
||||||
|
## Homebrew
|
||||||
|
|
||||||
|
### Update workflow
|
||||||
|
```bash
|
||||||
|
brew update # Update Homebrew itself and formulae list
|
||||||
|
brew outdated # List outdated formulae
|
||||||
|
brew outdated --cask # List outdated casks
|
||||||
|
brew upgrade # Upgrade all outdated formulae
|
||||||
|
brew upgrade <name> # Upgrade specific formula
|
||||||
|
brew upgrade --cask # Upgrade all casks
|
||||||
|
brew cleanup --prune=0 # Remove all cached downloads
|
||||||
|
brew autoremove # Remove orphaned dependencies
|
||||||
|
brew doctor # Diagnose issues
|
||||||
|
```
|
||||||
|
|
||||||
|
### Safety notes
|
||||||
|
- `brew upgrade` is generally safe but can break projects pinned to specific versions
|
||||||
|
- Always check `brew doctor` warnings — some are cosmetic, some indicate real issues
|
||||||
|
- `brew autoremove` only removes packages not depended on by anything
|
||||||
|
|
||||||
|
## npm / nvm
|
||||||
|
|
||||||
|
### Update workflow
|
||||||
|
```bash
|
||||||
|
nvm ls # List installed Node versions
|
||||||
|
nvm install --lts # Install latest LTS
|
||||||
|
npm outdated -g # Check global packages
|
||||||
|
npm update -g # Update all global packages
|
||||||
|
npm cache clean --force # Clear npm cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### Safety notes
|
||||||
|
- Global npm packages are version-independent from project dependencies
|
||||||
|
- Updating global packages won't affect project-level node_modules
|
||||||
|
|
||||||
|
## Python / pyenv
|
||||||
|
|
||||||
|
### Update workflow
|
||||||
|
```bash
|
||||||
|
pyenv versions # List installed versions
|
||||||
|
pyenv install --list # Available versions
|
||||||
|
pip list --outdated # Outdated packages in current env
|
||||||
|
pip install --upgrade <pkg> # Upgrade specific package
|
||||||
|
pip cache purge # Clear pip cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### Safety notes
|
||||||
|
- Only audit pyenv-managed environments (not system Python)
|
||||||
|
- pip upgrades can break dependency chains — suggest `--dry-run` first
|
||||||
|
- Virtual environments in project directories are not scanned (out of scope v1)
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Security Checks Reference
|
||||||
|
|
||||||
|
## SIP (System Integrity Protection)
|
||||||
|
- **Check**: `csrutil status`
|
||||||
|
- **Expected**: "System Integrity Protection status: enabled."
|
||||||
|
- **Remediation**: Reboot to Recovery Mode (Cmd+R), open Terminal, run `csrutil enable`
|
||||||
|
- **Severity**: Critical if disabled
|
||||||
|
|
||||||
|
## Gatekeeper
|
||||||
|
- **Check**: `spctl --status`
|
||||||
|
- **Expected**: "assessments enabled"
|
||||||
|
- **Remediation**: `sudo spctl --master-enable`
|
||||||
|
- **Severity**: Critical if disabled
|
||||||
|
|
||||||
|
## Firewall
|
||||||
|
- **Check**: `/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate`
|
||||||
|
- **Fallback**: `defaults read /Library/Preferences/com.apple.alf globalstate` (0=off, 1=on, 2=essential)
|
||||||
|
- **Remediation**: System Settings > Network > Firewall > Turn On
|
||||||
|
- **Severity**: Critical if disabled
|
||||||
|
|
||||||
|
## FileVault
|
||||||
|
- **Check**: `fdesetup status`
|
||||||
|
- **Expected**: "FileVault is On."
|
||||||
|
- **Remediation**: System Settings > Privacy & Security > FileVault > Turn On
|
||||||
|
- **Severity**: Critical if disabled (disk not encrypted)
|
||||||
|
|
||||||
|
## Open Ports
|
||||||
|
- **Check**: `lsof -iTCP -sTCP:LISTEN -nP`
|
||||||
|
- **Review**: unexpected services listening on all interfaces (0.0.0.0 or *)
|
||||||
|
- **Severity**: Info (review needed, not automatically bad)
|
||||||
|
|
||||||
|
## SSH Configuration
|
||||||
|
- **Check**: `/etc/ssh/sshd_config`
|
||||||
|
- **Flags**: `PasswordAuthentication yes`, `PermitRootLogin yes`
|
||||||
|
- **Remediation**: Set to `no` and restart sshd
|
||||||
|
- **Severity**: Warning
|
||||||
|
|
||||||
|
## Remote Services
|
||||||
|
- **Remote Login**: `systemsetup -getremotelogin`
|
||||||
|
- **Remote Management**: `defaults read /Library/Preferences/com.apple.RemoteManagement ARD_AllLocalUsers`
|
||||||
|
- **Remediation**: Disable in System Settings > General > Sharing
|
||||||
|
- **Severity**: Info/Warning depending on context
|
||||||
|
|
||||||
|
## Software Updates
|
||||||
|
- **Check**: `softwareupdate -l` (can be slow, 30s timeout)
|
||||||
|
- **Remediation**: System Settings > General > Software Update
|
||||||
|
- **Severity**: Warning if updates available
|
||||||
|
|
||||||
|
## Screen Lock
|
||||||
|
- **Check**: `defaults -currentHost read com.apple.screensaver idleTime`
|
||||||
|
- **Check**: `defaults read com.apple.screensaver askForPassword`
|
||||||
|
- **Remediation**: System Settings > Lock Screen
|
||||||
|
- **Severity**: Warning if password not required
|
||||||
160
custom-skills/92-mac-optimizer/skills/mac-optimizer/scripts/audit_cleanup.sh
Executable file
160
custom-skills/92-mac-optimizer/skills/mac-optimizer/scripts/audit_cleanup.sh
Executable file
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# audit_cleanup.sh — Scan caches, logs, and clutter on macOS, report sizes
|
||||||
|
# Output: JSON lines to stdout, one per finding
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' '; }
|
||||||
|
|
||||||
|
json_finding() {
|
||||||
|
local severity="$1" finding action details
|
||||||
|
finding=$(json_escape "$2")
|
||||||
|
action=$(json_escape "$3")
|
||||||
|
details=$(json_escape "$4")
|
||||||
|
printf '{"module":"cleanup","severity":"%s","finding":"%s","action":"%s","details":"%s"}\n' \
|
||||||
|
"$severity" "$finding" "$action" "$details"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get directory size in human-readable and bytes
|
||||||
|
dir_size() {
|
||||||
|
local path="$1"
|
||||||
|
if [ -d "$path" ]; then
|
||||||
|
local bytes=$(du -sk "$path" 2>/dev/null | awk '{print $1}')
|
||||||
|
local human=$(du -sh "$path" 2>/dev/null | awk '{print $1}')
|
||||||
|
echo "${bytes:-0}|${human:-0B}"
|
||||||
|
else
|
||||||
|
echo "0|0B"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
classify_size() {
|
||||||
|
local kb="$1"
|
||||||
|
if [ "$kb" -gt 1048576 ] 2>/dev/null; then # > 1GB
|
||||||
|
echo "warning"
|
||||||
|
else
|
||||||
|
echo "info"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
total_reclaimable_kb=0
|
||||||
|
|
||||||
|
# --- User logs ---
|
||||||
|
result=$(dir_size "$HOME/Library/Logs")
|
||||||
|
IFS='|' read -r kb human <<< "$result"
|
||||||
|
total_reclaimable_kb=$((total_reclaimable_kb + kb))
|
||||||
|
severity=$(classify_size "$kb")
|
||||||
|
json_finding "$severity" "User logs: ${human}" "clean" "~/Library/Logs/ [safe, no sudo]"
|
||||||
|
|
||||||
|
# --- System logs ---
|
||||||
|
result=$(dir_size "/var/log" 2>/dev/null || echo "0|0B")
|
||||||
|
IFS='|' read -r kb human <<< "$result"
|
||||||
|
if [ "$kb" -gt 0 ] 2>/dev/null; then
|
||||||
|
severity=$(classify_size "$kb")
|
||||||
|
json_finding "$severity" "System logs: ${human}" "clean" "/var/log/ [moderate, requires sudo]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- User caches ---
|
||||||
|
result=$(dir_size "$HOME/Library/Caches")
|
||||||
|
IFS='|' read -r kb human <<< "$result"
|
||||||
|
total_reclaimable_kb=$((total_reclaimable_kb + kb))
|
||||||
|
severity=$(classify_size "$kb")
|
||||||
|
json_finding "$severity" "User caches: ${human}" "clean" "~/Library/Caches/ [safe, per-app]"
|
||||||
|
|
||||||
|
# --- Homebrew cache ---
|
||||||
|
if command -v brew &>/dev/null; then
|
||||||
|
brew_cache=$(brew --cache 2>/dev/null || echo "")
|
||||||
|
if [ -n "$brew_cache" ] && [ -d "$brew_cache" ]; then
|
||||||
|
result=$(dir_size "$brew_cache")
|
||||||
|
IFS='|' read -r kb human <<< "$result"
|
||||||
|
total_reclaimable_kb=$((total_reclaimable_kb + kb))
|
||||||
|
severity=$(classify_size "$kb")
|
||||||
|
json_finding "$severity" "Homebrew cache: ${human}" "brew cleanup" "$brew_cache [safe]"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- npm cache ---
|
||||||
|
npm_cache="$HOME/.npm/_cacache"
|
||||||
|
if [ -d "$npm_cache" ]; then
|
||||||
|
result=$(dir_size "$npm_cache")
|
||||||
|
IFS='|' read -r kb human <<< "$result"
|
||||||
|
total_reclaimable_kb=$((total_reclaimable_kb + kb))
|
||||||
|
severity=$(classify_size "$kb")
|
||||||
|
json_finding "$severity" "npm cache: ${human}" "npm cache clean --force" "$npm_cache [safe]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- pip cache ---
|
||||||
|
pip_cache="$HOME/Library/Caches/pip"
|
||||||
|
if [ -d "$pip_cache" ]; then
|
||||||
|
result=$(dir_size "$pip_cache")
|
||||||
|
IFS='|' read -r kb human <<< "$result"
|
||||||
|
total_reclaimable_kb=$((total_reclaimable_kb + kb))
|
||||||
|
severity=$(classify_size "$kb")
|
||||||
|
json_finding "$severity" "pip cache: ${human}" "pip cache purge" "$pip_cache [safe]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Xcode derived data ---
|
||||||
|
xcode_derived="$HOME/Library/Developer/Xcode/DerivedData"
|
||||||
|
if [ -d "$xcode_derived" ]; then
|
||||||
|
result=$(dir_size "$xcode_derived")
|
||||||
|
IFS='|' read -r kb human <<< "$result"
|
||||||
|
total_reclaimable_kb=$((total_reclaimable_kb + kb))
|
||||||
|
severity=$(classify_size "$kb")
|
||||||
|
json_finding "$severity" "Xcode DerivedData: ${human}" "clean" "$xcode_derived [safe]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Xcode archives ---
|
||||||
|
xcode_archives="$HOME/Library/Developer/Xcode/Archives"
|
||||||
|
if [ -d "$xcode_archives" ]; then
|
||||||
|
result=$(dir_size "$xcode_archives")
|
||||||
|
IFS='|' read -r kb human <<< "$result"
|
||||||
|
total_reclaimable_kb=$((total_reclaimable_kb + kb))
|
||||||
|
severity=$(classify_size "$kb")
|
||||||
|
json_finding "$severity" "Xcode Archives: ${human}" "clean" "$xcode_archives [moderate - check before deleting]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- iOS Device Support ---
|
||||||
|
ios_support="$HOME/Library/Developer/Xcode/iOS DeviceSupport"
|
||||||
|
if [ -d "$ios_support" ]; then
|
||||||
|
result=$(dir_size "$ios_support")
|
||||||
|
IFS='|' read -r kb human <<< "$result"
|
||||||
|
total_reclaimable_kb=$((total_reclaimable_kb + kb))
|
||||||
|
severity=$(classify_size "$kb")
|
||||||
|
json_finding "$severity" "iOS DeviceSupport: ${human}" "clean" "$ios_support [safe]"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Docker ---
|
||||||
|
if command -v docker &>/dev/null; then
|
||||||
|
docker_df=$(docker system df 2>/dev/null || echo "")
|
||||||
|
if [ -n "$docker_df" ]; then
|
||||||
|
reclaimable=$(echo "$docker_df" | grep -i "reclaimable" | head -1 || echo "")
|
||||||
|
json_finding "info" "Docker space usage detected" "docker system prune" "Run docker system df for details [use docker system prune to clean]"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Skip silently if docker not installed
|
||||||
|
true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Trash ---
|
||||||
|
trash_dir="$HOME/.Trash"
|
||||||
|
if [ -d "$trash_dir" ]; then
|
||||||
|
result=$(dir_size "$trash_dir")
|
||||||
|
IFS='|' read -r kb human <<< "$result"
|
||||||
|
total_reclaimable_kb=$((total_reclaimable_kb + kb))
|
||||||
|
if [ "$kb" -gt 0 ] 2>/dev/null; then
|
||||||
|
severity=$(classify_size "$kb")
|
||||||
|
json_finding "$severity" "Trash: ${human}" "empty trash" "$trash_dir [safe]"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Old downloads (report only) ---
|
||||||
|
if [ -d "$HOME/Downloads" ]; then
|
||||||
|
old_files=$(find "$HOME/Downloads" -maxdepth 1 -mtime +90 -type f 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
if [ "$old_files" -gt 0 ] 2>/dev/null; then
|
||||||
|
old_size=$(find "$HOME/Downloads" -maxdepth 1 -mtime +90 -type f -exec du -ck {} + 2>/dev/null | tail -1 | awk '{print $1}')
|
||||||
|
old_human=$(echo "$old_size" | awk '{printf "%.1fM", $1/1024}')
|
||||||
|
json_finding "info" "${old_files} files in Downloads older than 90 days (${old_human})" "review" "Info only - review manually"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Total summary ---
|
||||||
|
total_human=$(echo "$total_reclaimable_kb" | awk '{if($1>1048576) printf "%.1fG",$1/1048576; else if($1>1024) printf "%.1fM",$1/1024; else printf "%dK",$1}')
|
||||||
|
json_finding "info" "Total scannable space: ${total_human}" "review" "Approx reclaimable across all categories"
|
||||||
127
custom-skills/92-mac-optimizer/skills/mac-optimizer/scripts/audit_environment.sh
Executable file
127
custom-skills/92-mac-optimizer/skills/mac-optimizer/scripts/audit_environment.sh
Executable file
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# audit_environment.sh — Audit shell environment and paths on macOS
|
||||||
|
# Output: JSON lines to stdout, one per finding
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' '; }
|
||||||
|
|
||||||
|
json_finding() {
|
||||||
|
local severity="$1" finding action details
|
||||||
|
finding=$(json_escape "$2")
|
||||||
|
action=$(json_escape "$3")
|
||||||
|
details=$(json_escape "$4")
|
||||||
|
printf '{"module":"environment","severity":"%s","finding":"%s","action":"%s","details":"%s"}\n' \
|
||||||
|
"$severity" "$finding" "$action" "$details"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- PATH analysis ---
|
||||||
|
IFS=':' read -ra path_dirs <<< "$PATH"
|
||||||
|
total_dirs=${#path_dirs[@]}
|
||||||
|
|
||||||
|
# Duplicate detection (compatible with bash 3)
|
||||||
|
duplicates=()
|
||||||
|
for i in "${!path_dirs[@]}"; do
|
||||||
|
dir="${path_dirs[$i]}"
|
||||||
|
is_dup=false
|
||||||
|
for j in "${!path_dirs[@]}"; do
|
||||||
|
if [ "$j" -lt "$i" ] && [ "${path_dirs[$j]}" = "$dir" ]; then
|
||||||
|
is_dup=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if $is_dup; then
|
||||||
|
duplicates+=("$dir")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#duplicates[@]} -gt 0 ]; then
|
||||||
|
dup_list=$(printf '%s, ' "${duplicates[@]}" | sed 's/, $//')
|
||||||
|
json_finding "warning" "${#duplicates[@]} duplicate PATH entries" "deduplicate" "$dup_list"
|
||||||
|
else
|
||||||
|
json_finding "info" "No duplicate PATH entries" "none" "${total_dirs} directories in PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Non-existent directories
|
||||||
|
missing=()
|
||||||
|
for dir in "${path_dirs[@]}"; do
|
||||||
|
if [ ! -d "$dir" ]; then
|
||||||
|
missing+=("$dir")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#missing[@]} -gt 0 ]; then
|
||||||
|
missing_list=$(printf '%s, ' "${missing[@]}" | sed 's/, $//')
|
||||||
|
json_finding "warning" "${#missing[@]} non-existent directories in PATH" "remove" "$missing_list"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Broken symlinks ---
|
||||||
|
broken_symlinks=()
|
||||||
|
for check_dir in /usr/local/bin "$HOME/.local/bin"; do
|
||||||
|
if [ -d "$check_dir" ]; then
|
||||||
|
while IFS= read -r link; do
|
||||||
|
broken_symlinks+=("$link")
|
||||||
|
done < <(find "$check_dir" -maxdepth 1 -type l ! -exec test -e {} \; -print 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#broken_symlinks[@]} -gt 0 ]; then
|
||||||
|
broken_list=$(printf '%s, ' "${broken_symlinks[@]}" | sed 's/, $//')
|
||||||
|
json_finding "warning" "${#broken_symlinks[@]} broken symlinks found" "remove" "$broken_list"
|
||||||
|
else
|
||||||
|
json_finding "info" "No broken symlinks in bin directories" "none" ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Shell config analysis ---
|
||||||
|
shell_configs=()
|
||||||
|
for cfg in "$HOME/.zshrc" "$HOME/.zprofile" "$HOME/.zshenv" "$HOME/.bash_profile" "$HOME/.bashrc"; do
|
||||||
|
if [ -f "$cfg" ]; then
|
||||||
|
shell_configs+=("$(basename "$cfg")")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
json_finding "info" "Shell configs found: ${shell_configs[*]}" "none" ""
|
||||||
|
|
||||||
|
# Check for PATH modifications across multiple files
|
||||||
|
path_mods=0
|
||||||
|
for cfg in "$HOME/.zshrc" "$HOME/.zprofile" "$HOME/.zshenv" "$HOME/.bash_profile" "$HOME/.bashrc"; do
|
||||||
|
if [ -f "$cfg" ]; then
|
||||||
|
count=$(grep -c -E 'export PATH|PATH=' "$cfg" 2>/dev/null || echo 0)
|
||||||
|
count=$(echo "$count" | tr -d '[:space:]')
|
||||||
|
path_mods=$((path_mods + count))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$path_mods" -gt 5 ]; then
|
||||||
|
json_finding "warning" "PATH modified ${path_mods} times across shell configs" "consolidate" "Consider consolidating PATH modifications"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Environment variable checks ---
|
||||||
|
if [ -z "${ANTHROPIC_API_KEY:-}" ]; then
|
||||||
|
json_finding "info" "ANTHROPIC_API_KEY not set" "none" "Set if using Claude API"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${JAVA_HOME:-}" ] && [ ! -d "$JAVA_HOME" ]; then
|
||||||
|
json_finding "warning" "JAVA_HOME points to non-existent directory" "fix" "$JAVA_HOME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Shell startup time ---
|
||||||
|
if command -v zsh &>/dev/null; then
|
||||||
|
startup_time=$( { time zsh -i -c exit; } 2>&1 | grep real | awk '{print $2}' || echo "unknown")
|
||||||
|
# Parse the time - handle both formats like "0m0.123s" and "0.123"
|
||||||
|
if echo "$startup_time" | grep -q 'm'; then
|
||||||
|
seconds=$(echo "$startup_time" | sed 's/.*m//' | sed 's/s//')
|
||||||
|
else
|
||||||
|
seconds="$startup_time"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v python3 &>/dev/null; then
|
||||||
|
is_slow=$(python3 -c "print('slow' if float('${seconds}') > 0.5 else 'ok')" 2>/dev/null || echo "ok")
|
||||||
|
else
|
||||||
|
is_slow="ok"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$is_slow" = "slow" ]; then
|
||||||
|
json_finding "warning" "Shell startup is slow (${startup_time})" "optimize" "Consider profiling with zsh -xv"
|
||||||
|
else
|
||||||
|
json_finding "info" "Shell startup time: ${startup_time}" "none" ""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
118
custom-skills/92-mac-optimizer/skills/mac-optimizer/scripts/audit_packages.sh
Executable file
118
custom-skills/92-mac-optimizer/skills/mac-optimizer/scripts/audit_packages.sh
Executable file
@@ -0,0 +1,118 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# audit_packages.sh — Audit package managers on macOS
|
||||||
|
# Output: JSON lines to stdout, one per finding
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' '; }
|
||||||
|
|
||||||
|
json_finding() {
|
||||||
|
local severity="$1" finding action details
|
||||||
|
finding=$(json_escape "$2")
|
||||||
|
action=$(json_escape "$3")
|
||||||
|
details=$(json_escape "$4")
|
||||||
|
printf '{"module":"packages","severity":"%s","finding":"%s","action":"%s","details":"%s"}\n' \
|
||||||
|
"$severity" "$finding" "$action" "$details"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Homebrew ---
|
||||||
|
if command -v brew &>/dev/null; then
|
||||||
|
echo '{"module":"packages","severity":"info","finding":"Homebrew detected","action":"none","details":"'$(brew --version | head -1)'"}'
|
||||||
|
|
||||||
|
# brew update
|
||||||
|
brew update --quiet 2>/dev/null || true
|
||||||
|
|
||||||
|
# outdated formulae
|
||||||
|
outdated=$(brew outdated --json=v2 2>/dev/null || echo '{}')
|
||||||
|
formula_count=$(echo "$outdated" | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d.get('formulae',[])))" 2>/dev/null || echo 0)
|
||||||
|
cask_count=$(echo "$outdated" | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d.get('casks',[])))" 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
if [ "$formula_count" -gt 0 ] 2>/dev/null; then
|
||||||
|
formula_list=$(echo "$outdated" | python3 -c "
|
||||||
|
import sys,json
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
names=[f['name'] for f in d.get('formulae',[])]
|
||||||
|
print(', '.join(names[:10]))
|
||||||
|
if len(names)>10: print(f'... and {len(names)-10} more')
|
||||||
|
" 2>/dev/null || echo "")
|
||||||
|
json_finding "warning" "${formula_count} Homebrew formulae outdated" "brew upgrade" "$formula_list"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$cask_count" -gt 0 ] 2>/dev/null; then
|
||||||
|
json_finding "warning" "${cask_count} Homebrew casks outdated" "brew upgrade --cask" ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$formula_count" -eq 0 ] && [ "$cask_count" -eq 0 ] 2>/dev/null; then
|
||||||
|
json_finding "info" "All Homebrew packages up to date" "none" ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# brew doctor
|
||||||
|
doctor_output=$(brew doctor 2>&1 || true)
|
||||||
|
doctor_warnings=$(echo "$doctor_output" | grep -c "Warning:" 2>/dev/null || echo 0)
|
||||||
|
if [ "$doctor_warnings" -gt 0 ] 2>/dev/null; then
|
||||||
|
json_finding "warning" "brew doctor found ${doctor_warnings} warning(s)" "brew doctor" "Run brew doctor for details"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# autoremove candidates
|
||||||
|
autoremove_output=$(brew autoremove --dry-run 2>/dev/null || echo "")
|
||||||
|
orphan_count=$(echo "$autoremove_output" | grep -c "^=" 2>/dev/null || echo 0)
|
||||||
|
if echo "$autoremove_output" | grep -q "would remove" 2>/dev/null; then
|
||||||
|
json_finding "info" "Orphaned Homebrew dependencies found" "brew autoremove" "$autoremove_output"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
json_finding "info" "Homebrew not installed" "none" "Consider installing from https://brew.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- npm / nvm ---
|
||||||
|
if command -v nvm &>/dev/null || [ -d "$HOME/.nvm" ]; then
|
||||||
|
# Source nvm if available
|
||||||
|
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
||||||
|
[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh" 2>/dev/null
|
||||||
|
|
||||||
|
if command -v nvm &>/dev/null; then
|
||||||
|
node_versions=$(nvm ls --no-colors 2>/dev/null | grep -c "v[0-9]" || echo 0)
|
||||||
|
current_node=$(nvm current 2>/dev/null || echo "none")
|
||||||
|
json_finding "info" "nvm detected: ${node_versions} version(s), current: ${current_node}" "none" ""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v npm &>/dev/null; then
|
||||||
|
json_finding "info" "npm detected" "none" "$(npm --version 2>/dev/null)"
|
||||||
|
|
||||||
|
npm_outdated=$(npm outdated -g --json 2>/dev/null || echo '{}')
|
||||||
|
npm_outdated_count=$(echo "$npm_outdated" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
if [ "$npm_outdated_count" -gt 0 ] 2>/dev/null; then
|
||||||
|
json_finding "warning" "${npm_outdated_count} global npm packages outdated" "npm update -g" ""
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
json_finding "info" "npm not installed" "none" ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Python / pyenv ---
|
||||||
|
if command -v pyenv &>/dev/null; then
|
||||||
|
pyenv_versions=$(pyenv versions --bare 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
current_python=$(pyenv version-name 2>/dev/null || echo "system")
|
||||||
|
json_finding "info" "pyenv detected: ${pyenv_versions} version(s), current: ${current_python}" "none" ""
|
||||||
|
|
||||||
|
# Check pip outdated for current environment
|
||||||
|
if command -v pip &>/dev/null || command -v pip3 &>/dev/null; then
|
||||||
|
pip_cmd=$(command -v pip3 || command -v pip)
|
||||||
|
pip_outdated=$($pip_cmd list --outdated --format=json 2>/dev/null || echo '[]')
|
||||||
|
pip_count=$(echo "$pip_outdated" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))" 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
if [ "$pip_count" -gt 0 ] 2>/dev/null; then
|
||||||
|
json_finding "warning" "${pip_count} pip packages outdated in current env" "pip install --upgrade" ""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
elif command -v python3 &>/dev/null; then
|
||||||
|
py_version=$(python3 --version 2>/dev/null || echo "unknown")
|
||||||
|
json_finding "info" "System Python: ${py_version} (no pyenv)" "none" ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Other package managers (presence detection only) ---
|
||||||
|
for mgr in yarn pnpm cargo go gem composer; do
|
||||||
|
if command -v "$mgr" &>/dev/null; then
|
||||||
|
version=$($mgr --version 2>/dev/null | head -1 || echo "unknown")
|
||||||
|
json_finding "info" "${mgr} detected: ${version}" "none" "Presence check only"
|
||||||
|
fi
|
||||||
|
done
|
||||||
143
custom-skills/92-mac-optimizer/skills/mac-optimizer/scripts/audit_resources.sh
Executable file
143
custom-skills/92-mac-optimizer/skills/mac-optimizer/scripts/audit_resources.sh
Executable file
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# audit_resources.sh — Monitor CPU, memory, disk, battery on macOS
|
||||||
|
# Output: JSON lines to stdout, one per finding
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' '; }
|
||||||
|
|
||||||
|
json_finding() {
|
||||||
|
local severity="$1" finding action details
|
||||||
|
finding=$(json_escape "$2")
|
||||||
|
action=$(json_escape "$3")
|
||||||
|
details=$(json_escape "$4")
|
||||||
|
printf '{"module":"resources","severity":"%s","finding":"%s","action":"%s","details":"%s"}\n' \
|
||||||
|
"$severity" "$finding" "$action" "$details"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Process deny-list (never suggest termination)
|
||||||
|
DENY_LIST="kernel_task|launchd|WindowServer|loginwindow|mds|mds_stores|opendirectoryd|coreaudiod|SystemUIServer|Finder|Dock"
|
||||||
|
|
||||||
|
# --- CPU ---
|
||||||
|
load_avg=$(sysctl -n vm.loadavg 2>/dev/null | tr -d '{}' | awk '{print $1}')
|
||||||
|
cpu_count=$(sysctl -n hw.ncpu 2>/dev/null || echo 1)
|
||||||
|
|
||||||
|
if command -v python3 &>/dev/null; then
|
||||||
|
cpu_severity=$(python3 -c "
|
||||||
|
load=$load_avg; cores=$cpu_count
|
||||||
|
if load > cores * 2: print('critical')
|
||||||
|
elif load > cores: print('warning')
|
||||||
|
else: print('info')
|
||||||
|
" 2>/dev/null || echo "info")
|
||||||
|
else
|
||||||
|
cpu_severity="info"
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_finding "$cpu_severity" "Load average: ${load_avg} (${cpu_count} cores)" "review" ""
|
||||||
|
|
||||||
|
# Top CPU-consuming processes (excluding deny-list from action suggestions)
|
||||||
|
top_cpu=$(ps aux | sort -nrk 3 | head -5 || echo "")
|
||||||
|
if [ -n "$top_cpu" ]; then
|
||||||
|
while IFS= read -r line; do
|
||||||
|
proc_name=$(echo "$line" | awk '{print $11}' | xargs basename 2>/dev/null || echo "unknown")
|
||||||
|
cpu_pct=$(echo "$line" | awk '{print $3}')
|
||||||
|
mem_pct=$(echo "$line" | awk '{print $4}')
|
||||||
|
pid=$(echo "$line" | awk '{print $2}')
|
||||||
|
|
||||||
|
# Skip low-usage processes
|
||||||
|
is_high=$(python3 -c "print('yes' if float('${cpu_pct}') > 10 else 'no')" 2>/dev/null || echo "no")
|
||||||
|
if [ "$is_high" = "yes" ]; then
|
||||||
|
if echo "$proc_name" | grep -qE "$DENY_LIST"; then
|
||||||
|
json_finding "info" "High CPU: ${proc_name} (${cpu_pct}% CPU, PID ${pid})" "none" "System process - do not terminate"
|
||||||
|
else
|
||||||
|
json_finding "warning" "High CPU: ${proc_name} (${cpu_pct}% CPU, PID ${pid})" "quit" "User process - can be terminated"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done <<< "$top_cpu"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Memory ---
|
||||||
|
vm_stat_output=$(vm_stat 2>/dev/null || echo "")
|
||||||
|
if [ -n "$vm_stat_output" ]; then
|
||||||
|
page_size=$(vm_stat | head -1 | grep -o '[0-9]*' || echo 16384)
|
||||||
|
|
||||||
|
free_pages=$(echo "$vm_stat_output" | grep "Pages free" | awk '{print $3}' | tr -d '.')
|
||||||
|
active_pages=$(echo "$vm_stat_output" | grep "Pages active" | awk '{print $3}' | tr -d '.')
|
||||||
|
inactive_pages=$(echo "$vm_stat_output" | grep "Pages inactive" | awk '{print $3}' | tr -d '.')
|
||||||
|
wired_pages=$(echo "$vm_stat_output" | grep "Pages wired" | awk '{print $4}' | tr -d '.')
|
||||||
|
compressed_pages=$(echo "$vm_stat_output" | grep "Pages stored in compressor" | awk '{print $5}' | tr -d '.' || echo 0)
|
||||||
|
|
||||||
|
if command -v python3 &>/dev/null; then
|
||||||
|
mem_summary=$(python3 -c "
|
||||||
|
ps=${page_size}
|
||||||
|
free_mb = int(${free_pages:-0}) * ps / 1048576
|
||||||
|
active_mb = int(${active_pages:-0}) * ps / 1048576
|
||||||
|
inactive_mb = int(${inactive_pages:-0}) * ps / 1048576
|
||||||
|
wired_mb = int(${wired_pages:-0}) * ps / 1048576
|
||||||
|
compressed_mb = int(${compressed_pages:-0}) * ps / 1048576
|
||||||
|
used_mb = active_mb + wired_mb + compressed_mb
|
||||||
|
total_mb = free_mb + active_mb + inactive_mb + wired_mb + compressed_mb
|
||||||
|
pct = used_mb / total_mb * 100 if total_mb > 0 else 0
|
||||||
|
print(f'{used_mb:.0f}MB used / {total_mb:.0f}MB total ({pct:.0f}%)')
|
||||||
|
if pct > 90: print('SEVERITY:critical')
|
||||||
|
elif pct > 75: print('SEVERITY:warning')
|
||||||
|
else: print('SEVERITY:info')
|
||||||
|
" 2>/dev/null || echo "unknown")
|
||||||
|
mem_severity=$(echo "$mem_summary" | grep "SEVERITY:" | cut -d: -f2)
|
||||||
|
mem_info=$(echo "$mem_summary" | head -1)
|
||||||
|
json_finding "${mem_severity:-info}" "Memory: ${mem_info}" "review" ""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Swap ---
|
||||||
|
swap_info=$(sysctl vm.swapusage 2>/dev/null || echo "")
|
||||||
|
if [ -n "$swap_info" ]; then
|
||||||
|
swap_used=$(echo "$swap_info" | grep -o "used = [0-9.]*M" | awk '{print $3}' | tr -d 'M')
|
||||||
|
if [ -n "$swap_used" ] && command -v python3 &>/dev/null; then
|
||||||
|
swap_severity=$(python3 -c "
|
||||||
|
s=float('${swap_used}')
|
||||||
|
if s > 4096: print('warning')
|
||||||
|
elif s > 1024: print('info')
|
||||||
|
else: print('info')
|
||||||
|
" 2>/dev/null || echo "info")
|
||||||
|
json_finding "$swap_severity" "Swap: ${swap_used}MB used" "review" "$swap_info"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Disk usage ---
|
||||||
|
disk_info=$(df -h / 2>/dev/null | tail -1)
|
||||||
|
if [ -n "$disk_info" ]; then
|
||||||
|
disk_used_pct=$(echo "$disk_info" | awk '{print $5}' | tr -d '%')
|
||||||
|
disk_avail=$(echo "$disk_info" | awk '{print $4}')
|
||||||
|
disk_total=$(echo "$disk_info" | awk '{print $2}')
|
||||||
|
|
||||||
|
if [ "$disk_used_pct" -gt 90 ] 2>/dev/null; then
|
||||||
|
json_finding "critical" "Disk ${disk_used_pct}% full (${disk_avail} free of ${disk_total})" "cleanup" "Consider running cleanup module"
|
||||||
|
elif [ "$disk_used_pct" -gt 75 ] 2>/dev/null; then
|
||||||
|
json_finding "warning" "Disk ${disk_used_pct}% full (${disk_avail} free of ${disk_total})" "monitor" ""
|
||||||
|
else
|
||||||
|
json_finding "info" "Disk ${disk_used_pct}% full (${disk_avail} free of ${disk_total})" "none" ""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Large directories (top 5 in home) ---
|
||||||
|
if command -v du &>/dev/null; then
|
||||||
|
large_dirs=$(du -sh "$HOME"/*/ "$HOME"/.[!.]* 2>/dev/null | sort -hr | head -5 || echo "")
|
||||||
|
if [ -n "$large_dirs" ]; then
|
||||||
|
dir_summary=$(echo "$large_dirs" | awk '{printf "%s (%s), ", $2, $1}' | sed 's/, $//')
|
||||||
|
json_finding "info" "Largest directories in home" "review" "$dir_summary"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Battery (laptops only) ---
|
||||||
|
battery_info=$(system_profiler SPPowerDataType 2>/dev/null || echo "")
|
||||||
|
if echo "$battery_info" | grep -q "Cycle Count"; then
|
||||||
|
cycle_count=$(echo "$battery_info" | grep "Cycle Count" | awk '{print $NF}')
|
||||||
|
condition=$(echo "$battery_info" | grep "Condition" | awk '{print $NF}')
|
||||||
|
max_capacity=$(echo "$battery_info" | grep "Maximum Capacity" | awk '{print $NF}' || echo "unknown")
|
||||||
|
|
||||||
|
if [ "$condition" = "Normal" ]; then
|
||||||
|
json_finding "info" "Battery: ${condition}, ${cycle_count} cycles, ${max_capacity} capacity" "none" ""
|
||||||
|
else
|
||||||
|
json_finding "warning" "Battery: ${condition}, ${cycle_count} cycles, ${max_capacity} capacity" "review" "Battery may need service"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
122
custom-skills/92-mac-optimizer/skills/mac-optimizer/scripts/audit_security.sh
Executable file
122
custom-skills/92-mac-optimizer/skills/mac-optimizer/scripts/audit_security.sh
Executable file
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# audit_security.sh — Security posture assessment for macOS (read-only)
|
||||||
|
# Output: JSON lines to stdout, one per finding
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
json_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' '; }
|
||||||
|
|
||||||
|
json_finding() {
|
||||||
|
local severity="$1" finding action details
|
||||||
|
finding=$(json_escape "$2")
|
||||||
|
action=$(json_escape "$3")
|
||||||
|
details=$(json_escape "$4")
|
||||||
|
printf '{"module":"security","severity":"%s","finding":"%s","action":"%s","details":"%s"}\n' \
|
||||||
|
"$severity" "$finding" "$action" "$details"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- SIP (System Integrity Protection) ---
|
||||||
|
sip_status=$(csrutil status 2>/dev/null || echo "unknown")
|
||||||
|
if echo "$sip_status" | grep -q "enabled"; then
|
||||||
|
json_finding "info" "SIP enabled" "none" ""
|
||||||
|
elif echo "$sip_status" | grep -q "disabled"; then
|
||||||
|
json_finding "critical" "SIP is disabled" "report" "System Integrity Protection should be enabled"
|
||||||
|
else
|
||||||
|
json_finding "warning" "SIP status unknown" "report" "$sip_status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Gatekeeper ---
|
||||||
|
gatekeeper_status=$(spctl --status 2>/dev/null || echo "unknown")
|
||||||
|
if echo "$gatekeeper_status" | grep -q "assessments enabled"; then
|
||||||
|
json_finding "info" "Gatekeeper enabled" "none" ""
|
||||||
|
elif echo "$gatekeeper_status" | grep -q "assessments disabled"; then
|
||||||
|
json_finding "critical" "Gatekeeper is disabled" "report" "Enable via: sudo spctl --master-enable"
|
||||||
|
else
|
||||||
|
json_finding "warning" "Gatekeeper status unknown" "report" "$gatekeeper_status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Firewall ---
|
||||||
|
fw_tool="/usr/libexec/ApplicationFirewall/socketfilterfw"
|
||||||
|
if [ -x "$fw_tool" ]; then
|
||||||
|
fw_status=$("$fw_tool" --getglobalstate 2>/dev/null || echo "unknown")
|
||||||
|
if echo "$fw_status" | grep -qi "enabled"; then
|
||||||
|
json_finding "info" "Firewall enabled" "none" ""
|
||||||
|
else
|
||||||
|
json_finding "critical" "Firewall is disabled" "report" "Enable in System Settings > Network > Firewall"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Fallback to defaults
|
||||||
|
fw_default=$(defaults read /Library/Preferences/com.apple.alf globalstate 2>/dev/null || echo "unknown")
|
||||||
|
case "$fw_default" in
|
||||||
|
0) json_finding "critical" "Firewall is disabled" "report" "Enable in System Settings > Network > Firewall" ;;
|
||||||
|
1|2) json_finding "info" "Firewall enabled" "none" "" ;;
|
||||||
|
*) json_finding "warning" "Firewall status unknown" "report" "" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- FileVault ---
|
||||||
|
fv_status=$(fdesetup status 2>/dev/null || echo "unknown")
|
||||||
|
if echo "$fv_status" | grep -q "FileVault is On"; then
|
||||||
|
json_finding "info" "FileVault enabled (disk encryption)" "none" ""
|
||||||
|
elif echo "$fv_status" | grep -q "FileVault is Off"; then
|
||||||
|
json_finding "critical" "FileVault is off (disk not encrypted)" "report" "Enable in System Settings > Privacy and Security > FileVault"
|
||||||
|
else
|
||||||
|
json_finding "warning" "FileVault status unknown" "report" "$fv_status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Open listening ports ---
|
||||||
|
listening_ports=$(lsof -iTCP -sTCP:LISTEN -nP 2>/dev/null | tail -n +2 || echo "")
|
||||||
|
port_count=$(echo "$listening_ports" | grep -c . 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
if [ "$port_count" -gt 0 ]; then
|
||||||
|
# Extract unique port/process combos
|
||||||
|
port_summary=$(echo "$listening_ports" | awk '{print $1":"$9}' | sort -u | head -10 | tr '\n' ', ' | sed 's/,$//')
|
||||||
|
json_finding "info" "${port_count} listening TCP connections" "review" "$port_summary"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- SSH config ---
|
||||||
|
ssh_config="/etc/ssh/sshd_config"
|
||||||
|
if [ -f "$ssh_config" ]; then
|
||||||
|
# Check password auth
|
||||||
|
if grep -qi "^PasswordAuthentication yes" "$ssh_config" 2>/dev/null; then
|
||||||
|
json_finding "warning" "SSH password authentication enabled" "report" "Consider key-based auth only"
|
||||||
|
fi
|
||||||
|
# Check root login
|
||||||
|
if grep -qi "^PermitRootLogin yes" "$ssh_config" 2>/dev/null; then
|
||||||
|
json_finding "warning" "SSH root login permitted" "report" "Consider disabling root SSH login"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Remote login ---
|
||||||
|
remote_login=$(systemsetup -getremotelogin 2>/dev/null || echo "unknown")
|
||||||
|
if echo "$remote_login" | grep -qi "on"; then
|
||||||
|
json_finding "info" "Remote Login (SSH) is enabled" "review" "Disable if not needed: System Settings > General > Sharing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Remote management ---
|
||||||
|
remote_mgmt=$(defaults read /Library/Preferences/com.apple.RemoteManagement ARD_AllLocalUsers 2>/dev/null || echo "not configured")
|
||||||
|
if [ "$remote_mgmt" = "1" ]; then
|
||||||
|
json_finding "warning" "Remote Management enabled for all users" "review" "Check System Settings > General > Sharing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Software updates ---
|
||||||
|
# Just check if updates are available (this can be slow, so we time-limit it)
|
||||||
|
# macOS lacks GNU timeout — use perl alarm as a portable alternative
|
||||||
|
update_check=$(perl -e 'alarm 30; exec @ARGV' -- softwareupdate -l 2>&1 || echo "timeout or error")
|
||||||
|
if echo "$update_check" | grep -q "No new software available"; then
|
||||||
|
json_finding "info" "macOS software is up to date" "none" ""
|
||||||
|
elif echo "$update_check" | grep -q "timeout or error"; then
|
||||||
|
json_finding "info" "Software update check timed out" "none" "Run softwareupdate -l manually"
|
||||||
|
else
|
||||||
|
update_count=$(echo "$update_check" | grep -c "^\*" 2>/dev/null || echo 0)
|
||||||
|
json_finding "warning" "${update_count} software update(s) available" "softwareupdate" "Run: softwareupdate -l for details"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Screen lock ---
|
||||||
|
screen_lock_delay=$(defaults -currentHost read com.apple.screensaver idleTime 2>/dev/null || echo "not set")
|
||||||
|
ask_password=$(defaults read com.apple.screensaver askForPassword 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
if [ "$ask_password" = "0" ]; then
|
||||||
|
json_finding "warning" "Screen lock password not required" "report" "Enable in System Settings > Lock Screen"
|
||||||
|
elif [ "$screen_lock_delay" != "not set" ] && [ "$screen_lock_delay" -gt 300 ] 2>/dev/null; then
|
||||||
|
json_finding "info" "Screen lock delay: ${screen_lock_delay}s (consider reducing)" "report" ""
|
||||||
|
fi
|
||||||
169
custom-skills/92-mac-optimizer/skills/mac-optimizer/scripts/cleanup_execute.sh
Executable file
169
custom-skills/92-mac-optimizer/skills/mac-optimizer/scripts/cleanup_execute.sh
Executable file
@@ -0,0 +1,169 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# cleanup_execute.sh — Execute cleanup actions on macOS
|
||||||
|
# Usage: cleanup_execute.sh [--dry-run|--execute] <target> [<target>...]
|
||||||
|
# Targets: user-logs, user-caches, brew-cache, npm-cache, pip-cache,
|
||||||
|
# xcode-derived, xcode-archives, ios-support, docker, trash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MODE="dry-run"
|
||||||
|
TARGETS=()
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-$HOME/.config/mac-optimizer-backups}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "Usage: $0 [--dry-run|--execute] <target> [<target>...]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " --dry-run Show what would be deleted (default)"
|
||||||
|
echo " --execute Actually perform cleanup"
|
||||||
|
echo ""
|
||||||
|
echo "Targets:"
|
||||||
|
echo " user-logs ~/Library/Logs/"
|
||||||
|
echo " user-caches ~/Library/Caches/"
|
||||||
|
echo " brew-cache Homebrew download cache"
|
||||||
|
echo " npm-cache npm cache"
|
||||||
|
echo " pip-cache pip cache"
|
||||||
|
echo " xcode-derived Xcode DerivedData"
|
||||||
|
echo " xcode-archives Xcode Archives"
|
||||||
|
echo " ios-support iOS DeviceSupport files"
|
||||||
|
echo " docker Docker system prune"
|
||||||
|
echo " trash Empty Trash"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse args
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run) MODE="dry-run"; shift ;;
|
||||||
|
--execute) MODE="execute"; shift ;;
|
||||||
|
--help|-h) usage ;;
|
||||||
|
*) TARGETS+=("$1"); shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#TARGETS[@]} -eq 0 ]; then
|
||||||
|
echo "Error: No targets specified"
|
||||||
|
usage
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_action() {
|
||||||
|
local target="$1" action="$2" size="$3"
|
||||||
|
echo "[${MODE}] ${target}: ${action} (${size})"
|
||||||
|
}
|
||||||
|
|
||||||
|
clean_dir() {
|
||||||
|
local name="$1" path="$2"
|
||||||
|
if [ ! -d "$path" ]; then
|
||||||
|
echo "[skip] ${name}: directory not found (${path})"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
local size=$(du -sh "$path" 2>/dev/null | awk '{print $1}')
|
||||||
|
if [ "$MODE" = "dry-run" ]; then
|
||||||
|
log_action "$name" "would remove contents of ${path}" "$size"
|
||||||
|
else
|
||||||
|
log_action "$name" "removing contents of ${path}" "$size"
|
||||||
|
find "$path" -mindepth 1 -delete 2>/dev/null || {
|
||||||
|
# Fallback: remove what we can
|
||||||
|
find "$path" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
}
|
||||||
|
echo "[done] ${name}: cleaned"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
for target in "${TARGETS[@]}"; do
|
||||||
|
case "$target" in
|
||||||
|
user-logs)
|
||||||
|
clean_dir "User Logs" "$HOME/Library/Logs"
|
||||||
|
;;
|
||||||
|
user-caches)
|
||||||
|
clean_dir "User Caches" "$HOME/Library/Caches"
|
||||||
|
;;
|
||||||
|
brew-cache)
|
||||||
|
if command -v brew &>/dev/null; then
|
||||||
|
brew_cache=$(brew --cache 2>/dev/null || echo "")
|
||||||
|
if [ -n "$brew_cache" ]; then
|
||||||
|
size=$(du -sh "$brew_cache" 2>/dev/null | awk '{print $1}')
|
||||||
|
if [ "$MODE" = "dry-run" ]; then
|
||||||
|
log_action "Homebrew Cache" "would run brew cleanup" "$size"
|
||||||
|
else
|
||||||
|
log_action "Homebrew Cache" "running brew cleanup" "$size"
|
||||||
|
brew cleanup --prune=0 2>/dev/null || true
|
||||||
|
echo "[done] Homebrew Cache: cleaned"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[skip] brew-cache: Homebrew not installed"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
npm-cache)
|
||||||
|
if command -v npm &>/dev/null; then
|
||||||
|
npm_cache="$HOME/.npm/_cacache"
|
||||||
|
if [ -d "$npm_cache" ]; then
|
||||||
|
size=$(du -sh "$npm_cache" 2>/dev/null | awk '{print $1}')
|
||||||
|
if [ "$MODE" = "dry-run" ]; then
|
||||||
|
log_action "npm Cache" "would run npm cache clean --force" "$size"
|
||||||
|
else
|
||||||
|
log_action "npm Cache" "running npm cache clean" "$size"
|
||||||
|
npm cache clean --force 2>/dev/null || true
|
||||||
|
echo "[done] npm Cache: cleaned"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[skip] npm-cache: npm not installed"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
pip-cache)
|
||||||
|
pip_cache="$HOME/Library/Caches/pip"
|
||||||
|
if [ -d "$pip_cache" ]; then
|
||||||
|
size=$(du -sh "$pip_cache" 2>/dev/null | awk '{print $1}')
|
||||||
|
if [ "$MODE" = "dry-run" ]; then
|
||||||
|
log_action "pip Cache" "would run pip cache purge" "$size"
|
||||||
|
else
|
||||||
|
log_action "pip Cache" "running pip cache purge" "$size"
|
||||||
|
pip3 cache purge 2>/dev/null || pip cache purge 2>/dev/null || true
|
||||||
|
echo "[done] pip Cache: cleaned"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[skip] pip-cache: no pip cache found"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
xcode-derived)
|
||||||
|
clean_dir "Xcode DerivedData" "$HOME/Library/Developer/Xcode/DerivedData"
|
||||||
|
;;
|
||||||
|
xcode-archives)
|
||||||
|
clean_dir "Xcode Archives" "$HOME/Library/Developer/Xcode/Archives"
|
||||||
|
;;
|
||||||
|
ios-support)
|
||||||
|
clean_dir "iOS DeviceSupport" "$HOME/Library/Developer/Xcode/iOS DeviceSupport"
|
||||||
|
;;
|
||||||
|
docker)
|
||||||
|
if command -v docker &>/dev/null; then
|
||||||
|
if [ "$MODE" = "dry-run" ]; then
|
||||||
|
echo "[dry-run] Docker: would run docker system prune"
|
||||||
|
docker system df 2>/dev/null || echo " (Docker not running)"
|
||||||
|
else
|
||||||
|
echo "[execute] Docker: running docker system prune -f"
|
||||||
|
docker system prune -f 2>/dev/null || echo " Docker prune failed (is Docker running?)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[skip] docker: Docker not installed"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
trash)
|
||||||
|
trash_dir="$HOME/.Trash"
|
||||||
|
if [ -d "$trash_dir" ]; then
|
||||||
|
size=$(du -sh "$trash_dir" 2>/dev/null | awk '{print $1}')
|
||||||
|
if [ "$MODE" = "dry-run" ]; then
|
||||||
|
log_action "Trash" "would empty Trash" "$size"
|
||||||
|
else
|
||||||
|
log_action "Trash" "emptying Trash" "$size"
|
||||||
|
rm -rf "$trash_dir"/* 2>/dev/null || true
|
||||||
|
rm -rf "$trash_dir"/.[!.]* 2>/dev/null || true
|
||||||
|
echo "[done] Trash: emptied"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "[error] Unknown target: ${target}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
28
custom-skills/93-tui-designer/README.md
Normal file
28
custom-skills/93-tui-designer/README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# 93-tui-designer
|
||||||
|
|
||||||
|
Build Norton Commander / Gopher style TUI wizard interfaces for any Python CLI tool using Rich.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Reusable patterns and battle-tested gotchas for building retro-style terminal wizards with:
|
||||||
|
- Dual-panel layout (status + content)
|
||||||
|
- Stack-based Gopher navigation (numbered menus, Back, Home)
|
||||||
|
- Arrow-key list selector for long option lists
|
||||||
|
- Bilingual i18n with runtime toggle
|
||||||
|
- Keyboard-driven input (F-keys with alphanumeric fallbacks)
|
||||||
|
- Three-tier responsive layout (wide/narrow/single panel)
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
93-tui-designer/
|
||||||
|
code/SKILL.md # Main skill (architecture, patterns, gotchas)
|
||||||
|
shared/references/
|
||||||
|
dtm-wizard-reference.md # DTM Agent implementation reference
|
||||||
|
docs/ # Future: logs, lessons learned
|
||||||
|
```
|
||||||
|
|
||||||
|
## First Implementation
|
||||||
|
|
||||||
|
DTM Agent TUI Wizard — 20 files, 21 screens, 29 tests.
|
||||||
|
See `shared/references/dtm-wizard-reference.md` for details.
|
||||||
303
custom-skills/93-tui-designer/code/SKILL.md
Normal file
303
custom-skills/93-tui-designer/code/SKILL.md
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
---
|
||||||
|
name: tui-designer
|
||||||
|
description: Build Norton Commander / Gopher style TUI wizard interfaces for CLI tools using Python Rich. Covers architecture, components, keyboard input, bilingual i18n, and battle-tested gotchas.
|
||||||
|
version: 1.0.0
|
||||||
|
triggers:
|
||||||
|
- "build TUI", "TUI wizard", "terminal UI", "CLI wizard"
|
||||||
|
- "Norton Commander style", "Gopher style", "retro TUI"
|
||||||
|
- "Rich TUI", "interactive CLI", "keyboard navigation"
|
||||||
|
- "dual panel interface", "terminal wizard"
|
||||||
|
tools:
|
||||||
|
- Read
|
||||||
|
- Write
|
||||||
|
- Edit
|
||||||
|
- Bash
|
||||||
|
- Grep
|
||||||
|
- Glob
|
||||||
|
---
|
||||||
|
|
||||||
|
# TUI Designer
|
||||||
|
|
||||||
|
Build retro-style terminal wizard interfaces (Norton Commander + Gopher) for any Python CLI tool using the Rich library. No new dependencies — Rich + stdlib only.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Building an interactive CLI wizard or setup flow
|
||||||
|
- Adding keyboard-driven navigation to an existing CLI tool
|
||||||
|
- Creating dual-panel terminal interfaces with status + menus
|
||||||
|
- Building bilingual (or multi-language) terminal interfaces
|
||||||
|
|
||||||
|
## Architecture Blueprint
|
||||||
|
|
||||||
|
### Component Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/{project}/tui/
|
||||||
|
__init__.py # Public API: launch_tui()
|
||||||
|
i18n.py # String registry with runtime language toggle
|
||||||
|
input.py # Raw tty/termios keypress reader + escape parser
|
||||||
|
themes.py # Color scheme + NO_COLOR support
|
||||||
|
widgets.py # Shared primitives (status icons, mini-tables)
|
||||||
|
breadcrumb.py # Top navigation path bar
|
||||||
|
function_bar.py # Bottom F-key shortcut bar
|
||||||
|
status_panel.py # Left panel: system health / status
|
||||||
|
menu.py # Right panel: numbered menu items
|
||||||
|
dialog.py # Modal overlays (confirm, input, error)
|
||||||
|
core.py # Layout engine (assembles frame)
|
||||||
|
runner.py # Main event loop + screen stack
|
||||||
|
selector.py # Arrow-key list selector for long lists
|
||||||
|
renderers.py # Content renderers for leaf screens
|
||||||
|
screens/
|
||||||
|
__init__.py # Screen/MenuItem/Action dataclasses + registry
|
||||||
|
home.py # Root screen
|
||||||
|
{category}.py # One file per menu category
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
User keypress
|
||||||
|
-> input.py (parse_key_bytes -> normalize_key)
|
||||||
|
-> runner.py (_handle_key dispatches)
|
||||||
|
-> Navigation: ScreenStack push/pop
|
||||||
|
-> F-keys: toggle language, refresh, help, quit
|
||||||
|
-> Digits: jump cursor or push screen
|
||||||
|
-> Arrows: ListSelector cursor movement
|
||||||
|
-> Enter: confirm selection via key_handler callback
|
||||||
|
-> core.py (render_frame assembles panels)
|
||||||
|
-> Console output
|
||||||
|
```
|
||||||
|
|
||||||
|
## Screen System
|
||||||
|
|
||||||
|
### Screen Dataclass
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class Screen:
|
||||||
|
id: str # "setup.credentials"
|
||||||
|
title: str # i18n key
|
||||||
|
breadcrumb: list[str] # i18n key path
|
||||||
|
menu_items: list[MenuItem] # For menu screens
|
||||||
|
content_renderer: Callable | None # For leaf screens: (console, status_data) -> str
|
||||||
|
key_handler: Callable | None # For interactive leaves: (app, key) -> bool
|
||||||
|
on_enter: Callable | None # Runs when screen is pushed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Navigation Model (Gopher-style stack)
|
||||||
|
|
||||||
|
```
|
||||||
|
ScreenStack (LIFO):
|
||||||
|
[1-9] = push screen (menu) or jump cursor (leaf)
|
||||||
|
[B] = pop (back)
|
||||||
|
[H] = clear to root
|
||||||
|
[Q] = quit with confirm
|
||||||
|
Enter = confirm selection on leaf screens
|
||||||
|
Up/Down = navigate ListSelector items
|
||||||
|
```
|
||||||
|
|
||||||
|
### Screen Registration Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
# screens/setup.py
|
||||||
|
from myapp.tui.screens import Screen, MenuItem, register_screen
|
||||||
|
from myapp.tui.renderers import render_credentials, handle_credential_key
|
||||||
|
|
||||||
|
register_screen(Screen(
|
||||||
|
id="setup.credentials",
|
||||||
|
title="setup.credentials",
|
||||||
|
breadcrumb=["nav.home", "home.setup", "setup.credentials"],
|
||||||
|
content_renderer=render_credentials,
|
||||||
|
key_handler=handle_credential_key, # For interactive leaves
|
||||||
|
))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Keyboard Input
|
||||||
|
|
||||||
|
### Escape Sequence Parser
|
||||||
|
|
||||||
|
```python
|
||||||
|
# input.py — stdlib only, no dependencies
|
||||||
|
import tty, termios, select, sys, os
|
||||||
|
|
||||||
|
def read_key() -> str:
|
||||||
|
fd = sys.stdin.fileno()
|
||||||
|
old = termios.tcgetattr(fd)
|
||||||
|
try:
|
||||||
|
tty.setraw(fd)
|
||||||
|
first = os.read(fd, 1)
|
||||||
|
if first == b"\x1b":
|
||||||
|
ready, _, _ = select.select([fd], [], [], 0.05) # 50ms timeout
|
||||||
|
if ready:
|
||||||
|
rest = os.read(fd, 5)
|
||||||
|
return parse_escape(first + rest)
|
||||||
|
return "escape"
|
||||||
|
return parse_single(first)
|
||||||
|
finally:
|
||||||
|
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
||||||
|
```
|
||||||
|
|
||||||
|
### F-key Alphanumeric Fallbacks
|
||||||
|
|
||||||
|
Always provide fallbacks — terminals intercept F-keys unpredictably:
|
||||||
|
|
||||||
|
| F-key | Fallback | Action |
|
||||||
|
|-------|----------|--------|
|
||||||
|
| F1 | ? | Help |
|
||||||
|
| F3 | l | Language toggle |
|
||||||
|
| F5 | r | Refresh |
|
||||||
|
| F10 | q | Quit |
|
||||||
|
|
||||||
|
## ListSelector Component
|
||||||
|
|
||||||
|
For screens with 10+ selectable items, use `ListSelector` instead of number-only input:
|
||||||
|
|
||||||
|
```python
|
||||||
|
selector = ListSelector(
|
||||||
|
items=[SelectableItem(label="model-name", status="ready", data={...})],
|
||||||
|
on_select=my_callback,
|
||||||
|
header="Select model:", # str or Callable[[], str] for dynamic headers
|
||||||
|
footer="Hint text",
|
||||||
|
)
|
||||||
|
# In content_renderer: return selector.render()
|
||||||
|
# In key_handler: return selector.handle_key(app, key)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Arrow Up/Down moves visible `>` cursor
|
||||||
|
- Enter/Space confirms selection
|
||||||
|
- 1-9 jumps cursor (does NOT auto-select — user must press Enter)
|
||||||
|
- Sections headers group items visually
|
||||||
|
- Callable headers re-evaluate on each render (for dynamic "Active:" display)
|
||||||
|
|
||||||
|
## Line Input Mode
|
||||||
|
|
||||||
|
For screens requiring text input (file paths, names, search), use `read_line()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from myapp.tui.input import read_line
|
||||||
|
|
||||||
|
# In a key_handler:
|
||||||
|
def handle_my_key(app, key):
|
||||||
|
if key == "p":
|
||||||
|
app.console.print("Enter file path: ", end="")
|
||||||
|
path = read_line()
|
||||||
|
if path is None: # Escape pressed
|
||||||
|
return True
|
||||||
|
# Use the path...
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
def read_line(prompt: str = "") -> str | None:
|
||||||
|
"""Read a full line in raw mode. Returns None on Escape/Ctrl-C."""
|
||||||
|
# Uses tty.setraw() same as read_key()
|
||||||
|
# Accumulates chars in buffer, prints each as typed
|
||||||
|
# Handles: Enter (return string), Escape (return None),
|
||||||
|
# Ctrl-C (return None), Backspace (delete last)
|
||||||
|
# Escape sequences (arrow keys pressed during input) are consumed and ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
### Line Input Gotchas
|
||||||
|
|
||||||
|
| Gotcha | Problem | Fix |
|
||||||
|
|--------|---------|-----|
|
||||||
|
| Pasted paths with quotes | Users paste `'/path/to/file'` with quotes | Strip outer quotes: `path.strip("'\"")` |
|
||||||
|
| Tilde expansion | `~/.config/...` not expanded | Use `Path(path).expanduser()` |
|
||||||
|
| Raw mode echo | Characters not visible while typing | Manually `sys.stdout.write(ch)` each keystroke |
|
||||||
|
| Escape during input | Arrow keys produce garbage in buffer | Detect `0x1B`, consume rest of sequence, ignore |
|
||||||
|
| Module-level imports for testability | `import select` inside function can't be patched | Import `select`, `termios`, `tty` at module level (in `try/except`) |
|
||||||
|
|
||||||
|
## Bilingual i18n
|
||||||
|
|
||||||
|
Simple dict registry — no framework needed:
|
||||||
|
|
||||||
|
```python
|
||||||
|
STRINGS = {
|
||||||
|
"nav.back": {"en": "Back", "ko": "뒤로"},
|
||||||
|
...
|
||||||
|
}
|
||||||
|
_lang = "en"
|
||||||
|
|
||||||
|
def t(key: str) -> str:
|
||||||
|
return STRINGS.get(key, {}).get(_lang, key)
|
||||||
|
|
||||||
|
def toggle_language():
|
||||||
|
global _lang
|
||||||
|
_lang = "ko" if _lang == "en" else "en"
|
||||||
|
```
|
||||||
|
|
||||||
|
On toggle, clear ALL cached screen content so every screen re-renders.
|
||||||
|
|
||||||
|
## Three-Tier Responsive Layout
|
||||||
|
|
||||||
|
```python
|
||||||
|
if cols >= 100: # Full dual-panel
|
||||||
|
Columns([status_panel, content_panel], padding=(0, 1))
|
||||||
|
elif cols >= 80: # Narrow status
|
||||||
|
Columns([narrow_status, content_panel], padding=(0, 1))
|
||||||
|
else: # Single panel (status line + content stacked)
|
||||||
|
console.print(f"Status: {health}")
|
||||||
|
console.print(content)
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `rich.columns.Columns` (NOT `rich.layout.Layout`) — Columns auto-fits content height.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gotchas (Battle-Tested)
|
||||||
|
|
||||||
|
### Rich Library
|
||||||
|
|
||||||
|
| Gotcha | Problem | Fix |
|
||||||
|
|--------|---------|-----|
|
||||||
|
| `Panel(box=None)` | `AttributeError: NoneType has no attribute substitute` | Use `box=SIMPLE` from `rich.box` |
|
||||||
|
| `Layout` stretches panels | Panels expand to fill available height with empty space | Use `Columns` with `expand=False` instead |
|
||||||
|
| `Status` spinner nesting | "Only one live display may be active at once" crash | Never nest `Status()` inside other Rich output. Use plain `console.print("Loading...")` |
|
||||||
|
| `[1]` in markup | Rich interprets `[1]` as a potential style tag | Escape: `\[1]` |
|
||||||
|
| Rich + raw stdin | `tty.setraw()` conflicts with Rich's terminal state | Always restore termios in `finally` block |
|
||||||
|
| `NO_COLOR` env var | Must respect `NO_COLOR` and `DTM_NO_COLOR` | Check env vars, return plain theme dict |
|
||||||
|
|
||||||
|
### Keyboard Input
|
||||||
|
|
||||||
|
| Gotcha | Problem | Fix |
|
||||||
|
|--------|---------|-----|
|
||||||
|
| F-key escape sequences | Vary between Terminal.app, iTerm2, VS Code | Map multiple sequences per key + provide alphanumeric fallbacks |
|
||||||
|
| Items 10+ unreachable | Pressing `1` then `0` selects item 1 immediately | Digits jump cursor only, Enter confirms. Items 10+ use arrows |
|
||||||
|
| `Ctrl+C` in raw mode | Crashes without terminal restoration | Wrap main loop in `try/except KeyboardInterrupt` |
|
||||||
|
| Non-interactive detection | Piped input hangs on `read_key()` | Check `sys.stdin.isatty()` before entering TUI mode |
|
||||||
|
|
||||||
|
### Content & Caching
|
||||||
|
|
||||||
|
| Gotcha | Problem | Fix |
|
||||||
|
|--------|---------|-----|
|
||||||
|
| API calls per render | `content_renderer` called on every keystroke | Cache rendered content in `_cached_content[screen.id]`; clear on F5 |
|
||||||
|
| Stale cached content | Model/account change doesn't update display | `invalidate_screen()` clears cache; use callable headers for dynamic data |
|
||||||
|
| Config has null names | `set_active_account(id)` saves id but not name | Resolve names from API as fallback; backfill to config on success |
|
||||||
|
| Korean char alignment | `{label:<12}` misaligns CJK double-width chars | Calculate display width: `sum(2 if ord(c) > 0x7F else 1 for c in s)` |
|
||||||
|
| Language toggle stale | Screens show old language after F3 | Clear ALL `_cached_content` on language toggle, not just current screen |
|
||||||
|
|
||||||
|
### UX Patterns
|
||||||
|
|
||||||
|
| Pattern | Why |
|
||||||
|
|---------|-----|
|
||||||
|
| Confirmation panels (0.8s delay) | Users need visual feedback that their action took effect |
|
||||||
|
| `<< active` markers | Users must see which item is currently selected at a glance |
|
||||||
|
| Section headers in lists | Group related items (Recommended / Installed / LM Studio) |
|
||||||
|
| Guidance hints at bottom | Tell users how to reach features that require other steps first |
|
||||||
|
| Breadcrumb path always visible | Users always know where they are in the hierarchy |
|
||||||
|
|
||||||
|
## Checklist for New TUI Projects
|
||||||
|
|
||||||
|
- [ ] `from __future__ import annotations` in all TUI files
|
||||||
|
- [ ] `_is_interactive()` guard before entering TUI mode
|
||||||
|
- [ ] `--no-tui` and `--legacy` CLI flags for fallback
|
||||||
|
- [ ] Non-TTY detection falls back to plain text
|
||||||
|
- [ ] `NO_COLOR` / `FORCE_COLOR` env var support
|
||||||
|
- [ ] F-key alphanumeric fallbacks defined
|
||||||
|
- [ ] `KeyboardInterrupt` handled in main loop
|
||||||
|
- [ ] Escape sequences for F1-F10, arrows mapped
|
||||||
|
- [ ] Content cached, invalidated on refresh/change
|
||||||
|
- [ ] All user-facing strings through `t()` i18n function
|
||||||
|
- [ ] Tests for: i18n, input parser, screen registry, navigation stack
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# DTM Wizard — Reference Implementation
|
||||||
|
|
||||||
|
The DTM Agent TUI wizard is the first project built with this skill's patterns.
|
||||||
|
|
||||||
|
## Repository
|
||||||
|
- **Project**: D.intelligence Tag Manager Agent
|
||||||
|
- **Location**: `github.com/D-intelligence/dintel-gtm-agent`
|
||||||
|
- **TUI code**: `src/dtm/tui/` (20 files, ~2500 lines)
|
||||||
|
- **Tests**: `tools/tests/unit/test_tui_*.py` (29 tests)
|
||||||
|
|
||||||
|
## File Inventory
|
||||||
|
|
||||||
|
| File | Lines | Purpose |
|
||||||
|
|------|-------|---------|
|
||||||
|
| `__init__.py` | 13 | Public API |
|
||||||
|
| `i18n.py` | 93 | 60+ bilingual EN/KR strings |
|
||||||
|
| `input.py` | 93 | Escape sequence parser |
|
||||||
|
| `themes.py` | 48 | Norton Commander color scheme |
|
||||||
|
| `widgets.py` | 35 | Status icons, mini-tables |
|
||||||
|
| `breadcrumb.py` | 22 | Navigation path bar |
|
||||||
|
| `function_bar.py` | 33 | F-key shortcut bar |
|
||||||
|
| `status_panel.py` | 85 | Left panel health display |
|
||||||
|
| `menu.py` | 35 | Gopher-style numbered menu |
|
||||||
|
| `dialog.py` | 70 | Modal overlays |
|
||||||
|
| `core.py` | 100 | Three-tier layout engine |
|
||||||
|
| `runner.py` | 170 | Event loop + ScreenStack |
|
||||||
|
| `selector.py` | 156 | Arrow-key list selector |
|
||||||
|
| `renderers.py` | ~1100 | 15 leaf screen content renderers |
|
||||||
|
| `screens/*.py` | ~250 | 21 screen definitions |
|
||||||
|
|
||||||
|
## Screen Hierarchy (21 screens)
|
||||||
|
|
||||||
|
```
|
||||||
|
home
|
||||||
|
setup
|
||||||
|
setup.credentials
|
||||||
|
setup.account
|
||||||
|
setup.account.containers (dynamic sub-screen)
|
||||||
|
setup.ai
|
||||||
|
setup.connectivity
|
||||||
|
operations
|
||||||
|
ops.workflow
|
||||||
|
ops.container_analysis
|
||||||
|
ops.ai_analysis
|
||||||
|
ops.performance
|
||||||
|
configuration
|
||||||
|
config.review
|
||||||
|
config.change_account
|
||||||
|
config.change_container
|
||||||
|
config.export
|
||||||
|
diagnostics
|
||||||
|
diag.health
|
||||||
|
diag.test
|
||||||
|
diag.recommendations
|
||||||
|
```
|
||||||
|
|
||||||
|
## PRs and Evolution
|
||||||
|
|
||||||
|
| PR | Title | Key Changes |
|
||||||
|
|----|-------|-------------|
|
||||||
|
| #10 | Core TUI redesign | 19 modules, 28 tests |
|
||||||
|
| #11 | 7 usability fixes | Panel height, no-color, bilingual icons |
|
||||||
|
| #12 | Interactive selection | Account/container by number, help screen |
|
||||||
|
| #13 | Name resolution | Resolve IDs to names from API |
|
||||||
|
| #14 | Loading spinners | Visual feedback during API calls |
|
||||||
|
| #15 | AI model selector | Ollama + LM Studio detection |
|
||||||
|
| #16 | Batch UX fixes | ListSelector, export formats, crash fixes |
|
||||||
|
|
||||||
|
## External Service Detection Pattern
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _detect_lm_studio() -> dict:
|
||||||
|
"""Detect LM Studio on localhost:1234."""
|
||||||
|
import requests
|
||||||
|
try:
|
||||||
|
resp = requests.get("http://localhost:1234/v1/models", timeout=3)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
return {"available": True, "models": [m["id"] for m in resp.json().get("data", [])]}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"available": False, "models": []}
|
||||||
|
```
|
||||||
|
|
||||||
|
This pattern works for any OpenAI-compatible local LLM server.
|
||||||
Reference in New Issue
Block a user