qualia-framework 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/bin/cli.js +519 -0
- package/framework/agents/architecture-strategist.md +53 -0
- package/framework/agents/backend-agent.md +150 -0
- package/framework/agents/code-simplicity-reviewer.md +86 -0
- package/framework/agents/frontend-agent.md +111 -0
- package/framework/agents/kieran-typescript-reviewer.md +96 -0
- package/framework/agents/performance-oracle.md +111 -0
- package/framework/agents/qualia-codebase-mapper.md +760 -0
- package/framework/agents/qualia-debugger.md +1203 -0
- package/framework/agents/qualia-executor.md +881 -0
- package/framework/agents/qualia-integration-checker.md +423 -0
- package/framework/agents/qualia-phase-researcher.md +453 -0
- package/framework/agents/qualia-plan-checker.md +699 -0
- package/framework/agents/qualia-planner.md +1241 -0
- package/framework/agents/qualia-project-researcher.md +602 -0
- package/framework/agents/qualia-research-synthesizer.md +236 -0
- package/framework/agents/qualia-roadmapper.md +605 -0
- package/framework/agents/qualia-verifier.md +685 -0
- package/framework/agents/team-orchestrator.md +228 -0
- package/framework/agents/teams/full-stack-team.md +48 -0
- package/framework/agents/teams/optimize-team.md +53 -0
- package/framework/agents/teams/review-team.md +62 -0
- package/framework/agents/teams/ship-team.md +86 -0
- package/framework/agents/test-agent.md +182 -0
- package/framework/askpass.sh +2 -0
- package/framework/commands/design.md +53 -0
- package/framework/commands/quick-db.md +22 -0
- package/framework/config/retention.json +35 -0
- package/framework/core/PRINCIPLES.md +77 -0
- package/framework/hooks/auto-format.sh +45 -0
- package/framework/hooks/block-env-edit.sh +42 -0
- package/framework/hooks/branch-guard.sh +46 -0
- package/framework/hooks/confirm-delete.sh +56 -0
- package/framework/hooks/migration-validate.sh +68 -0
- package/framework/hooks/notification-speak.sh +15 -0
- package/framework/hooks/pre-commit.sh +80 -0
- package/framework/hooks/pre-compact.sh +55 -0
- package/framework/hooks/pre-deploy-gate.sh +151 -0
- package/framework/hooks/qualia-colors.sh +32 -0
- package/framework/hooks/retention-cleanup.sh +43 -0
- package/framework/hooks/save-session-state.sh +153 -0
- package/framework/hooks/session-context-loader.sh +28 -0
- package/framework/hooks/session-learn.sh +30 -0
- package/framework/knowledge/claudecode-bible.md +1384 -0
- package/framework/knowledge/client-prefs.md +22 -0
- package/framework/knowledge/common-fixes.md +25 -0
- package/framework/knowledge/deployment-map.md +35 -0
- package/framework/knowledge/email-signature.html +1 -0
- package/framework/knowledge/employees.md +8 -0
- package/framework/knowledge/learned-patterns.md +51 -0
- package/framework/knowledge/optimization-research-2026.md +137 -0
- package/framework/knowledge/qualia-context.md +67 -0
- package/framework/knowledge/supabase-patterns.md +50 -0
- package/framework/knowledge/voice-agent-patterns.md +46 -0
- package/framework/qualia-engine/VERSION +1 -0
- package/framework/qualia-engine/bin/qualia-tools.js +2160 -0
- package/framework/qualia-engine/bin/qualia-tools.test.js +1054 -0
- package/framework/qualia-engine/references/checkpoints.md +775 -0
- package/framework/qualia-engine/references/continuation-format.md +249 -0
- package/framework/qualia-engine/references/decimal-phase-calculation.md +65 -0
- package/framework/qualia-engine/references/design-quality.md +56 -0
- package/framework/qualia-engine/references/git-integration.md +254 -0
- package/framework/qualia-engine/references/git-planning-commit.md +50 -0
- package/framework/qualia-engine/references/model-profile-resolution.md +32 -0
- package/framework/qualia-engine/references/model-profiles.md +73 -0
- package/framework/qualia-engine/references/phase-argument-parsing.md +61 -0
- package/framework/qualia-engine/references/planning-config.md +195 -0
- package/framework/qualia-engine/references/questioning.md +141 -0
- package/framework/qualia-engine/references/tdd.md +263 -0
- package/framework/qualia-engine/references/ui-brand.md +160 -0
- package/framework/qualia-engine/references/verification-patterns.md +612 -0
- package/framework/qualia-engine/templates/DEBUG.md +159 -0
- package/framework/qualia-engine/templates/DESIGN.md +81 -0
- package/framework/qualia-engine/templates/UAT.md +247 -0
- package/framework/qualia-engine/templates/codebase/architecture.md +255 -0
- package/framework/qualia-engine/templates/codebase/concerns.md +310 -0
- package/framework/qualia-engine/templates/codebase/conventions.md +307 -0
- package/framework/qualia-engine/templates/codebase/integrations.md +280 -0
- package/framework/qualia-engine/templates/codebase/stack.md +186 -0
- package/framework/qualia-engine/templates/codebase/structure.md +285 -0
- package/framework/qualia-engine/templates/codebase/testing.md +480 -0
- package/framework/qualia-engine/templates/config.json +35 -0
- package/framework/qualia-engine/templates/context.md +283 -0
- package/framework/qualia-engine/templates/continue-here.md +78 -0
- package/framework/qualia-engine/templates/debug-subagent-prompt.md +91 -0
- package/framework/qualia-engine/templates/discovery.md +146 -0
- package/framework/qualia-engine/templates/milestone-archive.md +123 -0
- package/framework/qualia-engine/templates/milestone.md +115 -0
- package/framework/qualia-engine/templates/phase-prompt.md +567 -0
- package/framework/qualia-engine/templates/planner-subagent-prompt.md +117 -0
- package/framework/qualia-engine/templates/project.md +184 -0
- package/framework/qualia-engine/templates/projects/ai-agent.md +156 -0
- package/framework/qualia-engine/templates/projects/mobile-app.md +181 -0
- package/framework/qualia-engine/templates/projects/voice-agent.md +134 -0
- package/framework/qualia-engine/templates/projects/website.md +137 -0
- package/framework/qualia-engine/templates/requirements.md +231 -0
- package/framework/qualia-engine/templates/research-project/ARCHITECTURE.md +204 -0
- package/framework/qualia-engine/templates/research-project/FEATURES.md +147 -0
- package/framework/qualia-engine/templates/research-project/PITFALLS.md +200 -0
- package/framework/qualia-engine/templates/research-project/STACK.md +120 -0
- package/framework/qualia-engine/templates/research-project/SUMMARY.md +170 -0
- package/framework/qualia-engine/templates/research.md +552 -0
- package/framework/qualia-engine/templates/roadmap.md +202 -0
- package/framework/qualia-engine/templates/state.md +176 -0
- package/framework/qualia-engine/templates/summary-complex.md +59 -0
- package/framework/qualia-engine/templates/summary-minimal.md +41 -0
- package/framework/qualia-engine/templates/summary-standard.md +48 -0
- package/framework/qualia-engine/templates/summary.md +246 -0
- package/framework/qualia-engine/templates/user-setup.md +311 -0
- package/framework/qualia-engine/templates/verification-report.md +322 -0
- package/framework/qualia-engine/workflows/add-phase.md +179 -0
- package/framework/qualia-engine/workflows/add-todo.md +157 -0
- package/framework/qualia-engine/workflows/audit-milestone.md +241 -0
- package/framework/qualia-engine/workflows/check-todos.md +176 -0
- package/framework/qualia-engine/workflows/complete-milestone.md +858 -0
- package/framework/qualia-engine/workflows/diagnose-issues.md +219 -0
- package/framework/qualia-engine/workflows/discovery-phase.md +289 -0
- package/framework/qualia-engine/workflows/discuss-phase.md +534 -0
- package/framework/qualia-engine/workflows/execute-phase.md +559 -0
- package/framework/qualia-engine/workflows/execute-plan.md +438 -0
- package/framework/qualia-engine/workflows/help.md +470 -0
- package/framework/qualia-engine/workflows/insert-phase.md +220 -0
- package/framework/qualia-engine/workflows/list-phase-assumptions.md +178 -0
- package/framework/qualia-engine/workflows/map-codebase.md +327 -0
- package/framework/qualia-engine/workflows/new-milestone.md +363 -0
- package/framework/qualia-engine/workflows/new-project.md +1037 -0
- package/framework/qualia-engine/workflows/pause-work.md +122 -0
- package/framework/qualia-engine/workflows/plan-milestone-gaps.md +256 -0
- package/framework/qualia-engine/workflows/plan-phase.md +422 -0
- package/framework/qualia-engine/workflows/progress.md +354 -0
- package/framework/qualia-engine/workflows/quick.md +252 -0
- package/framework/qualia-engine/workflows/remove-phase.md +326 -0
- package/framework/qualia-engine/workflows/research-phase.md +74 -0
- package/framework/qualia-engine/workflows/resume-project.md +306 -0
- package/framework/qualia-engine/workflows/set-profile.md +80 -0
- package/framework/qualia-engine/workflows/settings.md +145 -0
- package/framework/qualia-engine/workflows/transition.md +556 -0
- package/framework/qualia-engine/workflows/update.md +197 -0
- package/framework/qualia-engine/workflows/verify-phase.md +195 -0
- package/framework/qualia-engine/workflows/verify-work.md +625 -0
- package/framework/rules/context7.md +11 -0
- package/framework/rules/deployment.md +29 -0
- package/framework/rules/frontend.md +33 -0
- package/framework/rules/security.md +12 -0
- package/framework/rules/speed.md +20 -0
- package/framework/scripts/__pycache__/say.cpython-314.pyc +0 -0
- package/framework/scripts/apply-retention.sh +120 -0
- package/framework/scripts/bootstrap-pop-os.sh +354 -0
- package/framework/scripts/claude-voice +13 -0
- package/framework/scripts/cleanup.sh +131 -0
- package/framework/scripts/cowork-mode.sh +141 -0
- package/framework/scripts/generate-project-claude-md.sh +153 -0
- package/framework/scripts/load-test-webhook.js +172 -0
- package/framework/scripts/say.py +236 -0
- package/framework/scripts/showcase-video-recorder/ffmpeg-builder.js +167 -0
- package/framework/scripts/showcase-video-recorder/playwright-helpers.js +216 -0
- package/framework/scripts/speak.py +55 -0
- package/framework/scripts/speak.sh +18 -0
- package/framework/scripts/status.sh +138 -0
- package/framework/scripts/sync-to-framework.sh +65 -0
- package/framework/scripts/voice-hotkey.py +227 -0
- package/framework/scripts/voice-input.sh +51 -0
- package/framework/skills/animate/SKILL.md +202 -0
- package/framework/skills/bolder/SKILL.md +144 -0
- package/framework/skills/browser-qa/SKILL.md +536 -0
- package/framework/skills/clarify/SKILL.md +179 -0
- package/framework/skills/colorize/SKILL.md +170 -0
- package/framework/skills/critique/SKILL.md +126 -0
- package/framework/skills/deep-research/SKILL.md +271 -0
- package/framework/skills/delight/SKILL.md +329 -0
- package/framework/skills/deploy/SKILL.md +261 -0
- package/framework/skills/deploy-verify/SKILL.md +377 -0
- package/framework/skills/deploy-verify/scripts/canary-check.sh +206 -0
- package/framework/skills/deploy-verify/scripts/check-console-errors.js +147 -0
- package/framework/skills/deploy-verify/scripts/check-cwv.js +139 -0
- package/framework/skills/deploy-verify/scripts/project-detect.sh +84 -0
- package/framework/skills/deploy-verify/scripts/verify.sh +548 -0
- package/framework/skills/design-quieter/SKILL.md +130 -0
- package/framework/skills/distill/SKILL.md +149 -0
- package/framework/skills/docs-lookup/SKILL.md +78 -0
- package/framework/skills/fcm-notifications/SKILL.md +125 -0
- package/framework/skills/financial-ledger/SKILL.md +1039 -0
- package/framework/skills/frontend-master/NOTICE.md +4 -0
- package/framework/skills/frontend-master/SKILL.md +127 -0
- package/framework/skills/frontend-master/reference/color-and-contrast.md +132 -0
- package/framework/skills/frontend-master/reference/interaction-design.md +123 -0
- package/framework/skills/frontend-master/reference/motion-design.md +99 -0
- package/framework/skills/frontend-master/reference/responsive-design.md +114 -0
- package/framework/skills/frontend-master/reference/spatial-design.md +100 -0
- package/framework/skills/frontend-master/reference/typography.md +131 -0
- package/framework/skills/frontend-master/reference/ux-writing.md +107 -0
- package/framework/skills/harden/SKILL.md +357 -0
- package/framework/skills/i18n-rtl/SKILL.md +752 -0
- package/framework/skills/learn/SKILL.md +71 -0
- package/framework/skills/memory/SKILL.md +50 -0
- package/framework/skills/mobile-expo/SKILL.md +864 -0
- package/framework/skills/mobile-expo/references/store-checklist.md +550 -0
- package/framework/skills/nestjs-backend/README.md +73 -0
- package/framework/skills/nestjs-backend/SKILL.md +446 -0
- package/framework/skills/nestjs-backend/references/templates.md +1173 -0
- package/framework/skills/normalize/SKILL.md +79 -0
- package/framework/skills/onboard/SKILL.md +242 -0
- package/framework/skills/polish/SKILL.md +209 -0
- package/framework/skills/pr/SKILL.md +66 -0
- package/framework/skills/qualia/SKILL.md +153 -0
- package/framework/skills/qualia-add-todo/SKILL.md +68 -0
- package/framework/skills/qualia-audit-milestone/SKILL.md +92 -0
- package/framework/skills/qualia-check-todos/SKILL.md +55 -0
- package/framework/skills/qualia-complete-milestone/SKILL.md +108 -0
- package/framework/skills/qualia-debug/SKILL.md +149 -0
- package/framework/skills/qualia-design/SKILL.md +203 -0
- package/framework/skills/qualia-discuss-phase/SKILL.md +72 -0
- package/framework/skills/qualia-execute-phase/SKILL.md +86 -0
- package/framework/skills/qualia-help/SKILL.md +67 -0
- package/framework/skills/qualia-idk/SKILL.md +352 -0
- package/framework/skills/qualia-list-phase-assumptions/SKILL.md +67 -0
- package/framework/skills/qualia-new-milestone/SKILL.md +72 -0
- package/framework/skills/qualia-new-project/SKILL.md +92 -0
- package/framework/skills/qualia-optimize/SKILL.md +417 -0
- package/framework/skills/qualia-pause-work/SKILL.md +96 -0
- package/framework/skills/qualia-plan-milestone-gaps/SKILL.md +57 -0
- package/framework/skills/qualia-plan-phase/SKILL.md +101 -0
- package/framework/skills/qualia-progress/SKILL.md +53 -0
- package/framework/skills/qualia-quick/SKILL.md +89 -0
- package/framework/skills/qualia-research-phase/SKILL.md +88 -0
- package/framework/skills/qualia-resume-work/SKILL.md +62 -0
- package/framework/skills/qualia-review/SKILL.md +263 -0
- package/framework/skills/qualia-start/SKILL.md +182 -0
- package/framework/skills/qualia-verify-work/SKILL.md +105 -0
- package/framework/skills/qualia-workflow/SKILL.md +130 -0
- package/framework/skills/rag/SKILL.md +750 -0
- package/framework/skills/responsive/SKILL.md +231 -0
- package/framework/skills/retro/SKILL.md +284 -0
- package/framework/skills/sakani-conventions/SKILL.md +136 -0
- package/framework/skills/sakani-conventions/evals/evals.json +23 -0
- package/framework/skills/sakani-conventions/references/entities.md +365 -0
- package/framework/skills/sakani-conventions/references/error-codes.md +95 -0
- package/framework/skills/seo-master/SKILL.md +490 -0
- package/framework/skills/seo-master/references/checklist.md +199 -0
- package/framework/skills/seo-master/references/structured-data.md +609 -0
- package/framework/skills/ship/SKILL.md +202 -0
- package/framework/skills/stack-researcher/SKILL.md +215 -0
- package/framework/skills/status/SKILL.md +154 -0
- package/framework/skills/status/scripts/health-check.sh +562 -0
- package/framework/skills/subscription-payments/SKILL.md +250 -0
- package/framework/skills/supabase/SKILL.md +973 -0
- package/framework/skills/supabase/references/templates.md +159 -0
- package/framework/skills/team/SKILL.md +67 -0
- package/framework/skills/test-runner/SKILL.md +202 -0
- package/framework/skills/voice-agent/SKILL.md +407 -0
- package/framework/skills/zoho-workflow/SKILL.md +51 -0
- package/framework/statusline-command.sh +117 -0
- package/package.json +24 -0
- package/profiles/fawzi.json +16 -0
- package/profiles/hasan.json +16 -0
- package/profiles/moayad.json +16 -0
- package/templates/CLAUDE-owner.md +52 -0
- package/templates/CLAUDE.md.hbs +58 -0
- package/templates/env.claude.template +12 -0
- package/templates/settings.json +141 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Toggle between full Claude Code config and lean Cowork config
|
|
3
|
+
# Usage: cowork-mode.sh on|off|status
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
CLAUDE_DIR="$HOME/.claude"
|
|
8
|
+
CONFIG="$CLAUDE_DIR/settings.json"
|
|
9
|
+
SKILLS_DIR="$CLAUDE_DIR/skills"
|
|
10
|
+
ARCHIVE_DIR="$CLAUDE_DIR/skills-archive"
|
|
11
|
+
BACKUP="$CLAUDE_DIR/.settings-full.json"
|
|
12
|
+
LOCK="$CLAUDE_DIR/.cowork-mode"
|
|
13
|
+
|
|
14
|
+
# Skills to archive in Cowork (heavy/project-specific)
|
|
15
|
+
HEAVY_SKILLS=(
|
|
16
|
+
sakani-conventions
|
|
17
|
+
nestjs-backend
|
|
18
|
+
mobile-expo
|
|
19
|
+
financial-ledger
|
|
20
|
+
i18n-rtl
|
|
21
|
+
fcm-notifications
|
|
22
|
+
subscription-payments
|
|
23
|
+
voice-agent
|
|
24
|
+
zoho-workflow
|
|
25
|
+
skill-creator
|
|
26
|
+
deep-research
|
|
27
|
+
test-runner
|
|
28
|
+
git-worktree
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Plugins to disable in Cowork (keep: typescript-lsp, firecrawl, playwright)
|
|
32
|
+
DISABLE_PLUGINS=(
|
|
33
|
+
"code-review@claude-plugins-official"
|
|
34
|
+
"skill-creator@claude-plugins-official"
|
|
35
|
+
"claude-md-management@claude-plugins-official"
|
|
36
|
+
"code-simplifier@claude-plugins-official"
|
|
37
|
+
"cowork-plugin-management@knowledge-work-plugins"
|
|
38
|
+
"supabase@claude-plugins-official"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# MCP servers to disable in Cowork (keep: supabase, firecrawl)
|
|
42
|
+
DISABLE_MCP=(
|
|
43
|
+
"zohomcp"
|
|
44
|
+
"sentry"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
on() {
|
|
48
|
+
if [ -f "$LOCK" ]; then
|
|
49
|
+
echo "Already in Cowork mode. Run 'cowork-mode.sh off' first."
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
echo "Switching to Cowork mode (lean config)..."
|
|
54
|
+
|
|
55
|
+
# Backup full settings
|
|
56
|
+
cp "$CONFIG" "$BACKUP"
|
|
57
|
+
|
|
58
|
+
# Archive heavy skills
|
|
59
|
+
mkdir -p "$ARCHIVE_DIR"
|
|
60
|
+
local count=0
|
|
61
|
+
for skill in "${HEAVY_SKILLS[@]}"; do
|
|
62
|
+
if [ -d "$SKILLS_DIR/$skill" ]; then
|
|
63
|
+
mv "$SKILLS_DIR/$skill" "$ARCHIVE_DIR/$skill"
|
|
64
|
+
count=$((count + 1))
|
|
65
|
+
fi
|
|
66
|
+
done
|
|
67
|
+
|
|
68
|
+
# Disable plugins and MCP servers
|
|
69
|
+
node -e "
|
|
70
|
+
const fs = require('fs');
|
|
71
|
+
const config = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
|
|
72
|
+
|
|
73
|
+
// Disable plugins
|
|
74
|
+
const disablePlugins = process.argv.slice(3);
|
|
75
|
+
for (const p of disablePlugins) {
|
|
76
|
+
if (config.enabledPlugins && config.enabledPlugins[p] !== undefined) {
|
|
77
|
+
config.enabledPlugins[p] = false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Filter MCP servers
|
|
82
|
+
const disableMcp = process.argv[2].split(',').filter(Boolean);
|
|
83
|
+
if (config.enabledMcpjsonServers) {
|
|
84
|
+
config.enabledMcpjsonServers = config.enabledMcpjsonServers.filter(s => !disableMcp.includes(s));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fs.writeFileSync(process.argv[1], JSON.stringify(config, null, 2));
|
|
88
|
+
" "$CONFIG" "$(IFS=,; echo "${DISABLE_MCP[*]}")" "${DISABLE_PLUGINS[@]}"
|
|
89
|
+
|
|
90
|
+
# Mark as active
|
|
91
|
+
date > "$LOCK"
|
|
92
|
+
|
|
93
|
+
echo "Done. Archived $count skills, disabled ${#DISABLE_PLUGINS[@]} plugins, ${#DISABLE_MCP[@]} MCP servers."
|
|
94
|
+
echo ""
|
|
95
|
+
echo "Now restart Claude desktop app to pick up changes."
|
|
96
|
+
echo "Run 'cowork off' when done to restore full config."
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
off() {
|
|
100
|
+
if [ ! -f "$LOCK" ]; then
|
|
101
|
+
echo "Not in Cowork mode. Nothing to restore."
|
|
102
|
+
exit 1
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
echo "Restoring full Claude Code config..."
|
|
106
|
+
|
|
107
|
+
# Restore skills from archive
|
|
108
|
+
if [ -d "$ARCHIVE_DIR" ]; then
|
|
109
|
+
for skill in "$ARCHIVE_DIR"/*/; do
|
|
110
|
+
[ -d "$skill" ] || continue
|
|
111
|
+
name=$(basename "$skill")
|
|
112
|
+
mv "$skill" "$SKILLS_DIR/$name"
|
|
113
|
+
done
|
|
114
|
+
rmdir "$ARCHIVE_DIR" 2>/dev/null || true
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# Restore full settings
|
|
118
|
+
if [ -f "$BACKUP" ]; then
|
|
119
|
+
mv "$BACKUP" "$CONFIG"
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
rm "$LOCK"
|
|
123
|
+
echo "Done. Full config restored."
|
|
124
|
+
echo "Restart Claude desktop app to pick up changes."
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
status() {
|
|
128
|
+
if [ -f "$LOCK" ]; then
|
|
129
|
+
echo "Cowork mode: ON (since $(cat "$LOCK"))"
|
|
130
|
+
[ -d "$ARCHIVE_DIR" ] && echo "Archived skills: $(ls "$ARCHIVE_DIR" 2>/dev/null | wc -l)"
|
|
131
|
+
else
|
|
132
|
+
echo "Cowork mode: OFF (full config active)"
|
|
133
|
+
fi
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case "${1:-status}" in
|
|
137
|
+
on) on ;;
|
|
138
|
+
off) off ;;
|
|
139
|
+
status) status ;;
|
|
140
|
+
*) echo "Usage: cowork-mode.sh [on|off|status]" ;;
|
|
141
|
+
esac
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Generate CLAUDE.md for all Qualia projects that don't have one
|
|
3
|
+
# Run from your main dev machine: bash ~/.claude/scripts/generate-project-claude-md.sh
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
shopt -s nullglob
|
|
7
|
+
|
|
8
|
+
PROJECTS_BASE="$HOME/Projects"
|
|
9
|
+
# Note: Projects confirmed at ~/Projects/ (not ~/Desktop/Projects/)
|
|
10
|
+
DEPLOYMENT_MAP="$HOME/.claude/knowledge/deployment-map.md"
|
|
11
|
+
CLIENT_PREFS="$HOME/.claude/knowledge/client-prefs.md"
|
|
12
|
+
|
|
13
|
+
GREEN='\033[0;32m'
|
|
14
|
+
YELLOW='\033[1;33m'
|
|
15
|
+
CYAN='\033[0;36m'
|
|
16
|
+
NC='\033[0m'
|
|
17
|
+
|
|
18
|
+
GENERATED=0
|
|
19
|
+
SKIPPED=0
|
|
20
|
+
|
|
21
|
+
# Supabase refs from deployment-map.md
|
|
22
|
+
declare -A SUPABASE_REFS=(
|
|
23
|
+
["aibossbrainz"]="esymbjpzjjkffpfqukxw"
|
|
24
|
+
["aquador"]="hznpuxplqgszbacxzbhv"
|
|
25
|
+
["dababneh"]="toovladeybmlyzblqtbl"
|
|
26
|
+
["faris"]="lrgwxcqqnhxvvrruepcv"
|
|
27
|
+
["Hammah"]="ctcikdcjhxxqogwvtzrc"
|
|
28
|
+
["maud"]="vspyxscitcuwnrhchskl"
|
|
29
|
+
["mpm"]="llherorsfgbdyqkrrlpc"
|
|
30
|
+
["sofiatesting"]="vceeheaxcrhmpqueudqx"
|
|
31
|
+
["timo"]="qgaqvkmqterlljbegniu"
|
|
32
|
+
["under"]="vvppyijxpgeeijiozlve"
|
|
33
|
+
["vero"]="zskfdlqyzhkzefafqkpx"
|
|
34
|
+
["Zaid"]="uoqiwidqlsoamtugioik"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
generate_claude_md() {
|
|
38
|
+
local dir="$1"
|
|
39
|
+
local name=$(basename "$dir")
|
|
40
|
+
local category="$2"
|
|
41
|
+
|
|
42
|
+
# Skip if CLAUDE.md already exists
|
|
43
|
+
if [ -f "$dir/CLAUDE.md" ]; then
|
|
44
|
+
printf "${YELLOW}SKIP${NC} %s (already has CLAUDE.md)\n" "$name"
|
|
45
|
+
SKIPPED=$((SKIPPED + 1))
|
|
46
|
+
return
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Skip if not a real project (no package.json and no src/)
|
|
50
|
+
if [ ! -f "$dir/package.json" ] && [ ! -d "$dir/src" ] && [ ! -f "$dir/requirements.txt" ]; then
|
|
51
|
+
return
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Detect stack from package.json
|
|
55
|
+
local stack=""
|
|
56
|
+
local scripts=""
|
|
57
|
+
local nextjs_version=""
|
|
58
|
+
if [ -f "$dir/package.json" ]; then
|
|
59
|
+
nextjs_version=$(node -e "try{const p=require('$dir/package.json');console.log(p.dependencies?.next||p.devDependencies?.next||'')}catch(e){}" 2>/dev/null)
|
|
60
|
+
local has_supabase=$(node -e "try{const p=require('$dir/package.json');console.log(p.dependencies?.['@supabase/supabase-js']?'yes':'')}catch(e){}" 2>/dev/null)
|
|
61
|
+
local has_tailwind=$(node -e "try{const p=require('$dir/package.json');console.log(p.devDependencies?.tailwindcss||p.dependencies?.tailwindcss?'yes':'')}catch(e){}" 2>/dev/null)
|
|
62
|
+
|
|
63
|
+
stack="Next.js${nextjs_version:+ $nextjs_version}, React, TypeScript"
|
|
64
|
+
[ "$has_supabase" = "yes" ] && stack="$stack, Supabase"
|
|
65
|
+
[ "$has_tailwind" = "yes" ] && stack="$stack, Tailwind"
|
|
66
|
+
|
|
67
|
+
# Get npm scripts
|
|
68
|
+
scripts=$(node -e "try{const p=require('$dir/package.json');const s=p.scripts||{};Object.keys(s).forEach(k=>console.log('- \`npm run '+k+'\` — '+s[k]))}catch(e){}" 2>/dev/null)
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Python project detection
|
|
72
|
+
if [ -f "$dir/requirements.txt" ]; then
|
|
73
|
+
stack="Python"
|
|
74
|
+
[ -f "$dir/pyproject.toml" ] && stack="$stack (pyproject)"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# Supabase ref
|
|
78
|
+
local supa_ref="${SUPABASE_REFS[$name]:-}"
|
|
79
|
+
local supa_line=""
|
|
80
|
+
[ -n "$supa_ref" ] && supa_line="- **Supabase:** \`$supa_ref\`"
|
|
81
|
+
|
|
82
|
+
# Detect deploy target
|
|
83
|
+
local deploy_target="Vercel"
|
|
84
|
+
[ -f "$dir/wrangler.toml" ] || [ -f "$dir/wrangler.jsonc" ] && deploy_target="Cloudflare Workers"
|
|
85
|
+
|
|
86
|
+
# Detect domain from vercel.json
|
|
87
|
+
local domain=""
|
|
88
|
+
if [ -f "$dir/vercel.json" ]; then
|
|
89
|
+
domain=$(node -e "try{const v=require('$dir/vercel.json');console.log((v.alias||[])[0]||'')}catch(e){}" 2>/dev/null)
|
|
90
|
+
fi
|
|
91
|
+
[ -z "$domain" ] && domain="$name.vercel.app"
|
|
92
|
+
|
|
93
|
+
# Build the CLAUDE.md
|
|
94
|
+
cat > "$dir/CLAUDE.md" << CLAUDEMD
|
|
95
|
+
# $name
|
|
96
|
+
|
|
97
|
+
## Purpose
|
|
98
|
+
$category project. <!-- UPDATE: Add one-line description -->
|
|
99
|
+
|
|
100
|
+
## Stack
|
|
101
|
+
$stack
|
|
102
|
+
|
|
103
|
+
## Config
|
|
104
|
+
- **Deploy:** $deploy_target at $domain
|
|
105
|
+
${supa_line}
|
|
106
|
+
|
|
107
|
+
## Commands
|
|
108
|
+
${scripts:-"- \`npm run dev\` — local development
|
|
109
|
+
- \`npm run build\` — production build"}
|
|
110
|
+
|
|
111
|
+
## Notes
|
|
112
|
+
<!-- Add project-specific gotchas, client preferences, known issues -->
|
|
113
|
+
CLAUDEMD
|
|
114
|
+
|
|
115
|
+
printf "${GREEN}CREATED${NC} %s/CLAUDE.md\n" "$name"
|
|
116
|
+
GENERATED=$((GENERATED + 1))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
echo ""
|
|
120
|
+
printf "${CYAN}Scanning projects...${NC}\n"
|
|
121
|
+
echo ""
|
|
122
|
+
|
|
123
|
+
# Live Projects
|
|
124
|
+
if [ -d "$PROJECTS_BASE/Live-Projects" ]; then
|
|
125
|
+
for dir in "$PROJECTS_BASE/Live-Projects"/*/; do
|
|
126
|
+
[ -d "$dir" ] && generate_claude_md "$dir" "Client website"
|
|
127
|
+
done
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# AI Agents
|
|
131
|
+
if [ -d "$PROJECTS_BASE/aiagents" ]; then
|
|
132
|
+
for dir in "$PROJECTS_BASE/aiagents"/*/; do
|
|
133
|
+
[ -d "$dir" ] && generate_claude_md "$dir" "AI agent"
|
|
134
|
+
done
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# Voice
|
|
138
|
+
if [ -d "$PROJECTS_BASE/voice" ]; then
|
|
139
|
+
for dir in "$PROJECTS_BASE/voice"/*/; do
|
|
140
|
+
[ -d "$dir" ] && generate_claude_md "$dir" "Voice agent"
|
|
141
|
+
done
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
# Websites
|
|
145
|
+
if [ -d "$PROJECTS_BASE/websites" ]; then
|
|
146
|
+
for dir in "$PROJECTS_BASE/websites"/*/; do
|
|
147
|
+
[ -d "$dir" ] && generate_claude_md "$dir" "Internal tool"
|
|
148
|
+
done
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
echo ""
|
|
152
|
+
printf "${CYAN}Done.${NC} Generated: ${GREEN}$GENERATED${NC} | Skipped: ${YELLOW}$SKIPPED${NC}\n"
|
|
153
|
+
printf "Review the generated files and fill in the <!-- UPDATE --> placeholders.\n"
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Simple load testing for webhook endpoints
|
|
4
|
+
* Usage: node load-test-webhook.js <webhook-url> [concurrency=10] [total=100]
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const http = require('http');
|
|
9
|
+
|
|
10
|
+
async function loadTest(webhookUrl, concurrency = 10, totalRequests = 100) {
|
|
11
|
+
console.log(`🔥 Load testing ${webhookUrl}`);
|
|
12
|
+
console.log(`📊 Concurrency: ${concurrency}, Total: ${totalRequests}\n`);
|
|
13
|
+
|
|
14
|
+
const results = {
|
|
15
|
+
success: 0,
|
|
16
|
+
failed: 0,
|
|
17
|
+
times: [],
|
|
18
|
+
errors: []
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const batches = Math.ceil(totalRequests / concurrency);
|
|
22
|
+
|
|
23
|
+
for (let batch = 0; batch < batches; batch++) {
|
|
24
|
+
const batchSize = Math.min(concurrency, totalRequests - batch * concurrency);
|
|
25
|
+
const promises = [];
|
|
26
|
+
|
|
27
|
+
console.log(`🚀 Batch ${batch + 1}/${batches} (${batchSize} requests)`);
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < batchSize; i++) {
|
|
30
|
+
promises.push(makeRequest(webhookUrl, batch * concurrency + i + 1));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const batchResults = await Promise.allSettled(promises);
|
|
34
|
+
|
|
35
|
+
for (const result of batchResults) {
|
|
36
|
+
if (result.status === 'fulfilled') {
|
|
37
|
+
results.success++;
|
|
38
|
+
results.times.push(result.value.duration);
|
|
39
|
+
} else {
|
|
40
|
+
results.failed++;
|
|
41
|
+
results.errors.push(result.reason.message);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Brief pause between batches
|
|
46
|
+
if (batch < batches - 1) {
|
|
47
|
+
await sleep(100);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
printResults(results, totalRequests);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function makeRequest(url, requestId) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const startTime = Date.now();
|
|
57
|
+
const isHttps = url.startsWith('https');
|
|
58
|
+
const client = isHttps ? https : http;
|
|
59
|
+
|
|
60
|
+
// Sample WhatsApp webhook payload
|
|
61
|
+
const payload = JSON.stringify({
|
|
62
|
+
object: "whatsapp_business_account",
|
|
63
|
+
entry: [{
|
|
64
|
+
id: "123456789",
|
|
65
|
+
changes: [{
|
|
66
|
+
field: "messages",
|
|
67
|
+
value: {
|
|
68
|
+
messaging_product: "whatsapp",
|
|
69
|
+
metadata: { display_phone_number: "15551234567" },
|
|
70
|
+
contacts: [{ profile: { name: "Load Test User" }, wa_id: `loadtest${requestId}` }],
|
|
71
|
+
messages: [{
|
|
72
|
+
id: `loadtest_${requestId}_${Date.now()}`,
|
|
73
|
+
from: `loadtest${requestId}`,
|
|
74
|
+
timestamp: Math.floor(Date.now() / 1000).toString(),
|
|
75
|
+
type: "text",
|
|
76
|
+
text: { body: `Load test message ${requestId}` }
|
|
77
|
+
}]
|
|
78
|
+
}
|
|
79
|
+
}]
|
|
80
|
+
}]
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const options = {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: {
|
|
86
|
+
'Content-Type': 'application/json',
|
|
87
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
88
|
+
'User-Agent': 'WhatsApp/2.0 LoadTest'
|
|
89
|
+
},
|
|
90
|
+
timeout: 10000
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const req = client.request(url, options, (res) => {
|
|
94
|
+
let data = '';
|
|
95
|
+
res.on('data', chunk => data += chunk);
|
|
96
|
+
res.on('end', () => {
|
|
97
|
+
const duration = Date.now() - startTime;
|
|
98
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
99
|
+
resolve({ duration, statusCode: res.statusCode });
|
|
100
|
+
} else {
|
|
101
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
req.on('error', (error) => {
|
|
107
|
+
reject(new Error(`Request failed: ${error.message}`));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
req.on('timeout', () => {
|
|
111
|
+
req.destroy();
|
|
112
|
+
reject(new Error('Request timeout'));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
req.write(payload);
|
|
116
|
+
req.end();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function printResults(results, totalRequests) {
|
|
121
|
+
console.log('\n📈 RESULTS');
|
|
122
|
+
console.log('=' .repeat(50));
|
|
123
|
+
|
|
124
|
+
const successRate = (results.success / totalRequests * 100).toFixed(1);
|
|
125
|
+
const avgTime = results.times.length ? (results.times.reduce((a, b) => a + b, 0) / results.times.length).toFixed(0) : 0;
|
|
126
|
+
const p95Time = results.times.length ? results.times.sort((a, b) => a - b)[Math.floor(results.times.length * 0.95)] : 0;
|
|
127
|
+
|
|
128
|
+
console.log(`✅ Success: ${results.success}/${totalRequests} (${successRate}%)`);
|
|
129
|
+
console.log(`❌ Failed: ${results.failed}/${totalRequests}`);
|
|
130
|
+
console.log(`⚡ Avg response: ${avgTime}ms`);
|
|
131
|
+
console.log(`📊 95th percentile: ${p95Time}ms`);
|
|
132
|
+
|
|
133
|
+
if (results.errors.length > 0) {
|
|
134
|
+
console.log('\n🚨 ERRORS');
|
|
135
|
+
const errorCounts = {};
|
|
136
|
+
results.errors.forEach(error => {
|
|
137
|
+
errorCounts[error] = (errorCounts[error] || 0) + 1;
|
|
138
|
+
});
|
|
139
|
+
Object.entries(errorCounts).forEach(([error, count]) => {
|
|
140
|
+
console.log(` ${count}x ${error}`);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Production readiness assessment
|
|
145
|
+
console.log('\n🎯 PRODUCTION READINESS');
|
|
146
|
+
if (successRate >= 95 && avgTime < 5000) {
|
|
147
|
+
console.log('✅ READY FOR PRODUCTION');
|
|
148
|
+
} else {
|
|
149
|
+
console.log('❌ NOT READY FOR PRODUCTION');
|
|
150
|
+
if (successRate < 95) console.log(` - Success rate too low (${successRate}% < 95%)`);
|
|
151
|
+
if (avgTime >= 5000) console.log(` - Response too slow (${avgTime}ms ≥ 5000ms)`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function sleep(ms) {
|
|
156
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// CLI execution
|
|
160
|
+
if (require.main === module) {
|
|
161
|
+
const [webhookUrl, concurrency = 10, total = 100] = process.argv.slice(2);
|
|
162
|
+
|
|
163
|
+
if (!webhookUrl) {
|
|
164
|
+
console.error('Usage: node load-test-webhook.js <webhook-url> [concurrency=10] [total=100]');
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
loadTest(webhookUrl, parseInt(concurrency), parseInt(total))
|
|
169
|
+
.catch(console.error);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { loadTest };
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Smart TTS wrapper with ElevenLabs best practices.
|
|
4
|
+
- Filters code, errors, and system noise
|
|
5
|
+
- Normalizes text for natural speech (numbers, URLs, abbreviations)
|
|
6
|
+
- Only speaks natural conversation
|
|
7
|
+
|
|
8
|
+
Usage: echo "message" | say.py OR say.py "message"
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
import tempfile
|
|
16
|
+
import warnings
|
|
17
|
+
|
|
18
|
+
warnings.filterwarnings("ignore")
|
|
19
|
+
|
|
20
|
+
# ============================================================================
|
|
21
|
+
# CONFIGURATION
|
|
22
|
+
# ============================================================================
|
|
23
|
+
|
|
24
|
+
VOLUME = 50 # 0-100, set to 0 to mute completely
|
|
25
|
+
MAX_LENGTH = 500 # Max characters to speak
|
|
26
|
+
MIN_LENGTH = 2 # Min characters
|
|
27
|
+
MAX_NEWLINES = 4 # Max newlines allowed
|
|
28
|
+
|
|
29
|
+
# Patterns that indicate non-conversational content (reject if matched)
|
|
30
|
+
REJECT_PATTERNS = [
|
|
31
|
+
r'```', # Code blocks
|
|
32
|
+
r'^\s*import\s+', # Import statements
|
|
33
|
+
r'^\s*export\s+', # Export statements
|
|
34
|
+
r'^\s*const\s+\w+\s*=', # JS variable declarations
|
|
35
|
+
r'^\s*let\s+\w+\s*=', # JS let declarations
|
|
36
|
+
r'^\s*var\s+\w+\s*=', # JS var declarations
|
|
37
|
+
r'^\s*function\s+\w+\s*\(', # JS function definitions
|
|
38
|
+
r'^\s*def\s+\w+\s*\(', # Python functions
|
|
39
|
+
r'^\s*class\s+\w+', # Class definitions
|
|
40
|
+
r'^\s*async\s+', # Async declarations
|
|
41
|
+
r'^\s*await\s+', # Await expressions
|
|
42
|
+
r'\{\s*\n', # Opening braces with newline (code)
|
|
43
|
+
r'^\s*<[a-zA-Z]', # HTML/JSX tags
|
|
44
|
+
r'^\s*\d+[→│\|]\s*', # Line number prefixes (file reads)
|
|
45
|
+
r'node_modules', # Path noise
|
|
46
|
+
r'\.tsx?:\d+', # File:line references (file.ts:123)
|
|
47
|
+
r'\.jsx?:\d+', # File:line references (file.js:123)
|
|
48
|
+
r'\.py:\d+', # Python file:line references
|
|
49
|
+
r'Error:|error:|ERROR|ERR!', # Error messages
|
|
50
|
+
r'Warning:|warning:|WARN', # Warnings
|
|
51
|
+
r'^\s*at\s+\w+\s*\(', # Stack traces
|
|
52
|
+
r'Traceback \(most recent', # Python tracebacks
|
|
53
|
+
r'^\s*\$\s+', # Shell prompts
|
|
54
|
+
r'^\s*>\s+', # REPL prompts
|
|
55
|
+
r'npm\s+(ERR|WARN|notice)', # npm messages
|
|
56
|
+
r'^\s*[-*]\s+`', # Markdown list with code
|
|
57
|
+
r'^\s*\d+\.\s+`', # Numbered list with code
|
|
58
|
+
r'\|\s*\w+\s*\|', # Markdown tables
|
|
59
|
+
r'={3,}|_{3,}|-{3,}', # Markdown separators
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
# ============================================================================
|
|
63
|
+
# TEXT NORMALIZATION (ElevenLabs best practices)
|
|
64
|
+
# ============================================================================
|
|
65
|
+
|
|
66
|
+
def normalize_numbers(text: str) -> str:
|
|
67
|
+
"""Convert numbers to speakable format."""
|
|
68
|
+
# Simple number to words for common cases
|
|
69
|
+
number_words = {
|
|
70
|
+
'0': 'zero', '1': 'one', '2': 'two', '3': 'three', '4': 'four',
|
|
71
|
+
'5': 'five', '6': 'six', '7': 'seven', '8': 'eight', '9': 'nine',
|
|
72
|
+
'10': 'ten', '11': 'eleven', '12': 'twelve'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Handle percentages: 50% -> fifty percent
|
|
76
|
+
text = re.sub(r'(\d+)%', lambda m: f"{m.group(1)} percent", text)
|
|
77
|
+
|
|
78
|
+
# Handle simple standalone small numbers
|
|
79
|
+
for num, word in number_words.items():
|
|
80
|
+
text = re.sub(rf'\b{num}\b', word, text)
|
|
81
|
+
|
|
82
|
+
return text
|
|
83
|
+
|
|
84
|
+
def normalize_urls(text: str) -> str:
|
|
85
|
+
"""Convert URLs to speakable format or remove."""
|
|
86
|
+
# Remove full URLs - they're not speakable
|
|
87
|
+
text = re.sub(r'https?://[^\s]+', '', text)
|
|
88
|
+
# Simplify file paths
|
|
89
|
+
text = re.sub(r'[~/][\w/.-]+\.(py|sh|js|ts|json|md)', 'the script', text)
|
|
90
|
+
return text
|
|
91
|
+
|
|
92
|
+
def expand_abbreviations(text: str) -> str:
|
|
93
|
+
"""Expand common abbreviations for clearer speech."""
|
|
94
|
+
abbreviations = {
|
|
95
|
+
r'\bTTS\b': 'text to speech',
|
|
96
|
+
r'\bAPI\b': 'A P I',
|
|
97
|
+
r'\bURL\b': 'U R L',
|
|
98
|
+
r'\bUI\b': 'U I',
|
|
99
|
+
r'\bAI\b': 'A I',
|
|
100
|
+
r'\bOK\b': 'okay',
|
|
101
|
+
r'\bok\b': 'okay',
|
|
102
|
+
r'\bconfig\b': 'configuration',
|
|
103
|
+
r'\brepo\b': 'repository',
|
|
104
|
+
r'\binfo\b': 'information',
|
|
105
|
+
r'\bdocs?\b': 'documentation',
|
|
106
|
+
r'\bauth\b': 'authentication',
|
|
107
|
+
r'\benv\b': 'environment',
|
|
108
|
+
}
|
|
109
|
+
for pattern, replacement in abbreviations.items():
|
|
110
|
+
text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
|
|
111
|
+
return text
|
|
112
|
+
|
|
113
|
+
def clean_for_speech(text: str) -> str:
|
|
114
|
+
"""Clean and normalize text for natural speech."""
|
|
115
|
+
# Remove markdown formatting
|
|
116
|
+
text = re.sub(r'\*\*([^*]+)\*\*', r'\1', text) # Bold
|
|
117
|
+
text = re.sub(r'\*([^*]+)\*', r'\1', text) # Italic
|
|
118
|
+
text = re.sub(r'`([^`]+)`', r'\1', text) # Inline code
|
|
119
|
+
text = re.sub(r'#{1,6}\s*', '', text) # Headers
|
|
120
|
+
|
|
121
|
+
# Remove special characters that don't speak well
|
|
122
|
+
text = re.sub(r'[→│\|•]', ' ', text)
|
|
123
|
+
text = re.sub(r'[\[\]]', '', text) # Square brackets
|
|
124
|
+
|
|
125
|
+
# Normalize whitespace
|
|
126
|
+
text = re.sub(r'\s+', ' ', text)
|
|
127
|
+
|
|
128
|
+
# Apply normalizations
|
|
129
|
+
text = normalize_urls(text)
|
|
130
|
+
text = expand_abbreviations(text)
|
|
131
|
+
text = normalize_numbers(text)
|
|
132
|
+
|
|
133
|
+
return text.strip()
|
|
134
|
+
|
|
135
|
+
# ============================================================================
|
|
136
|
+
# FILTERING
|
|
137
|
+
# ============================================================================
|
|
138
|
+
|
|
139
|
+
def should_speak(text: str) -> bool:
|
|
140
|
+
"""Return True if text is natural conversation worth speaking."""
|
|
141
|
+
text = text.strip()
|
|
142
|
+
|
|
143
|
+
# Length checks
|
|
144
|
+
if len(text) < MIN_LENGTH or len(text) > MAX_LENGTH:
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
# Too many newlines = probably not conversation
|
|
148
|
+
if text.count('\n') > MAX_NEWLINES:
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
# Check reject patterns
|
|
152
|
+
for pattern in REJECT_PATTERNS:
|
|
153
|
+
if re.search(pattern, text, re.MULTILINE | re.IGNORECASE):
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
# ============================================================================
|
|
159
|
+
# SPEECH OUTPUT
|
|
160
|
+
# ============================================================================
|
|
161
|
+
|
|
162
|
+
def speak(text: str):
|
|
163
|
+
"""Speak text via ElevenLabs with optimized settings."""
|
|
164
|
+
if VOLUME <= 0:
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
from elevenlabs import ElevenLabs
|
|
169
|
+
except ImportError:
|
|
170
|
+
print("elevenlabs not installed", file=sys.stderr)
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
api_key = os.environ.get("ELEVEN_API_KEY")
|
|
174
|
+
voice_id = os.environ.get("ELEVEN_VOICE_ID", "PB6BdkFkZLbI39GHdnbQ")
|
|
175
|
+
|
|
176
|
+
if not api_key:
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
client = ElevenLabs(api_key=api_key)
|
|
180
|
+
|
|
181
|
+
# Clean text for speech
|
|
182
|
+
clean_text = clean_for_speech(text)
|
|
183
|
+
|
|
184
|
+
if not clean_text or len(clean_text) < MIN_LENGTH:
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
audio = client.text_to_speech.convert(
|
|
189
|
+
voice_id=voice_id,
|
|
190
|
+
text=clean_text,
|
|
191
|
+
model_id="eleven_turbo_v2_5", # Fast, good quality
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
|
|
195
|
+
for chunk in audio:
|
|
196
|
+
f.write(chunk)
|
|
197
|
+
f.flush()
|
|
198
|
+
temp_path = f.name
|
|
199
|
+
|
|
200
|
+
# Play audio
|
|
201
|
+
try:
|
|
202
|
+
subprocess.run(
|
|
203
|
+
["mpv", "--no-video", "--really-quiet", "--volume=50", temp_path],
|
|
204
|
+
check=True,
|
|
205
|
+
capture_output=True
|
|
206
|
+
)
|
|
207
|
+
except FileNotFoundError:
|
|
208
|
+
try:
|
|
209
|
+
subprocess.run(
|
|
210
|
+
["ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", "-volume", "50", temp_path],
|
|
211
|
+
check=True,
|
|
212
|
+
capture_output=True
|
|
213
|
+
)
|
|
214
|
+
except FileNotFoundError:
|
|
215
|
+
print("No audio player found (mpv or ffplay)", file=sys.stderr)
|
|
216
|
+
|
|
217
|
+
os.unlink(temp_path)
|
|
218
|
+
|
|
219
|
+
except Exception as e:
|
|
220
|
+
# Silently fail - don't interrupt workflow
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
# ============================================================================
|
|
224
|
+
# MAIN
|
|
225
|
+
# ============================================================================
|
|
226
|
+
|
|
227
|
+
if __name__ == "__main__":
|
|
228
|
+
if len(sys.argv) > 1:
|
|
229
|
+
text = " ".join(sys.argv[1:])
|
|
230
|
+
else:
|
|
231
|
+
text = sys.stdin.read()
|
|
232
|
+
|
|
233
|
+
text = text.strip()
|
|
234
|
+
|
|
235
|
+
if should_speak(text):
|
|
236
|
+
speak(text)
|