Import Valley of the Commons website from votc repo

Full website with:
- Landing page, game, privacy policy
- Speakers and community partners sections
- API routes for waitlist, game chat, GitHub sharing
- Docker deployment with Express server
- Traefik labels for votc.jeffemmett.com

Replaces initial splash page with complete site.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-02 12:48:08 +00:00
parent 8fab13d1f5
commit 033b9bccd3
67 changed files with 6423 additions and 169 deletions

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
node_modules
.git
.gitignore
.env
.env.example
*.md
!internal_thought.md

27
.env.example Normal file
View File

@ -0,0 +1,27 @@
GOOGLE_SERVICE_ACCOUNT=your_service_account_json_here
GOOGLE_SHEET_ID=your_sheet_id_here
GOOGLE_SHEET_NAME=Waitlist
AI_GATEWAY_API_KEY=your_vercel_ai_gateway_key_here
GAME_MODEL=mistral/mistral-large-latest
# AI Gateway Configuration for Game Chat
# Vercel AI Gateway API key (get from Vercel dashboard)
AI_GATEWAY_API_KEY=your_vercel_ai_gateway_key_here
# Model selection (optional, defaults to mistral/mistral-large-latest)
# Format: provider/model-name
# Examples:
# mistral/mistral-large-latest
# anthropic/claude-3-5-sonnet-20241022
GAME_MODEL=mistral/mistral-large-latest
# GitHub Configuration for Idea Sharing
# GitHub Personal Access Token (fine-grained token with repo write access)
# Create at: https://github.com/settings/tokens
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# GitHub Repository Configuration (optional, defaults shown)
GITHUB_OWNER=understories
GITHUB_REPO=votc
GITHUB_BRANCH=main
GITHUB_PATH=build_game/ideas

49
.gitignore vendored
View File

@ -1,25 +1,44 @@
# Dependencies
node_modules/
refs/
# Build output
dist/
build/
# OS files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Environment files
.env
.env.local
.env.*.local
# IDE
# Editor files
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
*~
# Logs
*.log
npm-debug.log*
# Temporary files
*.tmp
*.temp
# Internal planning and research
refs/
# Google service account credentials (NEVER commit these!)
*-service-account*.json
service-account*.json
google-credentials*.json
credentials.json
# Environment variables for local testing
.env
.env.local
.env.*.local
# But allow .env.example to be committed
!.env.example
.vercel
.env*.local

View File

@ -1,12 +1,21 @@
FROM nginx:alpine
FROM node:20-alpine
# Copy static files
COPY index.html /usr/share/nginx/html/
COPY styles.css /usr/share/nginx/html/
WORKDIR /app
# Copy custom nginx config if needed
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copy package files
COPY package*.json ./
EXPOSE 80
# Install dependencies
RUN npm ci --only=production
CMD ["nginx", "-g", "daemon off;"]
# Install express for serving
RUN npm install express
# Copy application files
COPY . .
# Expose port
EXPOSE 3000
# Start server
CMD ["node", "server.js"]

47
ENGINEERING_GUIDELINES.md Normal file
View File

@ -0,0 +1,47 @@
# Engineering Guidelines
## Git & Version Control
### Never Force Commit
**CRITICAL RULE:** Never use `git add -f` or `git commit --force` to commit files that are intentionally ignored by `.gitignore`.
**Rationale:**
- Files in `.gitignore` are ignored for good reasons (security, privacy, internal planning)
- Force committing bypasses these protections
- If a file needs to be tracked, update `.gitignore` instead, don't force it
**What to do instead:**
1. If a file should be tracked: Remove it from `.gitignore` first
2. If a file should remain ignored: Keep it local-only or use a different location
3. If unsure: Ask before committing
**Example of what NOT to do:**
```bash
# ❌ NEVER DO THIS
git add -f refs/some-file.md
git commit -m "Add file"
```
**Example of what TO do:**
```bash
# ✅ DO THIS INSTEAD
# If file should be tracked:
# 1. Edit .gitignore to remove the pattern
# 2. Then commit normally
git add refs/some-file.md
git commit -m "Add file"
```
---
## General Principles
1. **Respect `.gitignore`** - Files are ignored for a reason
2. **Ask before force operations** - Force operations can be destructive
3. **Follow existing patterns** - Maintain consistency with project structure
4. **Security first** - Never commit secrets, API keys, or sensitive data
---
**Last Updated:** January 2025

213
README.md Normal file
View File

@ -0,0 +1,213 @@
# Valley of the Commons
Landing page and interactive game terminal for the Valley of the Commons project by Commons Hub.
## Overview
**Our valley is at a crossroads.** Once shaped by industry, it is now quietly deserting: businesses have closed, jobs are gone, young people leave. Yet beneath this surface of decline lies a rare constellation of opportunity. Just an hour from Vienna, with Europe's cleanest water flowing from the springs next door, surrounded by fertile land and affordable real estate, the Rax valley could become something else entirely: a prototype of future living.
With the **commons hub** already established as a growing engine for community, events, and innovation, the immediate next step is expansion into an **Event Campus**. The bigger goal, however, is establishing a **Valley of the Commons**: a place where cooperative housing, cosmo-local production, and systemic resilience converge into a living laboratory of post-capitalism.
## Why here, why now?
Across Europe, villages like ours have been emptying out. Younger generations move to the cities, industries collapse, and what remains are elderly residents, shuttered factories, and a crumbling social fabric. Meanwhile, the digital nomad movement, the rise of smart villages, and the accelerating meta-crisis point towards a different trajectory: people are looking for ways out of the metropolis, towards self-sufficient and regenerative forms of life.
Our setting is uniquely well-suited for this transition:
- **Resilient resources:** The surrounding mountains cool the air and provide Vienna's drinking water, while the valleys in between contain fertile land.
- **Space to grow:** An abundance of vacant houses, commercial spaces and agricultural lots makes expansion affordable and welcomed by locals.
- **Connectivity:** One hour by train to Vienna and 90 minutes to the airport position us right at the center of Europe's rail and air connections.
With local conditions ripe and global pressures rising, it's time to turn opportunity into action and start building. The future is shaped by the actions we take today.
## From hub to valley
The **commons hub** already anchors this process. Battle-tested as an event venue and guesthouse, it hosts everything from collaborative finance conferences, solarpunk meetups, and activist gatherings to academic workshops and prototyping sessions. To an emerging Valley of the Commons, it provides:
- **An economic engine**: providing jobs, visibility, and a constant stream of curious people seeking alternatives to the status quo;
- **A platform for entrepreneurship**: supporting new event series, research projects, product prototyping, and startup incubation;
- **A beacon for metamodern thought and imagination**: holding space for narratives that orient us beyond collapse.
## Housing the future
A key pillar of the valley will be housing — not "smart homes" full of gadgets, but dwellings designed for the real challenges of the 21st century: climate change, resource constraints, and hyper-mobile lifestyles. We envision:
- **Renovated houses** integrating state-of-the-art sustainable living concepts;
- **Cooperative ownership**: from permanent co-housing to time-share models and shorter-term rentals;
- **Distributed governance**: each house organized by its residents, following the principle of subsidiarity.
The **commons hub GmbH** is a pragmatic bootstrap, but the valley itself will be **community-owned and self-governed**. Our role is to provide initial momentum — network, knowledge, infrastructure, and capital — then step back.
## Production and self-sufficiency
The Valley of the Commons won't just be a place to live — it will be a place to **collaboratively produce value** in diverse ways:
- **Digital production** in communal co-working spaces;
- **Physical production** in co-owned FabLabs seeking to develop innovative niche products and providing local manufacturing for community needs;
- **Basic needs provision**: 100% water and energy self-sufficiency asap, while gradually building food resilience through community farming.
In this way, production cycles will not only create a semi-autonomous local economy, but also build the capacity to provide for livelihoods and withstand future global disruptions.
## Governance and community
A key challenge in the political realm consists of balancing **local autonomy** with **collective coherence**, while also rebalancing the relationship between capital and labor through cooperative ownership and participatory governance. Guided by the principle of subsidiarity, we envision:
- Co-owned and self-governed **houses and productive units**;
- **Village-wide commons** coordinated through collectively chosen mechanisms;
- **Integration with municipal politics** based on mutual recognition and complementarity.
## Announcing: Popup Village 2026
For now, the Valley of the Commons remains a vision, inspired by conversations among commons-oriented networks, deep adaptation thinkers, and p2p communities preparing for civilizational transition. To make it real, it needs future commons villagers to step in.
In 2026, we plan a **46 week popup village** — exploring housing, governance, production, and the valley itself, with the goal of turning vision into concrete plans.
**Michel Bauwens**, eminent Commons and P2P scholar, is on board, bringing his vision of a shift toward a commons-based civilization and experience with cosmo-local production. Veterans in mutual credit, community currencies, and housing coops are planning local research projects, while community leaders from Web3 co-livings and ecovillages provide guidance to **help lay the foundations of the Valley of the Commons**.
The organizing team includes veterans of **Zuzalu** and other "zu-villages." Together, we will explore not only what is possible, but what it feels like to live in a valley oriented around the commons.
## A growing campus, a growing commons
The immediate next step on this journey is the expansion of the hub into **a monastery-like Event Campus** capable of hosting summits, exhibitions, and popups. To make this happen, we are running a **community lending campaign**, raising €200k in loans from our network to secure and renovate the buildings. **Over €80k have already been pledged**, with some lenders even donating their interest back to support future events.
Supporting **the church expansion lays the groundwork** for the Valley of the Commons — the first step toward a campus that could grow into a vibrant village.
## Get Involved
🌱 **Lend to us** help bridge the final stretch of funding.
🙋‍♂️ **Pre-register** for the 2026 popup village.
✍️ **Write to** f.fritsch@commons-hub.at **to get involved** in the popup organization.
Together, we can turn vision into reality.
## Project Structure
```
votc/
├── index.html # Main landing page
├── game.html # Game terminal interface
├── privacy.html # Privacy FAQ page
├── styles.css # Landing page stylesheet
├── game.css # Game page stylesheet
├── waitlist.js # Waitlist form handler (client-side)
├── game.js # Game terminal logic
├── package.json # Node.js dependencies
├── vercel.json # Vercel configuration
├── internal_thought.md # Game design context for AI
├── api/
│ ├── waitlist.js # Serverless function for Google Sheets
│ └── share-to-github.js # Serverless function for GitHub sharing
├── refs/ # Internal documentation (gitignored)
├── README.md # This file
├── commonshublogo.svg # Commons Hub logo
└── valley.webp # Hero image
```
## Features
- **Landing Page** - Event information and waitlist signup
- **Game Terminal** - Interactive Socratic dialogue with AI game master
- **Idea Sharing** - Share conversation excerpts to GitHub as structured ideas
- **Privacy-First** - No tracking, no cookies, no user accounts
## Setup
### Prerequisites
- Node.js 18.x+
- Vercel account (for deployment)
- Google Cloud account (for waitlist)
- Vercel AI Gateway API key (for game chat)
- GitHub Personal Access Token (for idea sharing)
### Installation
```bash
git clone https://github.com/understories/votc.git
cd votc
npm install
```
### Environment Variables
Create `.env` in the root directory (or set in Vercel dashboard):
**Waitlist:**
- `GOOGLE_SERVICE_ACCOUNT` - Google Service Account JSON (string)
- `GOOGLE_SHEET_ID` - Google Sheets spreadsheet ID
- `GOOGLE_SHEET_NAME` - Sheet name (default: 'Waitlist')
**Game Chat:**
- `GAME_INTELLIGENCE` or `AI_GATEWAY_API_KEY` - Vercel AI Gateway key (starts with `vck_`)
- `GAME_MODEL` - Model name (default: `mistral/devstral-2`)
**GitHub Sharing:**
- `GITHUB_TOKEN` - GitHub Personal Access Token
- `GITHUB_OWNER` - Repo owner (default: 'understories')
- `GITHUB_REPO` - Repo name (default: 'votc')
- `GITHUB_BRANCH` - Branch (default: 'main')
- `GITHUB_PATH` - Ideas path (default: 'build_game/ideas')
- `GITHUB_CONVERSATIONS_PATH` - Conversations path (default: 'build_game/conversations')
### Local Development
**Static pages only:**
```bash
python3 -m http.server 8000
# or: npx http-server
```
**Full functionality (with serverless functions):**
```bash
npx vercel dev
```
### Deployment
```bash
npm i -g vercel
vercel
# Set environment variables in Vercel dashboard
vercel --prod
```
## Waitlist Functionality
The site includes a waitlist form that securely stores email addresses in a private Google Sheet. The integration uses:
- **Frontend:** HTML form with JavaScript for submission handling
- **Backend:** Vercel serverless function (`/api/waitlist.js`) that acts as a secure proxy
- **Storage:** Google Sheets (private, accessible only to admins)
**Setup Instructions:** See `refs/google-sheets-setup.md` for detailed configuration steps.
**Security:** All Google Sheets credentials are stored in Vercel environment variables and never exposed to the client. The Google Sheet remains private even though the code is public.
## Game Terminal
The game terminal (`game.html`) provides an interactive Socratic dialogue interface where users can explore ideas about the Valley of the Commons with an AI game master. Features:
- **Socratic questioning** - The AI guides conversations through thoughtful questions
- **Idea sharing** - Users can share conversation excerpts or full conversations to GitHub
- **Privacy-first** - Conversations are not saved unless explicitly shared
- **Streaming responses** - Real-time text streaming for natural conversation flow
**Game Chat:** Uses Vercel AI Gateway with Mistral models. See `refs/llm-integration-plan-v2.1.md` for architecture details.
**Security:** All credentials stored in Vercel environment variables. No secrets in code.
## Design Notes
This landing page follows the design aesthetic of commons-hub.at:
- Clean, minimal design
- Uppercase headings
- Sticky CTA button that scrolls with the page
- Responsive layout
- Modern typography
## License
© 2025 Commons Hub

246
api/game-chat.js Normal file
View File

@ -0,0 +1,246 @@
// Vercel serverless function for game chat
// Uses Vercel AI Gateway with string model names
// Implements Socratic game master moderator
// CRITICAL: Set AI_GATEWAY_API_KEY before requiring 'ai' SDK
// The SDK reads this at initialization time
// Prefer GAME_INTELLIGENCE if it exists and is a valid key (starts with vck_)
// Otherwise use AI_GATEWAY_API_KEY if it's a valid key
const gatewayKey = process.env.GAME_INTELLIGENCE || process.env.AI_GATEWAY_API_KEY;
if (gatewayKey && gatewayKey.startsWith('vck_')) {
process.env.AI_GATEWAY_API_KEY = gatewayKey;
} else if (process.env.GAME_INTELLIGENCE && process.env.GAME_INTELLIGENCE.startsWith('vck_')) {
process.env.AI_GATEWAY_API_KEY = process.env.GAME_INTELLIGENCE;
}
const { streamText } = require('ai');
const fs = require('fs');
const path = require('path');
// Load internal thoughts for context
function loadInternalThoughts() {
try {
const thoughtsPath = path.join(process.cwd(), 'internal_thought.md');
if (fs.existsSync(thoughtsPath)) {
return fs.readFileSync(thoughtsPath, 'utf8');
}
} catch (error) {
console.warn('Could not load internal_thought.md:', error.message);
}
return '';
}
const INTERNAL_THOUGHTS = loadInternalThoughts();
// Base system prompt for ongoing Socratic dialogue
const SYSTEM_PROMPT = `You are a Socratic game master moderator at the Valley of the Commons.
ROLE:
- Ask ONE brief, open-ended question (1-2 sentences MAX)
- NEVER provide answers, definitions, or solutions
- If asked for a definition, respond with a question about meaning or context
- Build on previous exchanges
- Keep responses SHORT and terminal-friendly
CONTEXT: "Valley of the Commons" is a decade-long game becoming a real village. Participants can propose tools, add rules, name places, create quests, document paths, bind myth to reality.
DESIGN PRINCIPLES (reference subtly, never lecture):
- Game as instrument for generating new operations
- Physical-digital bridge: map, cards, projections
- Community-driven: people mark places, add quests, surface tools
- Ritualistic elements: mix of mythology and real life
- Out of the box thinking: generate unconventional approaches
- Commonalization: game becomes shared resource
${INTERNAL_THOUGHTS ? `\nINTERNAL NOTES (inform questions, help participants discover):\n${INTERNAL_THOUGHTS}\n` : ''}
CRITICAL: Be BRIEF. One question per response. Maximum 2 sentences. Probe, don't lecture.`;
// Context-setting prompt for first response
const FIRST_RESPONSE_PROMPT = `You are a game master moderator welcoming someone to the Valley of the Commons.
This is the FIRST response after the user agreed to help create a game that shapes reality.
TASK:
- Brief context-setting (2-3 sentences) introducing the Valley
- Use internal notes to paint what this game-village is becoming
- End with ONE open-ended question inviting exploration
- Do NOT respond to "yes" or "sure" - set context instead
CONTEXT: "Valley of the Commons" is a decade-long game becoming a real village in the Austrian Alps. Participants can propose tools, add rules, name places, create quests, document paths, bind myth to reality.
${INTERNAL_THOUGHTS ? `\nINTERNAL NOTES (inform context and question):\n${INTERNAL_THOUGHTS}\n` : ''}
CRITICAL: Be BRIEF. Maximum 3-4 sentences total. One question at the end.`;
module.exports = async function handler(req, res) {
console.log('[game-chat] Request received:', req.method, req.url);
// Only allow POST requests
if (req.method !== 'POST') {
console.log('[game-chat] Method not allowed:', req.method);
return res.status(405).json({ error: 'Method not allowed' });
}
// No CORS headers needed (same-origin only)
// If cross-origin needed later, restrict to specific domains
try {
const { messages } = req.body;
console.log('[game-chat] Messages received:', messages?.length || 0);
if (messages && messages.length > 0) {
console.log('[game-chat] First message:', {
role: messages[0]?.role,
content: messages[0]?.content?.substring(0, 100),
});
if (messages.length > 1) {
console.log('[game-chat] Last message:', {
role: messages[messages.length - 1]?.role,
content: messages[messages.length - 1]?.content?.substring(0, 100),
});
}
}
// Validate input
if (!messages || !Array.isArray(messages)) {
return res.status(400).json({ error: 'Invalid request format' });
}
// Sanitize and whitelist roles (CRITICAL: prevent system injection)
const sanitizedMessages = messages
.filter(msg => msg && typeof msg.content === 'string')
.map(msg => {
// Whitelist: only 'user' or 'assistant' roles allowed
const role = msg.role === 'assistant' ? 'assistant' : 'user';
return {
role: role,
content: msg.content.slice(0, 500), // Max 500 chars per message
};
})
.filter(msg => msg.content.length > 0); // Drop empty messages
console.log('[game-chat] Sanitized messages:', sanitizedMessages.length);
if (sanitizedMessages.length > 0) {
console.log('[game-chat] Sanitized first message:', {
role: sanitizedMessages[0]?.role,
content: sanitizedMessages[0]?.content?.substring(0, 100),
});
}
// Count user turns server-side (prevent gaming)
const userTurns = sanitizedMessages.filter(m => m.role === 'user').length;
if (userTurns > 12) {
return res.status(429).json({
error: 'Conversation limit reached. Please start a new session.'
});
}
// Detect if this is the first user response (context-setting phase)
const isFirstResponse = userTurns === 1 && sanitizedMessages.length === 1;
// Get API key (support both GAME_INTELLIGENCE and AI_GATEWAY_API_KEY)
// Vercel AI SDK automatically reads AI_GATEWAY_API_KEY from environment
// If GAME_INTELLIGENCE is set, use it as the API key
const apiKey = process.env.GAME_INTELLIGENCE || process.env.AI_GATEWAY_API_KEY;
console.log('[game-chat] API key check:', {
hasGAME_INTELLIGENCE: !!process.env.GAME_INTELLIGENCE,
hasAI_GATEWAY_API_KEY: !!process.env.AI_GATEWAY_API_KEY,
hasApiKey: !!apiKey,
apiKeyPrefix: apiKey ? apiKey.substring(0, 15) + '...' : 'none',
});
if (!apiKey) {
console.error('[game-chat] Missing GAME_INTELLIGENCE or AI_GATEWAY_API_KEY');
return res.status(500).json({ error: 'Server configuration error' });
}
// Ensure AI_GATEWAY_API_KEY is set for AI SDK (supports GAME_INTELLIGENCE alias)
// AI SDK reads AI_GATEWAY_API_KEY automatically when using string model names
// Must be set before streamText is called
if (!process.env.AI_GATEWAY_API_KEY) {
process.env.AI_GATEWAY_API_KEY = apiKey;
}
// Model selection - use string format for Vercel AI Gateway
// String format "provider/model" automatically routes through AI Gateway
// CRITICAL: AI_GATEWAY_API_KEY must be set in environment for this to work
const modelName = process.env.GAME_MODEL || 'mistral/devstral-2';
// Examples:
// - "mistral/devstral-2" (current - free tier)
// - "mistral/ministral-3b" (cost-effective)
// - "mistral/mistral-large-latest"
// - "anthropic/claude-3-5-sonnet-20241022"
console.log('[game-chat] Calling AI Gateway with string model:', modelName);
console.log('[game-chat] Message count:', sanitizedMessages.length);
console.log('[game-chat] AI_GATEWAY_API_KEY set:', !!process.env.AI_GATEWAY_API_KEY);
try {
// Use string model name - SDK automatically routes through AI Gateway
// when it sees the "provider/model" format and AI_GATEWAY_API_KEY is set
// Choose prompt based on whether this is the first response
const activePrompt = isFirstResponse ? FIRST_RESPONSE_PROMPT : SYSTEM_PROMPT;
console.log('[game-chat] Creating streamText with model:', modelName);
console.log('[game-chat] Using prompt type:', isFirstResponse ? 'FIRST_RESPONSE' : 'SOCRATIC');
const result = streamText({
model: modelName, // String format routes through Gateway
system: activePrompt, // System prompt (NOT in messages array)
messages: sanitizedMessages, // Only user/assistant messages
maxTokens: isFirstResponse ? 120 : 80, // Enforce brevity
temperature: 0.7,
});
console.log('[game-chat] StreamText result created');
// Set headers for streaming text response
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('Transfer-Encoding', 'chunked');
// Stream text chunks directly to client
console.log('[game-chat] Starting to stream text to client');
try {
for await (const textPart of result.textStream) {
res.write(textPart);
}
res.end();
console.log('[game-chat] Stream completed and sent to client');
} catch (streamError) {
console.error('[game-chat] Error during streaming:', streamError.message);
if (!res.headersSent) {
res.status(500).json({ error: 'Stream error' });
} else {
res.end();
}
}
} catch (streamError) {
console.error('[game-chat] Error in streamText call:', {
message: streamError.message,
stack: streamError.stack,
});
throw streamError;
}
} catch (error) {
console.error('AI Gateway error:', {
message: error.message,
stack: error.stack,
model: modelName,
hasApiKey: !!apiKey,
apiKeyPrefix: apiKey ? apiKey.substring(0, 10) : 'none',
});
// Return more detailed error in development
const isDev = process.env.NODE_ENV !== 'production';
return res.status(500).json({
error: isDev
? `AI Gateway error: ${error.message}`
: 'AI service temporarily unavailable. Please try again.',
...(isDev && { details: error.stack })
});
}
};

233
api/share-to-github.js Normal file
View File

@ -0,0 +1,233 @@
// Vercel serverless function to share conversation excerpts to GitHub
// Creates files directly in the repository (NOT issues) using GitHub API
// Uses GITHUB_TOKEN (github.com/vrnvrn account) to create files in:
// - build_game/ideas/ for selected excerpts
// - build_game/conversations/ for full chat history
const { Octokit } = require('@octokit/rest');
// Generate idea file template
function generateIdeaTemplate(excerpt, context) {
const timestamp = new Date().toISOString();
const date = new Date().toLocaleString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short'
});
return `# Idea: [Auto-generated from conversation]
**Source:** Valley of the Commons Game Master Dialogue
**Date:** ${date}
**Excerpt:**
${excerpt}
---
**Context:** This idea emerged from a Socratic dialogue about game design in the Valley of the Commons.
**Next Steps:**
- [ ] Refine this idea
- [ ] Connect to other ideas
- [ ] Propose as a tool/rule/quest
---
*Generated from game master conversation*`;
}
// Generate unique filename based on timestamp
function generateFilename(isFullChat = false) {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const prefix = isFullChat ? 'conversation' : 'idea';
return `${prefix}-${year}-${month}-${day}-${hours}${minutes}${seconds}.md`;
}
module.exports = async function handler(req, res) {
// Only allow POST requests
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
console.log('[share-to-github] Request received:', {
method: req.method,
hasBody: !!req.body,
bodyKeys: req.body ? Object.keys(req.body) : [],
});
try {
const { content, excerpt, context, isFullChat } = req.body;
console.log('[share-to-github] Request data:', {
hasContent: !!content,
contentLength: content ? content.length : 0,
hasExcerpt: !!excerpt,
isFullChat: isFullChat,
});
// Prefer full content if provided, otherwise use excerpt for backwards compatibility
let fileContent;
if (content && typeof content === 'string') {
// Use provided content (already includes template + chat history)
fileContent = content.slice(0, 50000).trim(); // Allow larger content for full chat history
if (fileContent.length === 0) {
return res.status(400).json({ error: 'Content cannot be empty' });
}
} else if (excerpt && typeof excerpt === 'string') {
// Fallback to generating template from excerpt (backwards compatibility)
const sanitizedExcerpt = excerpt.slice(0, 5000).trim();
if (sanitizedExcerpt.length === 0) {
return res.status(400).json({ error: 'Excerpt cannot be empty' });
}
fileContent = generateIdeaTemplate(sanitizedExcerpt, context || {});
} else {
return res.status(400).json({ error: 'Content or excerpt is required' });
}
// Get GitHub configuration
const token = process.env.GITHUB_TOKEN;
if (!token) {
console.error('[share-to-github] Missing GITHUB_TOKEN');
return res.status(500).json({ error: 'Server configuration error: Missing GitHub token' });
}
const owner = process.env.GITHUB_OWNER || 'understories';
const repo = process.env.GITHUB_REPO || 'votc';
const branch = process.env.GITHUB_BRANCH || 'main';
// Determine base path: conversations directory for full chats, ideas for excerpts
const isFullChatShare = isFullChat === true;
const basePath = isFullChatShare
? (process.env.GITHUB_CONVERSATIONS_PATH || 'build_game/conversations')
: (process.env.GITHUB_PATH || 'build_game/ideas');
console.log('[share-to-github] GitHub config:', {
owner,
repo,
branch,
basePath,
isFullChatShare,
hasToken: !!token,
tokenPrefix: token ? token.substring(0, 10) + '...' : 'none',
});
// Initialize Octokit
const octokit = new Octokit({
auth: token,
});
// Generate filename and path
const filename = generateFilename(isFullChatShare);
const path = `${basePath}/${filename}`;
console.log('[share-to-github] Creating file:', {
filename,
path,
contentLength: fileContent.length,
});
// fileContent is already set above (either from content param or generated from excerpt)
// Create file in GitHub
const commitMessage = isFullChatShare
? 'Add full conversation from game master dialogue'
: 'Add idea from game master conversation';
// Ensure content is properly encoded as base64
// GitHub API requires base64 encoding and the string must be valid
let base64Content;
try {
base64Content = Buffer.from(fileContent, 'utf8').toString('base64');
// Validate base64 encoding
if (!base64Content || base64Content.length === 0) {
throw new Error('Base64 encoding resulted in empty string');
}
} catch (encodeError) {
console.error('[share-to-github] Base64 encoding error:', encodeError);
return res.status(500).json({
error: 'Failed to encode content. Please try again.'
});
}
console.log('[share-to-github] Base64 content length:', base64Content.length);
const response = await octokit.repos.createOrUpdateFileContents({
owner: owner,
repo: repo,
path: path,
message: commitMessage,
content: base64Content,
branch: branch,
});
console.log('[share-to-github] File created successfully:', {
path: response.data.content.path,
sha: response.data.content.sha,
});
// Generate GitHub URL
const githubUrl = `https://github.com/${owner}/${repo}/blob/${branch}/${path}`;
return res.status(200).json({
success: true,
url: githubUrl,
filename: filename,
path: path,
});
} catch (error) {
console.error('[share-to-github] Error:', {
message: error.message,
status: error.status,
name: error.name,
response: error.response ? {
status: error.response.status,
statusText: error.response.statusText,
data: error.response.data,
} : null,
stack: error.stack,
});
// Handle specific GitHub API errors
if (error.status === 401) {
return res.status(500).json({
error: 'Authentication error. Please contact support.'
});
}
if (error.status === 403) {
return res.status(429).json({
error: 'Rate limit exceeded. Please try again later.'
});
}
if (error.status === 404) {
return res.status(500).json({
error: 'Repository not found. Please contact support.'
});
}
if (error.status === 422) {
return res.status(400).json({
error: 'Invalid file path. Please try again.'
});
}
// Generic error
return res.status(500).json({
error: 'Failed to share to GitHub. Please try again later.'
});
}
};

129
api/waitlist.js Normal file
View File

@ -0,0 +1,129 @@
// Vercel serverless function to handle waitlist submissions
// This keeps Google Sheets credentials private (server-side only)
module.exports = async function handler(req, res) {
// Only allow POST requests
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
// CORS headers for frontend access
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'POST');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
// Handle preflight requests
if (req.method === 'OPTIONS') {
return res.status(200).end();
}
try {
const { email, name, involvement } = req.body;
// Validate email
if (!email || !email.includes('@')) {
return res.status(400).json({ error: 'Valid email is required' });
}
// Validate name
if (!name || name.trim() === '') {
return res.status(400).json({ error: 'Name is required' });
}
// Validate involvement
if (!involvement || involvement.trim() === '') {
return res.status(400).json({ error: 'Please describe your desired involvement' });
}
// Get credentials from environment variables
const credentials = process.env.GOOGLE_SERVICE_ACCOUNT;
const spreadsheetId = process.env.GOOGLE_SHEET_ID;
const sheetName = process.env.GOOGLE_SHEET_NAME || 'Waitlist';
if (!credentials || !spreadsheetId) {
console.error('Missing Google Sheets configuration');
return res.status(500).json({ error: 'Server configuration error' });
}
// Parse service account credentials
// Trim whitespace and handle multi-line JSON from environment variable
const credentialsTrimmed = credentials.trim();
let serviceAccount;
try {
serviceAccount = JSON.parse(credentialsTrimmed);
} catch (parseError) {
console.error('Failed to parse service account credentials:', parseError.message);
return res.status(500).json({ error: 'Server configuration error' });
}
// Import googleapis (will be installed as dependency)
const { google } = require('googleapis');
// Authenticate with Google Sheets API
const auth = new google.auth.GoogleAuth({
credentials: serviceAccount,
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
const sheets = google.sheets({ version: 'v4', auth });
// Prepare the data to append
const timestamp = new Date().toISOString();
const values = [[timestamp, email, name || '', involvement || '']];
// Append to the sheet
await sheets.spreadsheets.values.append({
spreadsheetId,
range: `${sheetName}!A:D`,
valueInputOption: 'USER_ENTERED',
insertDataOption: 'INSERT_ROWS',
resource: {
values,
},
});
// Success response
return res.status(200).json({
success: true,
message: 'Successfully joined the waitlist!'
});
} catch (error) {
// Log detailed error for debugging (visible in Vercel logs)
console.error('Error adding to waitlist:', {
message: error.message,
code: error.code,
status: error.response?.status,
statusText: error.response?.statusText,
details: error.response?.data,
stack: error.stack
});
// Provide more specific error messages for common issues
if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
return res.status(500).json({
error: 'Network error connecting to Google Sheets. Please try again later.'
});
}
if (error.response?.status === 403) {
console.error('Permission denied - check service account has access to sheet');
return res.status(500).json({
error: 'Permission error. Please contact support.'
});
}
if (error.response?.status === 404) {
console.error('Sheet not found - check spreadsheet ID and sheet name');
return res.status(500).json({
error: 'Sheet configuration error. Please contact support.'
});
}
// Don't expose internal errors to client
return res.status(500).json({
error: 'Failed to join waitlist. Please try again later.'
});
}
}

1
commonshublogo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.6 KiB

84
community-partners.js Normal file
View File

@ -0,0 +1,84 @@
// Dynamically load community partners
document.addEventListener('DOMContentLoaded', function() {
const partnersContainer = document.getElementById('partners-container');
if (!partnersContainer) return;
// Define partners with placeholder links
// TODO: Replace placeholder URLs with actual partner websites
const partners = [
{
name: 'Hubs Network',
logo: 'hubs-network.jpg',
url: 'https://www.hubsnetwork.org'
},
{
name: 'Invisible Garden',
logo: 'invisible-garden.svg',
url: 'https://invisible.garden'
},
{
name: 'Understories',
logo: 'understories.png',
url: 'https://understories.github.io'
},
{
name: 'P2P Foundation',
logo: 'p2p.jpeg',
url: 'https://p2pfoundation.net/'
},
{
name: 'Crypto Commons Association',
logo: 'cca-logo.png',
url: 'https://www.crypto-commons.org'
},
{
name: 'Collaborative Finance',
logo: 'cofi_cropped.png',
url: 'https://www.collaborative-finance.net'
},
{
name: 'Akasha Hub',
logo: 'akasha.png',
url: 'https://akasha.barcelona'
},
{
name: 'dOrg',
logo: 'd0rg.png',
url: 'https://www.dorg.tech'
},
{
name: 'FarmLab',
logo: 'farmlab.png',
url: 'https://www.farmlab.at'
}
];
// Load each partner
partners.forEach(partner => {
loadPartner(partner);
});
function loadPartner(partner) {
try {
// Create partner link
const partnerLink = document.createElement('a');
partnerLink.href = partner.url;
partnerLink.target = '_blank';
partnerLink.rel = 'noopener noreferrer';
partnerLink.className = 'partner-link';
// Create logo image
const img = document.createElement('img');
img.src = `community-partners/${partner.logo}`;
img.alt = partner.name;
img.className = 'partner-logo';
img.loading = 'lazy';
// Assemble link
partnerLink.appendChild(img);
partnersContainer.appendChild(partnerLink);
} catch (error) {
console.error(`Error loading partner ${partner.name}:`, error);
}
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
community-partners/d0rg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 555 283">
<!-- Generator: Adobe Illustrator 29.7.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 8) -->
<defs>
<style>
.st0 {
fill: #e08370;
}
.st1 {
fill: #0d0d0d;
}
</style>
</defs>
<path class="st1" d="M222.5,133.8h-3.1V63.1h3.1v70.7Z"/>
<path class="st1" d="M244,133.8h-3.1v-50.4h2.5v11.8c2.4-7.9,8.6-13.2,18.3-13.2h.2c12.6,0,19.3,8.6,19.3,21.5v30.2h-3.1v-31c0-10.6-5.8-17.8-16.5-17.8s-17.6,7.4-17.6,18.2v30.6Z"/>
<path class="st1" d="M315.6,133.8h-7.2l-18.9-50.4h3.2l17.7,47.5h3.1l15.9-47.5h3.1l-17,50.4Z"/>
<path class="st1" d="M344.6,71.7c-2.1,0-3.3-1.3-3.3-3.2s1.2-3.2,3.3-3.2,3.3,1.4,3.3,3.2-1.2,3.2-3.3,3.2ZM348.3,133.8h-3.1v-47.5h-8.7v-2.9h11.8v50.4Z"/>
<path class="st1" d="M379,135.2c-12.3,0-18.9-6.4-18.9-15.1h3.1c0,6.7,4.7,12.2,15.8,12.2s15.8-5.6,15.8-12.2-5.5-10-12.9-10.8l-6.2-.7c-8.4-1-13.5-5.5-13.5-12.8s6-13.9,16.8-13.9,16.8,5.7,16.8,13.9h-3.1c0-6.3-4.9-11-13.7-11s-13.7,5.3-13.7,11.2,3.7,9,10.7,9.8l6.2.7c8.8,1,15.7,5.2,15.7,13.8s-6.6,14.9-18.9,14.9Z"/>
<path class="st1" d="M412,71.7c-2.1,0-3.3-1.3-3.3-3.2s1.2-3.2,3.3-3.2,3.3,1.4,3.3,3.2-1.2,3.2-3.3,3.2ZM415.7,133.8h-3.1v-47.5h-8.7v-2.9h11.8v50.4Z"/>
<path class="st1" d="M456.5,135.4c-11.1,0-18.9-6.6-22-15.1v13.6h-2.5V63.1h3.1v32.6c3.4-8.1,10.8-13.9,21.4-13.9,15.6,0,24.3,13.1,24.3,26.4v.6c0,12.6-8.3,26.4-24.3,26.4ZM456.3,132.5c13.9,0,21.4-11.8,21.4-23.8s-7.8-23.8-21.4-23.8-21.4,9.2-21.4,21.2v5.2c0,11.2,8.2,21.2,21.4,21.2Z"/>
<path class="st1" d="M495.7,133.8h-3.1v-67.8h-7.9v-2.9h11v70.7Z"/>
<path class="st1" d="M531.9,135.4c-16.9,0-24.6-13.3-24.6-26.7v-.6c0-13.2,8.1-26.2,24-26.2s23.2,11.5,23.2,23.5v3.1h-44.4c0,12.8,7.1,23.9,21.7,23.9s17.2-5.7,18.8-12.8h3.1c-2,9.1-9.8,15.7-21.9,15.7ZM531.3,84.8c-12.9,0-20.1,9.5-21,20.8h41.4c-.4-11.7-7.9-20.8-20.3-20.8Z"/>
<path class="st1" d="M248.7,219.9c-25.9,0-35.4-19.8-35.4-36.4v-1.2c0-17.9,10.9-36.4,33.8-36.4s27.8,9.8,29.6,24.5h-3.1c-1.7-13.8-12.6-21.6-26.5-21.6-20.1,0-30.7,16.2-30.7,34.1s9.1,34.1,32.3,34.1,18.6-3.5,23.9-9.4v-17.5h-26.6v-2.7h36.6v2.7h-6.9v18.4c-5.5,6.9-14.7,11.4-27,11.4Z"/>
<path class="st1" d="M305.6,219.7c-9.6,0-17.5-5.5-17.5-15.6s6.7-15,18.3-15h16.8v-4.8c0-8.6-4.1-13.9-15.2-13.9s-10,.4-13.7,1v-2.9c3.8-.6,8.2-1,12.1-1,15,0,19.9,6.2,19.9,17.5v33.3h-2.5v-10.8c-2.6,8.2-9.9,12.2-18.1,12.2ZM305.8,217c8,0,17-4.2,17.3-16.9v-8.3h-17.3c-9.5,0-14.6,4.9-14.6,12.4s6.2,12.8,14.6,12.8Z"/>
<path class="st1" d="M344.8,218.4h-3.1v-50.4h2.5v12.8c2.2-8.7,8.5-13.2,20.4-13.2h1.6v2.9h-2.4c-12.8,0-19.1,6.5-19.1,19.1v28.8Z"/>
<path class="st1" d="M393.7,219.9c-15.6,0-24.3-13.1-24.3-26.4v-.6c0-12.6,8.3-26.4,24.3-26.4s18.1,6,21.4,14v-32.8h3.1v70.7h-2.5v-13.4c-3.2,8.7-10.9,14.9-22,14.9ZM393.9,217c12.8,0,21.4-9.2,21.4-21.2v-5.2c0-11.2-8.2-21.2-21.4-21.2s-21.4,11.8-21.4,23.8,7.8,23.8,21.4,23.8Z"/>
<path class="st1" d="M454.4,219.9c-16.9,0-24.6-13.3-24.6-26.7v-.6c0-13.2,8.1-26.2,24-26.2s23.2,11.5,23.2,23.5v3.1h-44.4c0,12.8,7.1,23.9,21.7,23.9s17.2-5.7,18.8-12.8h3.1c-2,9.1-9.8,15.7-21.9,15.7ZM453.8,169.4c-12.9,0-20.1,9.5-21,20.8h41.4c-.4-11.7-7.9-20.8-20.3-20.8Z"/>
<path class="st1" d="M490.8,218.4h-3.1v-50.4h2.5v11.8c2.4-7.9,8.6-13.2,18.3-13.2h.2c12.6,0,19.3,8.6,19.3,21.5v30.2h-3.1v-31c0-10.6-5.8-17.8-16.5-17.8s-17.6,7.4-17.6,18.2v30.6Z"/>
<polygon class="st0" points="164.1 169.3 84.8 282.2 84.8 172.2 156 113.6 164.1 169.3"/>
<path class="st0" d="M78.6,174.7v69l-13.7-8.6c-3.2-2-6.7-3.9-10.6-5.9-6.6-3.2-19.4-10.2-31.4-20.6-11.8-10.3-19.2-20.6-21.8-30.8-1.1-4.2-1.4-10.2-.9-18,.4-7,1.5-15.3,3.1-24.2,1.2-6.6,2.6-13.3,4.3-20l5.5-22.1,11.1,19.9c5.8,10.4,13.5,19.1,22.7,25.8,3.6,2.6,10.4,7.7,17,13.9,4.3,4,7.8,7.8,10.2,11.1,1.4,2,2.5,3.8,3.3,5.5.7,1.5,1,2.8,1.2,4.1"/>
<path class="st0" d="M78.4.6v30.2c-.2,36.2-.2,79.2-.2,101.7v19l-14.7-12.1c-1.8-1.4-3.5-2.8-5.1-4.1l-1.2-1c-19.9-15.9-28.8-30.4-28.8-46.8s1.3-9.9,3.8-15.4c2.6-5.7,5.5-9.5,6.3-10.5L78.4.6Z"/>
<polygon class="st0" points="133.3 95.3 133.3 119.4 84.8 161.4 84.8 18.3 133.3 95.3"/>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
community-partners/p2p.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -1,11 +1,14 @@
services:
valley-commons:
votc:
build: .
container_name: votc
restart: unless-stopped
env_file:
- .env
labels:
- "traefik.enable=true"
- "traefik.http.routers.valley-commons.rule=Host(`valleyofthecommons.org`) || Host(`www.valleyofthecommons.org`)"
- "traefik.http.services.valley-commons.loadbalancer.server.port=80"
- "traefik.http.routers.votc.rule=Host(`votc.jeffemmett.com`)"
- "traefik.http.services.votc.loadbalancer.server.port=3000"
networks:
- traefik-public

828
game.css Normal file
View File

@ -0,0 +1,828 @@
/* Terminal Game Page Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100%;
height: 100vh;
background: #000;
color: #00ff00;
font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
overflow: hidden;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
}
/* GitHub Repo Link & Privacy FAQ */
.top-links {
position: fixed;
top: 1rem;
left: 1rem;
z-index: 1000;
display: flex;
gap: 0.75rem;
align-items: center;
}
.home-link,
.github-link,
.privacy-link {
color: #666;
opacity: 0.5;
transition: opacity 0.2s ease, color 0.2s ease;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
/* Increase touch target size for mobile */
padding: 0.5rem;
min-width: 44px;
min-height: 44px;
}
.home-link:hover,
.github-link:hover,
.privacy-link:hover {
opacity: 1;
color: #00ff00;
}
.home-link svg {
width: 18px;
height: 18px;
}
.github-link svg,
.privacy-link svg {
width: 20px;
height: 20px;
}
.terminal {
width: 100%;
flex: 1;
display: flex;
align-items: flex-start;
justify-content: center;
overflow: hidden;
padding: var(--spacing-lg, 2rem);
padding-top: 4rem; /* Space for top links */
padding-bottom: 0; /* Input line handles its own space */
position: relative;
min-height: 0; /* Allow flex shrinking */
}
.terminal-content {
width: 100%;
max-width: 800px;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
min-height: 0; /* Allow flex shrinking */
}
.terminal-line {
display: flex;
align-items: flex-start;
margin-bottom: 1rem;
font-size: 1.1rem;
line-height: 1.6;
word-wrap: break-word;
overflow-wrap: break-word;
}
.prompt {
color: #00ff00;
margin-right: 0.5rem;
font-weight: bold;
flex-shrink: 0; /* Prevent prompt from shrinking */
}
.question {
color: #fff;
font-size: clamp(1.2rem, 3vw, 1.5rem);
}
#input-line {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: #000;
padding: 1rem 0;
margin-top: 0;
z-index: 10;
border-top: 1px solid #333;
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: nowrap;
width: 100%;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.5); /* Subtle shadow to separate from content */
}
#terminal-input {
background: transparent;
border: none;
outline: none;
color: #00ff00;
font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
font-size: 1.1rem;
flex: 1;
padding: 0;
margin-left: 0.5rem;
min-width: 0; /* Allow flex shrinking */
}
.submit-button {
background: #00ff00;
color: #000;
border: 1px solid #00ff00;
padding: 0.5rem 1rem;
font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
font-size: 0.9rem;
font-weight: bold;
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
transition: background 0.2s ease, color 0.2s ease;
}
.submit-button:hover {
background: #00cc00;
border-color: #00cc00;
}
.submit-button:active {
background: #009900;
border-color: #009900;
}
.submit-button:disabled {
background: #333;
color: #666;
border-color: #333;
cursor: not-allowed;
}
#terminal-input::placeholder {
color: #555;
}
.cursor {
color: #00ff00;
animation: blink 1s infinite;
margin-left: 2px;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
.terminal-output {
margin-top: 2rem;
color: #fff;
font-size: 1rem;
line-height: 1.8;
white-space: pre-wrap;
opacity: 0;
animation: fadeIn 0.5s ease-in forwards;
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 6rem; /* Space for input line - increased to prevent overlap */
-webkit-overflow-scrolling: touch;
min-height: 0; /* Allow flex shrinking */
word-wrap: break-word;
overflow-wrap: break-word;
}
.terminal-output.show {
opacity: 1;
}
.terminal-output p {
margin-bottom: 1rem;
}
.terminal-output ul {
list-style: none;
margin: 1.5rem 0;
padding-left: 0;
}
.terminal-output li {
margin-bottom: 0.8rem;
padding-left: 1.5rem;
position: relative;
}
.terminal-output li:before {
content: "→";
position: absolute;
left: 0;
color: #00ff00;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.ai-message-streaming {
animation: blink 1s infinite;
}
.user-message {
color: #00ff00;
flex: 1;
min-width: 0;
word-wrap: break-word;
overflow-wrap: break-word;
}
.ai-message {
color: #fff;
flex: 1;
min-width: 0;
word-wrap: break-word;
overflow-wrap: break-word;
}
.loading-indicator {
opacity: 0.7;
}
.loading-text {
color: #666;
font-style: italic;
}
.error-message {
margin-top: 1rem;
}
.error-text {
color: #ff4444;
}
/* Message selection and sharing */
.message-line {
cursor: pointer;
transition: background-color 0.2s ease;
padding: 0.5rem 0.75rem;
margin: 0.5rem 0;
border-radius: 2px;
display: flex;
align-items: flex-start;
gap: 0.5rem;
}
.message-line:hover {
background-color: rgba(0, 255, 0, 0.1);
}
.message-line.selected {
background-color: rgba(0, 255, 0, 0.2);
border-left: 2px solid #00ff00;
padding-left: 0.75rem;
}
/* User messages appear below AI messages with distinct styling */
.message-line[data-role="user"] {
margin-top: 0.75rem;
}
.message-line[data-role="assistant"] {
margin-bottom: 0.25rem;
}
.share-buttons-container {
margin-top: 1rem;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.full-chat-share-container {
margin-top: 1rem;
margin-bottom: 1rem;
display: flex;
justify-content: center;
position: absolute;
bottom: 4rem; /* Above input line */
left: 0;
right: 0;
background: transparent;
padding: 0.5rem 0;
z-index: 9;
pointer-events: none; /* Allow clicks to pass through to scrollable area */
}
.full-chat-share-container button {
pointer-events: auto; /* Re-enable clicks for the button */
}
/* Desktop: Keep input fixed at bottom like real CLI */
@media (min-width: 769px) {
#input-line {
position: absolute;
bottom: 0;
}
.terminal-output {
padding-bottom: 6rem; /* Space for fixed input line - increased to prevent overlap */
}
.full-chat-share-container {
position: absolute;
bottom: 4rem;
}
/* Ensure desktop input is at least 16px to prevent zoom on some tablets */
#terminal-input {
font-size: max(1.1rem, 16px);
}
}
.share-button {
padding: 0.5rem 1rem;
background: transparent;
border: 2px solid #00ff00;
color: #00ff00;
font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s ease;
text-transform: uppercase;
letter-spacing: 0.05em;
flex: 1;
min-width: 150px;
}
.share-button:hover:not(:disabled) {
background: #00ff00;
color: #000;
}
.share-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.share-button-default {
border-color: #00ff00;
}
.share-button-own {
border-color: #666;
color: #999;
}
.share-button-own:hover:not(:disabled) {
background: #666;
color: #000;
border-color: #666;
}
.success-message {
margin-top: 1rem;
}
.success-text {
color: #00ff00;
}
.success-text a {
color: #00ff00;
text-decoration: underline;
}
.success-text a:hover {
text-decoration: none;
}
/* LLM Information Footer */
.llm-info {
position: fixed;
bottom: 4rem; /* Above input line */
left: 0.5rem;
font-size: 0.6rem;
color: #666;
max-width: 300px;
line-height: 1.3;
z-index: 8;
pointer-events: none; /* Allow clicks to pass through unless on a link */
}
.llm-info a {
color: #00ff00;
text-decoration: none;
pointer-events: auto;
}
.llm-info a:hover {
text-decoration: underline;
}
.llm-info p {
margin: 0.25rem 0;
}
.model-note {
font-size: 0.55rem;
color: #555;
margin-top: 0.25rem;
font-style: italic;
line-height: 1.2;
}
/* Share Modal */
.share-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
z-index: 2000;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.share-modal-content {
background: #000;
border: 2px solid #00ff00;
width: 100%;
max-width: 900px;
max-height: 90vh;
display: flex;
flex-direction: column;
color: #00ff00;
font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
}
.share-modal-header {
padding: 1rem;
border-bottom: 1px solid #00ff00;
display: flex;
justify-content: space-between;
align-items: center;
}
.share-modal-header h2 {
margin: 0;
font-size: 1.2rem;
color: #00ff00;
}
.share-modal-close {
background: transparent;
border: none;
color: #00ff00;
font-size: 2rem;
cursor: pointer;
padding: 0;
width: 2rem;
height: 2rem;
line-height: 1;
transition: color 0.2s ease;
}
.share-modal-close:hover {
color: #fff;
}
.share-modal-body {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 1rem;
gap: 1rem;
}
.share-template-section,
.share-history-section {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.share-template-section {
flex: 1;
min-height: 0;
}
.share-template-section label,
.share-history-section label {
font-size: 0.85rem;
color: #666;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.share-template-editor {
flex: 1;
min-height: 200px;
background: #111;
border: 1px solid #333;
color: #00ff00;
font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
font-size: 0.85rem;
padding: 1rem;
resize: vertical;
white-space: pre-wrap;
word-wrap: break-word;
}
.share-template-editor:focus {
outline: 2px solid #00ff00;
border-color: #00ff00;
}
.share-history-section {
flex: 0 0 200px;
min-height: 0;
}
.share-history-viewer {
flex: 1;
overflow-y: auto;
background: #111;
border: 1px solid #333;
padding: 1rem;
font-size: 0.8rem;
max-height: 200px;
}
.share-history-viewer .terminal-line {
margin-bottom: 0.5rem;
padding: 0.25rem 0;
}
.share-history-viewer .user-message {
color: #00ff00;
}
.share-history-viewer .assistant-message {
color: #fff;
}
.share-modal-footer {
padding: 1rem;
border-top: 1px solid #00ff00;
display: flex;
gap: 0.5rem;
justify-content: flex-end;
}
.share-modal-footer .share-button {
min-width: 150px;
}
@media (max-width: 768px) {
/* Mobile-first: Prevent body scroll, handle in terminal */
body {
overflow: hidden;
height: 100vh;
height: 100dvh; /* Dynamic viewport height for mobile browsers */
position: fixed;
width: 100%;
}
.terminal {
padding: 0;
padding-top: 3rem; /* Space for top links */
padding-left: 0.75rem;
padding-right: 0.75rem;
height: 100vh;
height: 100dvh;
max-height: 100vh;
max-height: 100dvh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
/* Allow scrolling when keyboard appears */
}
.terminal-content {
max-width: 100%;
min-height: 100%;
padding: 0;
padding-bottom: 2rem; /* Space at bottom */
display: flex;
flex-direction: column;
}
/* Initial question - smaller on mobile */
.terminal-line:first-child {
font-size: 0.9rem;
margin-bottom: 1rem;
padding: 0.5rem 0;
}
.question {
font-size: 0.95rem;
line-height: 1.4;
}
/* Chat output - scrollable area, input appears below */
.terminal-output {
margin-top: 1rem;
font-size: 0.9rem;
line-height: 1.6;
padding-bottom: 0.5rem;
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
margin-bottom: 0;
/* Input will appear below in natural flow */
}
/* Input line - follows chat content, appears below messages */
#input-line {
position: relative;
padding: 0.75rem;
padding-bottom: max(0.75rem, env(safe-area-inset-bottom));
margin-top: 1rem;
margin-bottom: 1rem;
background: #000;
border-top: 1px solid #333;
z-index: 10;
/* Ensure safe area on iOS */
padding-left: max(0.75rem, env(safe-area-inset-left));
padding-right: max(0.75rem, env(safe-area-inset-right));
display: flex;
align-items: center;
gap: 0.5rem;
/* Sticky at bottom when keyboard is closed, but follows content when keyboard opens */
position: sticky;
bottom: 0;
}
#terminal-input {
font-size: 16px; /* Prevent zoom on iOS */
padding: 0.75rem 0.5rem;
min-height: 44px; /* Minimum touch target size */
}
.submit-button {
padding: 0.75rem 1rem;
font-size: 0.9rem;
min-height: 44px; /* Minimum touch target size */
min-width: 70px;
}
/* Top links - smaller and positioned */
.top-links {
position: fixed;
top: 0.5rem;
left: 0.5rem;
z-index: 1000;
gap: 0.5rem;
padding-left: env(safe-area-inset-left);
}
.home-link svg,
.github-link svg,
.privacy-link svg {
width: 20px;
height: 20px;
}
/* Ensure touch targets are large enough on mobile */
.home-link,
.github-link,
.privacy-link {
min-width: 44px;
min-height: 44px;
padding: 0.5rem;
}
/* Full chat share button - appears above input in flow */
.full-chat-share-container {
position: relative;
margin-top: 1rem;
margin-bottom: 0.5rem;
padding: 0 0.75rem;
z-index: 9;
display: flex;
justify-content: center;
}
.full-chat-share-container button {
width: 100%;
padding: 0.75rem;
font-size: 0.85rem;
min-height: 44px;
}
/* LLM info - hide on mobile to save space */
.llm-info {
display: none !important; /* Hide on mobile to save space */
visibility: hidden !important;
opacity: 0 !important;
height: 0 !important;
overflow: hidden !important;
}
/* Share buttons - touch-friendly */
.share-button {
font-size: 0.9rem;
padding: 0.75rem 1rem;
min-height: 44px;
min-width: auto;
}
/* Message lines - better spacing on mobile */
.message-line {
padding: 0.75rem 0.5rem;
margin: 0.75rem 0;
font-size: 0.9rem;
line-height: 1.6;
}
/* Share modal - full screen on mobile */
.share-modal {
padding: 0;
align-items: flex-end;
}
.share-modal-content {
max-height: 90vh;
max-height: 90dvh;
border-radius: 1rem 1rem 0 0;
width: 100%;
max-width: 100%;
}
.share-modal-header {
padding: 1rem;
border-bottom: 1px solid #333;
}
.share-modal-body {
flex-direction: column;
padding: 1rem;
max-height: calc(90vh - 150px);
max-height: calc(90dvh - 150px);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.share-template-section,
.share-history-section {
flex: none;
margin-bottom: 1rem;
}
.share-history-section {
max-height: 200px;
}
.share-modal-footer {
flex-direction: column;
padding: 1rem;
gap: 0.75rem;
}
.share-modal-footer .share-button {
width: 100%;
min-height: 48px;
}
/* Prompt styling */
.prompt {
font-size: 0.9rem;
margin-right: 0.4rem;
}
/* Prevent text selection on buttons for better touch */
.share-button,
.submit-button {
-webkit-tap-highlight-color: rgba(0, 255, 0, 0.2);
-webkit-touch-callout: none;
user-select: none;
}
/* Better scrolling */
.terminal-output {
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
}
}

62
game.html Normal file
View File

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<link rel="icon" type="image/svg+xml" href="icon.svg">
<link rel="alternate icon" href="icon.svg">
<title>Game - Valley of the Commons</title>
<link rel="stylesheet" href="game.css">
</head>
<body>
<!-- Commons Hub Home Link, GitHub Repo Link & Privacy FAQ -->
<div class="top-links">
<a href="index.html" class="home-link" title="Commons Hub - Home" aria-label="Go to Commons Hub home">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
</svg>
</a>
<a href="https://github.com/understories/votc" target="_blank" rel="noopener" class="github-link" title="open source repo" aria-label="View source code on GitHub">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
</a>
<a href="privacy.html" class="privacy-link" title="Privacy & Open Source FAQ" aria-label="Privacy and open source information">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"/>
</svg>
</a>
</div>
<div class="terminal">
<div class="terminal-content">
<div class="terminal-line">
<span class="prompt">$</span>
<span class="question">Want to help create a game that shapes reality?</span>
</div>
<div id="input-line" class="terminal-line">
<span class="prompt">></span>
<input type="text" id="terminal-input" autocomplete="off" spellcheck="false" aria-label="Type your message to the game master">
<span class="cursor"></span>
<button id="submit-button" class="submit-button" type="button" aria-label="Send message">Enter</button>
</div>
<div id="full-chat-share-container" class="full-chat-share-container"></div>
<div id="output" class="terminal-output"></div>
</div>
</div>
<!-- LLM Information Footer -->
<div class="llm-info">
<p>
Powered by <span id="model-name">Mistral Devstral-2</span> via
<a href="https://vercel.com/docs/ai/ai-gateway" target="_blank" rel="noopener">Vercel AI Gateway</a>
</p>
<p class="model-note">
Note: Some models used in MVP are proprietary APIs.
Long-term roadmap includes migration to open-weights or self-hosted models.
</p>
</div>
<script src="game.js"></script>
</body>
</html>

895
game.js Normal file
View File

@ -0,0 +1,895 @@
// Game Terminal Chat Interface
// Implements Socratic game master moderator with text streaming
document.addEventListener('DOMContentLoaded', function() {
const input = document.getElementById('terminal-input');
const output = document.getElementById('output');
const cursor = document.querySelector('.cursor');
// Detect mobile device
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
(window.matchMedia && window.matchMedia('(max-width: 768px)').matches);
let messageHistory = [];
let turnCount = 0;
const MAX_TURNS = 12;
let selectedText = null;
let selectedMessages = [];
// Auto-focus only on desktop (not mobile to avoid immediate keyboard)
if (!isMobile) {
input.focus();
}
// Initial question is set in HTML
// After first response, Socratic game master moderator takes over
async function sendMessage(userInput) {
// Check turn limit
if (turnCount >= MAX_TURNS) {
displayMessage('system', 'Conversation limit reached. The game master has stepped away.');
return;
}
// Add user message to history
messageHistory.push({ role: 'user', content: userInput });
turnCount++;
// Display user message
displayMessage('user', userInput);
// Show loading indicator
showLoadingIndicator('AI is thinking...');
try {
const response = await fetch('/api/game-chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: messageHistory })
});
if (!response.ok) {
const errorText = await response.text().catch(() => '');
let error;
try {
error = JSON.parse(errorText);
} catch {
error = { error: errorText || `API error: ${response.status}` };
}
throw new Error(error.error || `API error: ${response.status}`);
}
// Check if response body exists
if (!response.body) {
console.error('[game] No response body received');
throw new Error('No response body received');
}
console.log('[game] Response received, starting to read stream');
// Text stream (simple: just append chunks)
const reader = response.body.getReader();
const decoder = new TextDecoder();
let aiMessage = '';
// Create streaming message element
const streamingElement = createStreamingMessageElement();
console.log('[game] Streaming element created');
try {
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log('[game] Stream complete, received', aiMessage.length, 'characters');
break;
}
// Decode chunk and append directly (text stream, no parsing needed)
const chunk = decoder.decode(value, { stream: true });
console.log('[game] Chunk received:', chunk.substring(0, 50), 'length:', chunk.length);
if (chunk) {
aiMessage += chunk;
// Update UI (typewriter effect)
updateStreamingMessage(streamingElement, aiMessage);
}
}
} catch (streamError) {
console.error('[game] Stream reading error:', streamError);
throw streamError;
}
// Finalize streaming element
finalizeStreamingMessage(streamingElement, 'assistant', aiMessage);
// Add to history
messageHistory.push({ role: 'assistant', content: aiMessage });
hideLoadingIndicator();
// Show full chat share button if we have conversation history
if (messageHistory.length >= 2) {
showFullChatShareButton();
}
// Check if approaching limit
if (turnCount >= MAX_TURNS - 2) {
displayMessage('system', `[${MAX_TURNS - turnCount} exchanges remaining]`);
}
} catch (error) {
displayError('Connection error. Please try again.');
hideLoadingIndicator();
console.error('Chat error:', error);
}
}
function displayMessage(role, content) {
const output = document.getElementById('output');
const line = document.createElement('div');
line.className = 'terminal-line message-line';
line.dataset.role = role;
line.dataset.content = content;
const prompt = document.createElement('span');
prompt.className = 'prompt';
prompt.textContent = role === 'user' ? '>' : '$';
prompt.style.flexShrink = '0'; // Prevent prompt from shrinking
const text = document.createElement('span');
text.className = role === 'user' ? 'user-message' : 'ai-message';
text.textContent = content;
line.appendChild(prompt);
line.appendChild(text);
output.appendChild(line);
output.scrollTop = output.scrollHeight;
// Add click handler for selection
line.addEventListener('click', function() {
selectMessage(line, role, content);
});
}
function createStreamingMessageElement() {
const output = document.getElementById('output');
const line = document.createElement('div');
line.className = 'terminal-line';
line.style.display = 'flex';
line.style.alignItems = 'flex-start';
line.style.gap = '0.5rem';
const prompt = document.createElement('span');
prompt.className = 'prompt';
prompt.textContent = '$';
prompt.style.flexShrink = '0';
const text = document.createElement('span');
text.className = 'ai-message ai-message-streaming';
text.textContent = '';
text.style.flex = '1';
line.appendChild(prompt);
line.appendChild(text);
output.appendChild(line);
return text;
}
function updateStreamingMessage(element, content) {
if (!element) {
console.error('[game] updateStreamingMessage: element is null');
return;
}
element.textContent = content;
const output = document.getElementById('output');
if (output) {
output.scrollTop = output.scrollHeight;
}
console.log('[game] Updated streaming message, length:', content.length);
}
function finalizeStreamingMessage(element, role, content) {
element.classList.remove('ai-message-streaming');
// Add dataset attributes for message selection
const line = element.closest('.terminal-line');
if (line) {
line.classList.add('message-line');
const finalRole = role || 'assistant';
const finalContent = content || element.textContent;
line.dataset.role = finalRole;
line.dataset.content = finalContent;
// Ensure proper layout (flexbox already set in createStreamingMessageElement)
// Add click handler for selection
line.addEventListener('click', function() {
selectMessage(line, finalRole, finalContent);
});
}
}
function showLoadingIndicator(message) {
const output = document.getElementById('output');
const indicator = document.createElement('div');
indicator.className = 'terminal-line loading-indicator';
indicator.id = 'loading-indicator';
const prompt = document.createElement('span');
prompt.className = 'prompt';
prompt.textContent = '$';
const text = document.createElement('span');
text.className = 'loading-text';
text.textContent = message;
indicator.appendChild(prompt);
indicator.appendChild(text);
output.appendChild(indicator);
output.scrollTop = output.scrollHeight;
}
function hideLoadingIndicator() {
const indicator = document.getElementById('loading-indicator');
if (indicator) {
indicator.remove();
}
}
function displayError(message) {
const output = document.getElementById('output');
const line = document.createElement('div');
line.className = 'terminal-line error-message';
const prompt = document.createElement('span');
prompt.className = 'prompt';
prompt.textContent = '!';
prompt.style.color = '#ff4444';
const text = document.createElement('span');
text.className = 'error-text';
text.textContent = message;
text.style.color = '#ff4444';
line.appendChild(prompt);
line.appendChild(text);
output.appendChild(line);
output.scrollTop = output.scrollHeight;
}
// Handle Enter key and submit button
function handleSubmit() {
const userInput = input.value.trim();
if (userInput) {
sendMessage(userInput);
input.value = '';
// Only auto-focus on desktop (not mobile)
if (!isMobile) {
input.focus();
}
}
}
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
handleSubmit();
}
});
// Submit button for mobile
const submitButton = document.getElementById('submit-button');
if (submitButton) {
submitButton.addEventListener('click', handleSubmit);
}
// Keep focus on input (only on desktop, not mobile to avoid interference)
if (!isMobile) {
input.addEventListener('blur', function() {
if (turnCount < MAX_TURNS) {
input.focus();
}
});
}
// Text selection functionality
function selectMessage(line, role, content) {
// Clear previous selection
clearSelection();
// Select this message
line.classList.add('selected');
selectedText = `${role === 'user' ? '>' : '$'} ${content}`;
selectedMessages = [{ role, content }];
// Show share button
showShareButton();
}
function clearSelection() {
document.querySelectorAll('.message-line.selected').forEach(line => {
line.classList.remove('selected');
});
selectedText = null;
selectedMessages = [];
hideShareButton();
}
function showShareButton() {
// Remove existing buttons if any
hideShareButton();
const output = document.getElementById('output');
const buttonContainer = document.createElement('div');
buttonContainer.id = 'share-buttons-container';
buttonContainer.className = 'share-buttons-container';
// Share button (opens modal)
const shareButton = document.createElement('button');
shareButton.id = 'share-btn';
shareButton.className = 'share-button share-button-default';
shareButton.textContent = 'share on github repo';
shareButton.addEventListener('click', () => showShareModal());
buttonContainer.appendChild(shareButton);
output.appendChild(buttonContainer);
}
function hideShareButton() {
const container = document.getElementById('share-buttons-container');
if (container) {
container.remove();
}
}
function showFullChatShareButton() {
// Remove existing button if any
hideFullChatShareButton();
const container = document.getElementById('full-chat-share-container');
if (!container) return;
const button = document.createElement('button');
button.id = 'full-chat-share-btn';
button.className = 'share-button share-button-own';
button.textContent = 'add whole chat history to github repo';
button.addEventListener('click', () => showShareFullChatModal());
container.appendChild(button);
}
function hideFullChatShareButton() {
const container = document.getElementById('full-chat-share-container');
if (container) {
container.innerHTML = '';
}
}
function generateIdeaTemplate(selectedText, selectedMessages, fullHistory) {
const timestamp = new Date().toISOString();
const date = new Date().toLocaleString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short'
});
// Extract key themes from conversation context
const assistantMessages = fullHistory.filter(m => m.role === 'assistant').map(m => m.content);
// Generate a title suggestion from selected text (first line or first 60 chars)
const firstLine = selectedText.split('\n')[0];
const titleSuggestion = firstLine.length > 60
? firstLine.substring(0, 60).trim() + '...'
: firstLine.trim();
// Extract key themes from recent assistant messages
const keyThemes = assistantMessages.length > 0
? assistantMessages.slice(-3).map(msg => {
// Extract first question or key phrase (first sentence or first 100 chars)
const firstSentence = msg.split(/[.!?]/)[0] || msg.substring(0, 100);
return firstSentence.trim();
}).filter(t => t.length > 0)
: ['Emerging from dialogue'];
return `# Idea: ${titleSuggestion}
**Source:** Valley of the Commons Game Master Dialogue
**Date:** ${date}
**Selected Excerpt:**
${selectedText}
---
## Context
This idea emerged from a Socratic dialogue about game design in the Valley of the Commons.
**Key Themes:**
${keyThemes.map(theme => `- ${theme}`).join('\n')}
---
## Next Steps
- [ ] Refine this idea
- [ ] Connect to other ideas
- [ ] Propose as a tool/rule/quest/place
- [ ] Document implementation approach
---
## Full Conversation History
${formatChatHistory(fullHistory)}
---
*Generated from game master conversation*`;
}
function generateFullChatTemplate(fullHistory) {
const timestamp = new Date().toISOString();
const date = new Date().toLocaleString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short'
});
// Generate title from first user message or first AI response
const firstUserMessage = fullHistory.find(m => m.role === 'user');
const firstAIMessage = fullHistory.find(m => m.role === 'assistant');
const titleSource = firstUserMessage?.content || firstAIMessage?.content || 'Conversation';
const titleSuggestion = titleSource.length > 60
? titleSource.substring(0, 60).trim() + '...'
: titleSource.trim();
// Extract key themes from assistant messages
const assistantMessages = fullHistory.filter(m => m.role === 'assistant').map(m => m.content);
const keyThemes = assistantMessages.length > 0
? assistantMessages.slice(0, 5).map(msg => {
const firstSentence = msg.split(/[.!?]/)[0] || msg.substring(0, 100);
return firstSentence.trim();
}).filter(t => t.length > 0)
: ['Emerging from dialogue'];
return `# Conversation: ${titleSuggestion}
**Source:** Valley of the Commons Game Master Dialogue
**Date:** ${date}
**Message Count:** ${fullHistory.length} exchanges
---
## Context
This is a complete Socratic dialogue about game design in the Valley of the Commons.
**Key Themes:**
${keyThemes.map(theme => `- ${theme}`).join('\n')}
---
## Full Conversation History
${formatChatHistory(fullHistory)}
---
## Next Steps
- [ ] Extract specific ideas from this conversation
- [ ] Connect themes to other conversations
- [ ] Propose tools/rules/quests/places based on insights
- [ ] Document implementation approaches
---
*Generated from complete game master conversation*`;
}
function formatChatHistory(history) {
if (!history || history.length === 0) return 'No conversation history available.';
return history.map((msg, index) => {
const prefix = msg.role === 'user' ? '>' : '$';
const roleLabel = msg.role === 'user' ? 'User' : 'Game Master';
return `### ${roleLabel} (${index + 1})
${prefix} ${msg.content}`;
}).join('\n\n---\n\n');
}
function showShareFullChatModal() {
if (!messageHistory || messageHistory.length === 0) {
displayError('No conversation history to share');
return;
}
// Get full conversation history
const fullHistory = messageHistory;
// Generate template for full chat
const template = generateFullChatTemplate(fullHistory);
// Create modal
const modal = document.createElement('div');
modal.id = 'share-modal';
modal.className = 'share-modal';
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-labelledby', 'share-modal-title-full');
modal.innerHTML = `
<div class="share-modal-content">
<div class="share-modal-header">
<h2 id="share-modal-title-full">Share Full Chat History to GitHub</h2>
<button class="share-modal-close" id="share-modal-close" aria-label="Close dialog">&times;</button>
</div>
<div class="share-modal-body">
<div class="share-template-section">
<label>Full Conversation Template (editable):</label>
<textarea id="share-template-editor" class="share-template-editor">${escapeHtml(template)}</textarea>
</div>
<div class="share-history-section">
<label>Conversation Summary:</label>
<div class="share-history-viewer" id="share-history-viewer">
<div class="terminal-line">Total messages: ${fullHistory.length}</div>
<div class="terminal-line">User messages: ${fullHistory.filter(m => m.role === 'user').length}</div>
<div class="terminal-line">AI messages: ${fullHistory.filter(m => m.role === 'assistant').length}</div>
</div>
</div>
</div>
<div class="share-modal-footer">
<button class="share-button share-button-own" id="share-modal-cancel">Cancel</button>
<button class="share-button share-button-default" id="share-modal-share-default">Share (directly with dev github vrnvrn)</button>
<button class="share-button share-button-own" id="share-modal-share-own">Share (open github in new tab)</button>
</div>
</div>
`;
document.body.appendChild(modal);
// Focus management for accessibility
const closeButton = document.getElementById('share-modal-close');
// Focus the close button first (for keyboard navigation)
setTimeout(() => {
closeButton.focus();
}, 100);
// Event listeners
closeButton.addEventListener('click', () => hideShareModal());
document.getElementById('share-modal-cancel').addEventListener('click', () => hideShareModal());
document.getElementById('share-modal-share-default').addEventListener('click', () => {
const content = document.getElementById('share-template-editor').value;
shareToGitHub('default', content, fullHistory, true); // Full chat, save to conversations directory
});
document.getElementById('share-modal-share-own').addEventListener('click', () => {
const content = document.getElementById('share-template-editor').value;
shareToGitHub('own', content, fullHistory, true); // Full chat, save to conversations directory
});
// Close on outside click
modal.addEventListener('click', (e) => {
if (e.target === modal) {
hideShareModal();
}
});
// Close on Escape key
const handleEscape = (e) => {
if (e.key === 'Escape') {
hideShareModal();
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
}
function showShareModal() {
if (!selectedText || selectedMessages.length === 0) {
displayError('No text selected');
return;
}
// Get full conversation history
const fullHistory = messageHistory;
// Generate template
const template = generateIdeaTemplate(selectedText, selectedMessages, fullHistory);
// Create modal
const modal = document.createElement('div');
modal.id = 'share-modal';
modal.className = 'share-modal';
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-labelledby', 'share-modal-title-idea');
modal.innerHTML = `
<div class="share-modal-content">
<div class="share-modal-header">
<h2 id="share-modal-title-idea">Share Idea to GitHub</h2>
<button class="share-modal-close" id="share-modal-close" aria-label="Close dialog">&times;</button>
</div>
<div class="share-modal-body">
<div class="share-template-section">
<label>Idea Template (editable):</label>
<textarea id="share-template-editor" class="share-template-editor">${escapeHtml(template)}</textarea>
</div>
<div class="share-history-section">
<label>Full Conversation History:</label>
<div class="share-history-viewer" id="share-history-viewer">
${formatChatHistoryForDisplay(fullHistory)}
</div>
</div>
</div>
<div class="share-modal-footer">
<button class="share-button share-button-own" id="share-modal-cancel">Cancel</button>
<button class="share-button share-button-default" id="share-modal-share-default">Share (directly with dev github vrnvrn)</button>
<button class="share-button share-button-own" id="share-modal-share-own">Share (open github in new tab)</button>
</div>
</div>
`;
document.body.appendChild(modal);
// Focus management for accessibility
const closeButton = document.getElementById('share-modal-close');
const textarea = document.getElementById('share-template-editor');
// Focus the close button first (for keyboard navigation)
setTimeout(() => {
closeButton.focus();
}, 100);
// Event listeners
closeButton.addEventListener('click', () => hideShareModal());
document.getElementById('share-modal-cancel').addEventListener('click', () => hideShareModal());
document.getElementById('share-modal-share-default').addEventListener('click', () => {
const content = document.getElementById('share-template-editor').value;
shareToGitHub('default', content, fullHistory, false); // Selected excerpt, not full chat
});
document.getElementById('share-modal-share-own').addEventListener('click', () => {
const content = document.getElementById('share-template-editor').value;
shareToGitHub('own', content, fullHistory, false); // Selected excerpt, not full chat
});
// Close on outside click
modal.addEventListener('click', (e) => {
if (e.target === modal) {
hideShareModal();
}
});
// Close on Escape key
const handleEscape = (e) => {
if (e.key === 'Escape') {
hideShareModal();
document.removeEventListener('keydown', handleEscape);
}
};
document.addEventListener('keydown', handleEscape);
}
function formatChatHistoryForDisplay(history) {
if (!history || history.length === 0) return '<div class="terminal-line">No conversation history available.</div>';
return history.map((msg) => {
const prefix = msg.role === 'user' ? '>' : '$';
const roleClass = msg.role === 'user' ? 'user-message' : 'assistant-message';
return `<div class="terminal-line ${roleClass}">${prefix} ${escapeHtml(msg.content)}</div>`;
}).join('');
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function hideShareModal() {
const modal = document.getElementById('share-modal');
if (modal) {
// Return focus to the element that opened the modal (if available)
const previouslyFocused = document.activeElement;
modal.remove();
// Focus back to input if it was previously focused
if (input && document.activeElement === document.body) {
input.focus();
}
}
}
async function shareToGitHub(method, content, fullHistory, isFullChat = false) {
if (!content) {
displayError('No content to share');
return;
}
const shareButton = document.getElementById(`share-modal-share-${method === 'default' ? 'default' : 'own'}`);
const cancelButton = document.getElementById('share-modal-cancel');
if (shareButton) {
shareButton.disabled = true;
shareButton.textContent = 'Sharing...';
}
if (cancelButton) {
cancelButton.disabled = true;
}
if (method === 'own') {
// Redirect to GitHub to create file directly with user's account
shareWithOwnAccount(content, isFullChat);
hideShareModal();
return;
}
// Default: Use API with default account
try {
const response = await fetch('/api/share-to-github', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: content, // Full template with chat history
excerpt: selectedText, // Keep for backwards compatibility
isFullChat: isFullChat, // Flag to save to conversations directory
context: {
messages: selectedMessages,
fullHistory: fullHistory
}
})
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to share');
}
// Show success message
displaySuccess(data.url, data.filename);
// Clear selection and close modal
hideShareModal();
clearSelection();
} catch (error) {
displayError(`Failed to share: ${error.message}`);
if (shareButton) {
shareButton.disabled = false;
shareButton.textContent = method === 'default' ? 'Share (directly with dev github vrnvrn)' : 'Share (open github in new tab)';
}
if (cancelButton) {
cancelButton.disabled = false;
}
}
}
async function shareWithOwnAccount(content, isFullChat = false) {
// Determine directory and filename based on whether it's a full chat or excerpt
const basePath = isFullChat ? 'build_game/conversations' : 'build_game/ideas';
const prefix = isFullChat ? 'conversation' : 'idea';
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const filename = `${prefix}-${year}-${month}-${day}-${hours}${minutes}${seconds}.md`;
const filePath = `${basePath}/${filename}`;
const owner = 'understories';
const repo = 'votc';
const branch = 'main';
// Open GitHub's "Create new file" interface
// This will prompt user to fork if they don't have write access
// The filename is pre-filled, user just needs to paste content
const createFileUrl = `https://github.com/${owner}/${repo}/new/${branch}?filename=${encodeURIComponent(filePath)}`;
// Copy content to clipboard
let clipboardSuccess = false;
try {
await navigator.clipboard.writeText(content);
clipboardSuccess = true;
} catch (clipboardError) {
console.error('Clipboard API failed:', clipboardError);
}
// Open GitHub in new tab
window.open(createFileUrl, '_blank', 'noopener,noreferrer');
// Always show content in a copyable box for easy access
const output = document.getElementById('output');
const fileType = isFullChat ? 'conversation' : 'idea';
// Show message
if (clipboardSuccess) {
displayMessage('system', `Opened GitHub file creation page. Content copied to clipboard! Paste (Cmd/Ctrl+V) into the editor. If clipboard didn't work, copy from the box below:`);
} else {
displayMessage('system', `Opened GitHub file creation page. Copy the content from the box below and paste it into GitHub's editor:`);
}
// Display content in a copyable format
const contentBox = document.createElement('div');
contentBox.className = 'terminal-line';
contentBox.style.marginTop = '1rem';
contentBox.style.padding = '1rem';
contentBox.style.backgroundColor = '#111';
contentBox.style.border = '2px solid #00ff00';
contentBox.style.borderRadius = '4px';
contentBox.style.maxHeight = '400px';
contentBox.style.overflowY = 'auto';
contentBox.style.whiteSpace = 'pre-wrap';
contentBox.style.fontSize = '0.85rem';
contentBox.style.color = '#00ff00';
contentBox.style.fontFamily = 'Courier New, Monaco, Menlo, monospace';
contentBox.style.lineHeight = '1.6';
contentBox.textContent = content;
contentBox.style.cursor = 'text';
contentBox.setAttribute('contenteditable', 'true');
contentBox.setAttribute('spellcheck', 'false');
// Add a "Select All" button
const selectAllBtn = document.createElement('button');
selectAllBtn.textContent = 'Select All';
selectAllBtn.className = 'share-button share-button-default';
selectAllBtn.style.marginTop = '0.5rem';
selectAllBtn.style.fontSize = '0.8rem';
selectAllBtn.style.padding = '0.4rem 0.8rem';
selectAllBtn.addEventListener('click', () => {
const range = document.createRange();
range.selectNodeContents(contentBox);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
});
const container = document.createElement('div');
container.style.marginTop = '0.5rem';
container.appendChild(contentBox);
container.appendChild(selectAllBtn);
output.appendChild(container);
output.scrollTop = output.scrollHeight;
}
function displaySuccess(url, filename, accountInfo = '') {
const output = document.getElementById('output');
const line = document.createElement('div');
line.className = 'terminal-line success-message';
const prompt = document.createElement('span');
prompt.className = 'prompt';
prompt.textContent = '✓';
prompt.style.color = '#00ff00';
const text = document.createElement('span');
text.className = 'success-text';
const accountNote = accountInfo ? ` (${accountInfo})` : '';
text.innerHTML = `Shared! <a href="${url}" target="_blank" rel="noopener" style="color: #00ff00; text-decoration: underline;">View on GitHub</a>${accountNote}`;
text.style.color = '#00ff00';
line.appendChild(prompt);
line.appendChild(text);
output.appendChild(line);
output.scrollTop = output.scrollHeight;
// Auto-dismiss after 5 seconds
setTimeout(() => {
line.remove();
}, 5000);
}
// Click outside to deselect
document.addEventListener('click', function(e) {
if (!e.target.closest('.message-line') && !e.target.closest('.share-button')) {
clearSelection();
}
});
});

12
icon.svg Normal file
View File

@ -0,0 +1,12 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Black background -->
<rect width="32" height="32" fill="#000000"/>
<!-- Light gray circle -->
<circle cx="16" cy="16" r="14" fill="#B0B0B0"/>
<!-- Dark gray mountain shape - exact path from understories, scaled to fit in circle -->
<g transform="translate(4, 6) scale(0.9)">
<path d="M14 6l-3.75 5 2.85 3.8-1.6 1.2C9.81 13.75 7 10 7 10l-6 8h22L14 6z" fill="#4A4A4A"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 505 B

View File

@ -3,36 +3,271 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Valley of the Commons</title>
<link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/svg+xml" href="icon.svg">
<link rel="alternate icon" href="icon.svg">
<title>Valley of the Commons | Commons Hub</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Inter:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
<script src="waitlist.js" defer></script>
<script src="speakers.js" defer></script>
<script src="community-partners.js" defer></script>
</head>
<body>
<div class="container">
<header>
<body id="top">
<!-- Loading Screen -->
<div id="loading-screen" class="loading-screen">
<button id="skip-loading" class="skip-loading-button">Skip</button>
<div class="loading-image-container">
<video id="loading-video" class="loading-image" autoplay muted playsinline preload="auto" poster="photos/shortintro-poster.png">
<source src="photos/shortintro.mov" type="video/quicktime">
<source src="photos/shortintro.mov" type="video/mp4">
</video>
</div>
<div class="loading-title">
<h1>Valley of the Commons</h1>
<p class="tagline">Coming Soon</p>
</header>
<main>
<p class="description">
A space for collaborative stewardship and shared abundance.
</p>
<div class="signup">
<p>Stay updated on our progress:</p>
<form class="email-form" action="#" method="post">
<input type="email" placeholder="your@email.com" required>
<button type="submit">Notify Me</button>
</form>
</div>
</main>
<footer>
<p>&copy; 2025 Valley of the Commons</p>
</footer>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const loadingVideo = document.getElementById('loading-video');
if (loadingVideo) {
loadingVideo.playbackRate = 0.6;
}
});
</script>
<!-- Header -->
<header class="header">
<div class="header-container">
<div class="logo">
<img src="commonshublogo.svg" alt="Commons Hub">
</div>
<nav class="nav">
<a href="#top">Home</a>
<a href="#schedule">Schedule</a>
<a href="#collaborators" class="nav-speakers">
<span class="nav-speakers-full">Collaborators</span>
<span class="nav-speakers-short">Collaborators</span>
</a>
<a href="#explore">Explore the Valley</a>
<a href="#waitlist" class="nav-get-involved">Get involved</a>
<a href="game.html" target="_blank" rel="noopener noreferrer" class="nav-rabbit">🐰</a>
<a href="https://blocksurvey.io/valley-of-the-commons-application-form-NVCeexCKTBunKj8xOlLJ_g?v=o" target="_blank" rel="noopener noreferrer" class="nav-cta-button">APPLY NOW</a>
</nav>
</div>
</header>
<!-- Hero Section -->
<section class="hero">
<div class="hero-image">
<img src="valley.webp" alt="Valley of the Commons">
</div>
<div class="hero-content">
<h1>Valley of the Commons</h1>
<p class="hero-tagline">a village built on common ground</p>
<p class="hero-subtitle">Valley of the Commons is a four-week pop-up village by a nascent network society envisioning life beyond extractive systems.</p>
<p class="hero-subtitle">Rooted at <a href="https://www.commons-hub.at/" target="_blank" rel="noopener noreferrer">commons hub</a> and held by the forests, mountains, and open skies of the Austrian Alps, this gathering is a living commons shared in work and study, in making and care, in governance and everyday life.</p>
<p class="hero-subtitle">Together, we lay the foundations for permanence by exploring housing, production, decision-making and ownership in community.</p>
</div>
</section>
<!-- Event Dates Banner -->
<section class="event-dates-banner">
<div class="event-dates-content">
<h2 class="event-title">Pop-Up Event to Seed the Valley</h2>
<p class="event-dates">24 August 2026 20 September 2026</p>
</div>
<a href="https://blocksurvey.io/valley-of-the-commons-application-form-NVCeexCKTBunKj8xOlLJ_g?v=o" target="_blank" rel="noopener noreferrer" class="cta-button">APPLY NOW</a>
</section>
<!-- Main Content -->
<main class="main-content">
<!-- What — A Popup Building Toward Permanence -->
<section class="section">
<!--What — A popup building toward permanence</h2>-->
<p>Valley of the Commons emerges from years of hands-on experimentation with long-format community events. From co-organizing Zu-villages and Invisible Gardens to bootstrapping the commons hub, our team has ample experience in prototyping early nodes of future societies.</p>
<p>Over the last few years, popups have evolved into a cultural phenomenon: distributed communities gathering for multiple weeks to test new forms of living, governance, and collective purpose. These experiments have produced invaluable insights into technical innovations, governance mechanisms and financial tools enabling future network societies.</p>
<p>Building on that momentum, we believe it's time to shift gears - to make the social, productive, and infrastructural commons as tangible as the tech stack beneath.</p>
<p>The Popup is designed as a practice-oriented phase of preparation toward our own permanence. Over four weeks, participants will actively explore central aspects of a future settlement in theory and practice, while building relationships, clarifying shared priorities, and co-shaping the first real building blocks of the commons-centric network society waiting to be born.</p>
<p>The program will revolve around three guiding themes, positioned in the context of current global transformations and the renewed relevance of the commons:</p>
<ul class="feature-list">
<li>Cosmo-local production & open value accounting</li>
<li>Nomad-friendly communal life in housing co-ops</li>
<li>Horizontal governance & funding mechanisms</li>
</ul>
<p>Beyond these core tracks, participants are invited to introduce complementary topics they see relevant, and explore how their individual projects might integrate with the perspectives we weave as a community.</p>
<p>This popup is not a simulation. It is a real co-living community taking steps toward valley transformation. At the same time, its outcomes feed back into the broader conversation on transitioning network societies from temporary concentrations of talent into durable, regenerative forms of collective life.</p>
</section>
<img src="photos/hubfront.jpg" alt="Commons Hub Front" class="schedule-photo">
<!-- The Return of the Commons -->
<section class="section">
<h2>The Return of the Commons</h2>
<p>Across Europe and beyond, digital workers, post-corporate professionals, and remote-friendly entrepreneurs seek grounded, cooperative ways of living, beyond the isolation of the metropolis and the fragility of atomized nomadism.</p>
<p>This turn toward intentional communities is a structural process enabled by remote work, motivated by a longing for connection and nature, and driven by the need to hedge against rising volatility from climate shocks, fragile supply chains, precarious employment, and social or environmental degradation.</p>
<p>As corporate capitalism fails harder and harder, the urgent need for socio-technical systems, resilient enough to endure the coming decades, becomes apparent. The concepts are here, but we need real places, shared practices, and long-term structures to anchor them in daily life. This is why we build the Valley of the Commons.</p>
</section>
<!-- Why Here -->
<section class="section">
<!-- <h2>Why here</h2> -->
<h3>Venue: <a href="https://www.commons-hub.at/" target="_blank" rel="noopener noreferrer">Commons Hub</a></h3>
<p>As an experimental playground for regenerative systems design, the commons hub explores the liberatory potential of emerging technologies and social techniques that facilitate horizontal communities.</p>
<p>Drawing on years of experience in managing creative chaos, it provides the stability and freedom to let emerging communities bloom. Its facilities versatile event spaces, a fablab, atelier, and outdoor gym are complemented by self-organizing practices, gently guiding creative energy into productive flows.</p>
<p>In short, this village does not start from zero. It sprouts from a living, breathing center of the commons movement that offers an economic engine, a platform for entrepreneurship, and a beacon for metamodern thought and praxis.</p>
<img src="photos/campfire.png" alt="Campfire at Commons Hub" class="schedule-photo">
<h3>Location: Höllental</h3>
<p>Just an hour south of Vienna, Höllental closes the gentle Danube basin with a dramatic landscape of high peaks, steep cliffs, and cold rushing streams framed by dense forests. It feels remote, almost untouched, yet remains remarkably accessible: nearly 10 million people live within a three-hour drive, and Viennas international airport is under two hours away by rail.</p>
<p>Formerly a famed imperial summer retreat and wood-industry hub, the valley spent the last century dormant, its villages slowly decaying.</p>
<p>Recent signs of reversal point to a shift toward outdoor tourism, as hotels renovate and factories close for good. And yet, real estate remains affordable, with many houses and villas still awaiting new purpose.</p>
<p>Adding to its beauty and accessibility, the opportunities on the ground make this valley ideal for building for permanence.</p>
<p><a href="#explore" class="explore-link">Explore the Valley →</a></p>
</section>
<!-- Schedule Section -->
<section id="schedule" class="section schedule-section">
<h2>Schedule</h2>
<div class="schedule-intro">
<p>The four-week popup follows a simple rhythm: mornings are structured learning paths, afternoons host workshops, field visits, and working groups, and evenings are reserved for shared life, reflection, and communal time.</p>
<p>Weekends remain open for exploration and connection with local initiatives. Life happens in between: shared meals, scenic hikes, river plunges, mushroom foraging, fire circles, late-night conversations under alpine stars, and much more.</p>
<p>We create these moments together, each contributing their own skills, energy, and quirks to the village.</p>
</div>
<img src="photos/gardenfront.jpg" alt="Gardenfront at Commons Hub" class="schedule-photo">
<div class="schedule-weeks">
<div class="schedule-week">
<a href="#" class="read-more-link" onclick="event.preventDefault(); this.closest('.schedule-week').classList.toggle('expanded'); this.textContent = this.closest('.schedule-week').classList.contains('expanded') ? 'click to collapse' : 'READ MORE v'; return false;">READ MORE v</a>
<h3>Week 1</h3><p class="week-theme">Return of the Commons</p>
<p class="week-dates">24 30 August 2026</p>
<p class="week-description">
<span class="week-description-preview">To set the scene, we begin with a fiveday course led by Michel Bauwens and Adam Arvidsson, two of the most insightful and engaging contemporary thinkers on the commons. Expect a wild ride through history, economic and sociological theory, peppered with anecdotes, examples, and insights.</span>
<span class="week-description-full">Bauwens, founder of the P2P Foundation, brings a visionary lens, exploring the transformative potential of commons-based systems and their central role in post-capitalist collaboration. Arvidssons work instead turns the focus to how commons emerge in everyday production and social life where capitalism fails. Together, their perspectives offer both inspiration and grounded insight: a visionary account of the commons civilizational potential, and a practical, historically informed view of how commons-based social and productive forms can arise as capitalisms reach and allure contract. Afternoons provide a practical counterpart: workshops, small-group exercises, and working sessions allow participants to translate theory into practice, exploring how commons ideas can be operationalized in daily life, collaborative projects, and the emerging Valley of the Commons.</span>
</p>
</div>
<div class="schedule-week">
<a href="#" class="read-more-link" onclick="event.preventDefault(); this.closest('.schedule-week').classList.toggle('expanded'); this.textContent = this.closest('.schedule-week').classList.contains('expanded') ? 'click to collapse' : 'READ MORE v'; return false;">READ MORE v</a>
<h3>Week 2</h3><p class="week-theme">Cosmo-local Production & Open Value Accounting (OVA)</p>
<p class="week-dates">31 August 6 September 2026</p>
<p class="week-description">
<span class="week-description-preview">In the second week, we shift from macro-narratives to the mechanics of how a village economy could actually function. Through a mix of theory sessions and hands-on workshops, participants explore how global knowledge commons and local production capacities can sustain livelihoods, small-scale industry, and community resilience.</span>
<span class="week-description-full">We look at business models for cosmo-local manufacturing, mutual credit and community currencies, distributed supply chains, and the workflows that make small-scale fabrication viable. Alongside the economic layer, we dive into open value accounting: reviewing existing models, examining where they succeed (or fail), and collectively selecting a pragmatic framework that we can begin testing immediately. The emphasis throughout the week is on feasibility rather than perfection, focusing on economic structures that real people can actually use. Afternoons extend these concepts into practice: prototyping small production workflows, mapping local resources, drafting economic scenarios, and experimenting with simple Open Value Accounting tools that could underpin the emerging Valley of the Commons. This week is where abstract ideas meet tangible economic strategy.</span>
</p>
</div>
<div class="schedule-week">
<a href="#" class="read-more-link" onclick="event.preventDefault(); this.closest('.schedule-week').classList.toggle('expanded'); this.textContent = this.closest('.schedule-week').classList.contains('expanded') ? 'click to collapse' : 'READ MORE v'; return false;">READ MORE v</a>
<h3>Week 3</h3><p class="week-theme">Future Living</p>
<p class="week-dates">7 13 September 2026</p>
<p class="week-description">
<span class="week-description-preview">Week three turns toward how we might actually live together architecturally, legally, ecologically, and socially. We explore cooperative housing concepts, map local resources and unused buildings, and examine the ecological potentials of the valley as a site for long-term habitation.</span>
<span class="week-description-full">Sessions cover sustainable renovation techniques, cooperative ownership structures (from co-housing to time-share to hybrid models), and governance frameworks for housing clusters grounded in subsidiarity and shared stewardship. The weeks aim is to converge on minimal, resilient living systems suited to the 21st century: homes that are affordable, adaptable, energy-efficient, and embedded in community. Afternoons translate ideas into grounded work: from site visits to sketches and spatial planning, we will develop early drafts of what a first housing cluster could look like. This is where speculative idealism meets architectural, legal, and economic reality.</span>
</p>
</div>
<div class="schedule-week">
<a href="#" class="read-more-link" onclick="event.preventDefault(); this.closest('.schedule-week').classList.toggle('expanded'); this.textContent = this.closest('.schedule-week').classList.contains('expanded') ? 'click to collapse' : 'READ MORE v'; return false;">READ MORE v</a>
<h3>Week 4</h3><p class="week-theme">Governance & Funding Models</p>
<p class="week-dates">14 20 September 2026</p>
<p class="week-description">
<span class="week-description-preview">The fourth week brings everything into the domain of stewardship: how we organize, decide, invest, and protect what we build together. We explore participatory governance frameworks, cooperative legal structures, long-term investment models, and mechanisms for holding shared assets in trust.</span>
<span class="week-description-full">Drawing on lessons from ecovillages, DAOs, co-ops, and community land trusts, participants map what actually works, and where idealistic models tend to fail in practice. Afternoons shift into applied design: drafting governance charters, testing decision-making protocols, sketching legal wrappers, and evaluating funding pathways that balance autonomy, flexibility, and long-term resilience. The week culminates in a coherent set of next steps: populating the Valley of the Commons association, defining its initial governance and funding architecture, and - potentially - outlining the first concrete co-living project. This final stretch lays the groundwork for the governance of a future permanent village.</span>
</p>
</div>
</div>
</section>
<!-- Video Section -->
<section class="section">
<div class="explore-embed">
<iframe src="https://www.youtube.com/embed/r0-6Ch_Z09s" width="100%" height="450" style="border:0;" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>
</div>
</section>
<!-- Collaborators Section -->
<section id="collaborators" class="section speakers-section">
<h2>Collaborators</h2>
<div id="speakers-container" class="speakers-container">
<!-- Speakers will be dynamically loaded here -->
</div>
</section>
<!-- Community Partners Section -->
<section id="community-partners" class="section community-partners-section">
<h2>Community Partners</h2>
<div id="partners-container" class="partners-container">
<!-- Partners will be dynamically loaded here -->
</div>
<p style="text-align: center; margin-top: var(--spacing-md);">
<a href="#waitlist" class="explore-link">Get involved →</a>
</p>
</section>
<!-- Explore the Valley Section -->
<section id="explore" class="section explore-section">
<h2>Explore the Valley</h2>
<div class="explore-embed">
<iframe src="https://www.google.com/maps/embed?pb=!4v1766310431171!6m8!1m7!1sr9uQoPJ34gG31yhEHcHduA!2m2!1d47.70606088305691!2d15.80986228902277!3f308.96847808225624!4f4.7715382336231045!5f0.7820865974627469" width="100%" height="450" style="border:0;" allowfullscreen="" loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>
</div>
</section>
<!-- Call to Action Section -->
<section id="waitlist" class="section cta-section">
<div class="cta-content">
<h2>Get involved</h2>
<p>Join as a partner, theme curator, or sponsor.</p>
<!-- Waitlist Form -->
<form id="waitlist-form" class="waitlist-form">
<div class="form-group">
<input
type="email"
id="waitlist-email"
name="email"
placeholder="Enter your email"
required
aria-label="Email address"
>
</div>
<div class="form-group">
<input
type="text"
id="waitlist-name"
name="name"
placeholder="Your name"
required
aria-label="Your name"
>
</div>
<div class="form-group">
<textarea
id="waitlist-involvement"
name="involvement"
placeholder="Describe your desired involvement."
required
aria-label="Describe your desired involvement"
rows="4"
></textarea>
</div>
<button type="submit" class="waitlist-submit">Submit</button>
<div id="waitlist-message" class="waitlist-message" role="status" aria-live="polite"></div>
</form>
<p class="cta-final">Together, we can turn vision into reality.</p>
</div>
</section>
</main>
<!-- Footer -->
<footer class="footer">
<div class="footer-container">
<p>&copy; 2026 Commons Hub. <a href="https://www.commons-hub.at" target="_blank" rel="noopener noreferrer">commons-hub.at</a></p>
</div>
</footer>
</body>
</html>

94
internal_thought.md Normal file
View File

@ -0,0 +1,94 @@
# Internal Thoughts: Game Intelligence LLM Context
## Valley of the Commons - Game Design Notes
**Purpose:** Reference document for the Socratic game master moderator
**Last Updated:** December 2025
---
## Core Concepts
### Commoners -> New Faction
- Not a faction yet as far as others are concerned
- What can a Game-like approach add to this mix?
- Generate out of the box thinking
### World Context
- World says - you can buy places, business negotiations… what else?
- Mix of mythology and real life
- Ritualistic elements
- Game can be an instrument of coming up with different kinds of operations
### Challenges & Opportunities
- Lack cultural, social capital
- Use the map and the instrument that it brings
- With projections, pieces, take snapshots of people and locations
- Turn them into physical cards (ahem)
- Out of the box moves in the valley
- Attracting games more permanently
---
## Map & Visualization
### Map as Instrument
- Map as visualization with real riddles, prompts to go out
- Map view in map room
- Projection numbers 1-50
- Physical book with quests
- Groups of people come here, mark places where things are playable
- Add things as they come in into the game
- War game ops do work like this
---
## Development Phases
### MVP: Minimalist Place
- Interesting game is developing
- Interesting material -> the reason they engage
- Hero's call
- Game commonalized in some form
- Take place in 3D printing from form
### Phase of Collecting Tools
- Gathering tools
- People help us surface which tools we end up using
- Open call to action
- Get involved to help shape the game, and thus our village
---
## Key Questions
**What can we offer to the village?**
This question remains open and should be explored through Socratic dialogue.
---
## Design Principles
1. **Game as Instrument:** The game is a tool for generating new forms of operations
2. **Physical-Digital Bridge:** Map, cards, projections connect digital and physical
3. **Community-Driven:** People mark places, add quests, surface tools
4. **Ritualistic Elements:** Mix of mythology and real life
5. **Out of the Box Thinking:** Generate unconventional approaches
6. **Commonalization:** Game becomes shared resource, common good
7. **One Question at a Time:** Always ask only one question per response to maintain focus and allow for deeper exploration
---
## Implementation Notes
- Map room with projections
- Physical cards from snapshots
- Quest book (numbers 1-50)
- 3D printing integration
- Tool collection phase
- Open call to action for participation
---
**Note for Game Master:** Use these concepts to guide Socratic questions, but never lecture. Probe with questions that help participants discover these connections themselves. **CRITICAL: Ask only one question at a time** - this maintains focus, allows for deeper exploration, and prevents overwhelming participants with multiple prompts.

View File

@ -1,27 +0,0 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/javascript application/json;
gzip_min_length 1000;
# Cache static assets
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Main location
location / {
try_files $uri $uri/ /index.html;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}

945
package-lock.json generated Normal file
View File

@ -0,0 +1,945 @@
{
"name": "valley-of-the-commons",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "valley-of-the-commons",
"version": "1.0.0",
"dependencies": {
"@ai-sdk/anthropic": "^3.0.0",
"@ai-sdk/mistral": "^3.0.0",
"@octokit/rest": "^22.0.1",
"ai": "^6.0.1",
"googleapis": "^126.0.1"
},
"devDependencies": {
"dotenv": "^16.3.1"
},
"engines": {
"node": ">=18.x"
}
},
"node_modules/@ai-sdk/anthropic": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-3.0.0.tgz",
"integrity": "sha512-4BnxkXwRkvh+OB1ze0mHbskT90HL4MNrg6JUsRDkIsU9w5vitvGzxwc/XwlByUGMap/5I8/LZ3XZDzv6KViCuQ==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "3.0.0",
"@ai-sdk/provider-utils": "4.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/gateway": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.0.tgz",
"integrity": "sha512-JcjePYVpbezv+XOxkxPemwnorjWpgDiiKWMYy6FXTCG2rFABIK2Co1bFxIUSDT4vYO6f1448x9rKbn38vbhDiA==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "3.0.0",
"@ai-sdk/provider-utils": "4.0.0",
"@vercel/oidc": "3.0.5"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/mistral": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@ai-sdk/mistral/-/mistral-3.0.0.tgz",
"integrity": "sha512-c7k/hLqXUdsbwco63kbM9mB7JA6c76kwPTtn9ulpsO8jvL+3n3+MKn0zWF7wjIZXc3kWAOBBK1zZB492hm+f3w==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "3.0.0",
"@ai-sdk/provider-utils": "4.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@ai-sdk/provider": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.0.tgz",
"integrity": "sha512-m9ka3ptkPQbaHHZHqDXDF9C9B5/Mav0KTdky1k2HZ3/nrW2t1AgObxIVPyGDWQNS9FXT/FS6PIoSjpcP/No8rQ==",
"license": "Apache-2.0",
"dependencies": {
"json-schema": "^0.4.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@ai-sdk/provider-utils": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.0.tgz",
"integrity": "sha512-HyCyOls9I3a3e38+gtvOJOEjuw9KRcvbBnCL5GBuSmJvS9Jh9v3fz7pRC6ha1EUo/ZH1zwvLWYXBMtic8MTguA==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/provider": "3.0.0",
"@standard-schema/spec": "^1.1.0",
"eventsource-parser": "^3.0.6"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/@octokit/auth-token": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
"integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
"license": "MIT",
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/core": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz",
"integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==",
"license": "MIT",
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.3",
"@octokit/request": "^10.0.6",
"@octokit/request-error": "^7.0.2",
"@octokit/types": "^16.0.0",
"before-after-hook": "^4.0.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/endpoint": {
"version": "11.0.2",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.2.tgz",
"integrity": "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/graphql": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz",
"integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==",
"license": "MIT",
"dependencies": {
"@octokit/request": "^10.0.6",
"@octokit/types": "^16.0.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/openapi-types": {
"version": "27.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz",
"integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==",
"license": "MIT"
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz",
"integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
},
"engines": {
"node": ">= 20"
},
"peerDependencies": {
"@octokit/core": ">=6"
}
},
"node_modules/@octokit/plugin-request-log": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz",
"integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==",
"license": "MIT",
"engines": {
"node": ">= 20"
},
"peerDependencies": {
"@octokit/core": ">=6"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "17.0.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz",
"integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
},
"engines": {
"node": ">= 20"
},
"peerDependencies": {
"@octokit/core": ">=6"
}
},
"node_modules/@octokit/request": {
"version": "10.0.7",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.7.tgz",
"integrity": "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA==",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^11.0.2",
"@octokit/request-error": "^7.0.2",
"@octokit/types": "^16.0.0",
"fast-content-type-parse": "^3.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/request-error": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz",
"integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^16.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/rest": {
"version": "22.0.1",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz",
"integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==",
"license": "MIT",
"dependencies": {
"@octokit/core": "^7.0.6",
"@octokit/plugin-paginate-rest": "^14.0.0",
"@octokit/plugin-request-log": "^6.0.0",
"@octokit/plugin-rest-endpoint-methods": "^17.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/types": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz",
"integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^27.0.0"
}
},
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/@vercel/oidc": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz",
"integrity": "sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==",
"license": "Apache-2.0",
"engines": {
"node": ">= 20"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/ai": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ai/-/ai-6.0.1.tgz",
"integrity": "sha512-g/jPakC6h4vUJKDww0d6+VaJmfMC38UqH3kKsngiP+coT0uvCUdQ7lpFDJ0mNmamaOyRMaY2zwEB2RnTAaJU/w==",
"license": "Apache-2.0",
"dependencies": {
"@ai-sdk/gateway": "3.0.0",
"@ai-sdk/provider": "3.0.0",
"@ai-sdk/provider-utils": "4.0.0",
"@opentelemetry/api": "1.9.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"zod": "^3.25.76 || ^4.1.8"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/before-after-hook": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
"integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
"license": "Apache-2.0"
},
"node_modules/bignumber.js": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/eventsource-parser": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
"integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/fast-content-type-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
"integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gaxios": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/gcp-metadata": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^6.1.1",
"google-logging-utils": "^0.0.2",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/google-auth-library": {
"version": "9.15.1",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^6.1.1",
"gcp-metadata": "^6.1.0",
"gtoken": "^7.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/google-logging-utils": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/googleapis": {
"version": "126.0.1",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-126.0.1.tgz",
"integrity": "sha512-4N8LLi+hj6ytK3PhE52KcM8iSGhJjtXnCDYB4fp6l+GdLbYz4FoDmx074WqMbl7iYMDN87vqD/8drJkhxW92mQ==",
"license": "Apache-2.0",
"dependencies": {
"google-auth-library": "^9.0.0",
"googleapis-common": "^7.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/googleapis-common": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz",
"integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"gaxios": "^6.0.3",
"google-auth-library": "^9.7.0",
"qs": "^6.7.0",
"url-template": "^2.0.8",
"uuid": "^9.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gtoken": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"license": "MIT",
"dependencies": {
"gaxios": "^6.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/json-bigint": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"license": "MIT",
"dependencies": {
"bignumber.js": "^9.0.0"
}
},
"node_modules/json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"license": "(AFL-2.1 OR BSD-3-Clause)"
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/universal-user-agent": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
"integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
"license": "ISC"
},
"node_modules/url-template": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
"license": "BSD"
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/zod": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz",
"integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

22
package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "valley-of-the-commons",
"version": "1.0.0",
"description": "Landing page for Valley of the Commons",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.21.0",
"@ai-sdk/anthropic": "^3.0.0",
"@ai-sdk/mistral": "^3.0.0",
"@octokit/rest": "^22.0.1",
"ai": "^6.0.1",
"googleapis": "^126.0.1"
},
"devDependencies": {
"dotenv": "^16.3.1"
},
"engines": {
"node": ">=18.x"
}
}

BIN
photos/campfire.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

BIN
photos/gardenfront.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

BIN
photos/hubfront.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 KiB

BIN
photos/shortintro.mov Normal file

Binary file not shown.

368
privacy.html Normal file
View File

@ -0,0 +1,368 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/svg+xml" href="icon.svg">
<link rel="alternate icon" href="icon.svg">
<title>Privacy & Open Source - Valley of the Commons</title>
<link rel="stylesheet" href="game.css">
<style>
/* Override body styles for privacy page to allow scrolling */
html {
overflow: auto !important;
height: auto !important;
}
body {
overflow: auto !important;
overflow-x: hidden !important;
height: auto !important;
min-height: 100vh;
min-height: 100dvh; /* Dynamic viewport height for mobile */
display: block !important;
align-items: unset !important;
justify-content: unset !important;
position: relative !important;
-webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
}
/* Override terminal styles that might interfere */
.terminal {
display: none !important;
}
.privacy-container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
padding-top: 4rem; /* Space for top links if they exist */
color: #00ff00;
font-family: 'Courier New', 'Monaco', 'Menlo', monospace;
min-height: 100vh;
min-height: 100dvh;
}
/* Mobile responsive padding */
@media (max-width: 768px) {
.privacy-container {
padding: 1rem;
padding-top: 3.5rem; /* Space for top links */
}
.privacy-header h1 {
font-size: 1.3rem;
}
.privacy-section h2 {
font-size: 1.1rem;
}
.privacy-section h3 {
font-size: 0.95rem;
}
}
.privacy-header {
margin-bottom: 2rem;
border-bottom: 1px solid #00ff00;
padding-bottom: 1rem;
}
.privacy-header h1 {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.privacy-header p {
color: #666;
font-size: 0.9rem;
}
.privacy-section {
margin-bottom: 2rem;
}
.privacy-section h2 {
font-size: 1.2rem;
color: #00ff00;
margin-bottom: 1rem;
border-left: 2px solid #00ff00;
padding-left: 1rem;
}
.privacy-section h3 {
font-size: 1rem;
color: #fff;
margin-top: 1.5rem;
margin-bottom: 0.5rem;
}
.privacy-section p {
line-height: 1.6;
margin-bottom: 1rem;
color: #ccc;
}
.privacy-section ul {
margin-left: 1.5rem;
margin-bottom: 1rem;
color: #ccc;
}
.privacy-section li {
margin-bottom: 0.5rem;
line-height: 1.5;
}
.privacy-section code {
background: #111;
padding: 0.2rem 0.4rem;
border: 1px solid #333;
color: #00ff00;
font-size: 0.9em;
}
.privacy-section a {
color: #00ff00;
text-decoration: underline;
}
.privacy-section a:hover {
text-decoration: none;
}
.back-link {
display: inline-block;
margin-top: 2rem;
padding: 0.5rem 1rem;
border: 1px solid #00ff00;
color: #00ff00;
text-decoration: none;
transition: all 0.2s ease;
}
.back-link:hover {
background: #00ff00;
color: #000;
}
.highlight {
color: #00ff00;
font-weight: bold;
}
.warning {
color: #ffaa00;
}
</style>
</head>
<body>
<div class="privacy-container">
<div class="privacy-header">
<h1>Privacy & Open Source FAQ</h1>
<p>Valley of the Commons - Transparency & Open Commons Best Practices</p>
</div>
<div class="privacy-section">
<h2>Model Choice & AI Infrastructure</h2>
<h3>Current Model: Mistral Devstral-2</h3>
<p>
We use <span class="highlight">Mistral Devstral-2</span> via
<a href="https://vercel.com/docs/ai/ai-gateway" target="_blank" rel="noopener">Vercel AI Gateway</a>.
This is a cost-effective model suitable for MVP testing.
</p>
<h3>Why This Model?</h3>
<ul>
<li><strong>Free tier:</strong> Available on Vercel AI Gateway free tier</li>
<li><strong>Fast response times:</strong> Optimized for real-time dialogue</li>
<li><strong>Open-source roadmap:</strong> We're committed to migrating to open-weights or self-hosted models</li>
</ul>
<h3>Long-term Vision</h3>
<p>
<span class="warning">Note:</span> Some models used in MVP are proprietary APIs.
Our long-term roadmap includes migration to <strong>open-weights or self-hosted models</strong>
to align with open commons principles.
</p>
</div>
<div class="privacy-section">
<h2>Data Tracking & Privacy</h2>
<h3>What We Track</h3>
<ul>
<li><strong>Server logs:</strong> Basic request metadata (method, timestamp, message count) for debugging and monitoring</li>
<li><strong>Shared ideas:</strong> If you choose to share an idea or conversation to GitHub, it becomes part of the public repository</li>
</ul>
<h3>What We Do NOT Save</h3>
<ul>
<li><strong>We do NOT save conversations:</strong> Your dialogue with the game master is processed temporarily to generate responses, but we do not store conversations in any database or persistent storage</li>
<li><strong>We do NOT track conversations:</strong> Conversations exist only in your browser session and are not saved by us</li>
</ul>
<h3>AI Provider Tracking</h3>
<p>
<span class="warning">Important:</span> While <strong>we do not save your conversations</strong>, the AI provider
(Mistral via Vercel AI Gateway) may track conversations according to their own privacy policy.
We have no control over their data collection practices. For details, see
<a href="https://mistral.ai/legal/privacy-policy/" target="_blank" rel="noopener">Mistral's Privacy Policy</a>
and <a href="https://vercel.com/legal/privacy-policy" target="_blank" rel="noopener">Vercel's Privacy Policy</a>.
</p>
<h3>What We Do NOT Track</h3>
<ul>
<li><strong>No user accounts:</strong> No registration, no login, no personal profiles</li>
<li><strong>No cookies:</strong> No tracking cookies, no analytics cookies, no advertising trackers</li>
<li><strong>No IP logging:</strong> IP addresses are not stored or logged</li>
<li><strong>No third-party analytics:</strong> No Google Analytics, no Facebook Pixel, no tracking scripts</li>
<li><strong>No email collection:</strong> The game interface does not collect email addresses</li>
</ul>
<h3>Conversation Handling</h3>
<p>
<strong>We do not save conversations.</strong> Conversations exist only in your browser session.
Messages are sent to the server for AI processing but are <strong>not stored persistently by us</strong>.
Each conversation is ephemeral and exists only during your active session. When you close your browser,
the conversation is gone from our systems.
</p>
<h3>When Conversations Are Saved</h3>
<p>
Conversations are <strong>only saved</strong> when you explicitly choose to share them to GitHub using
the "Share to GitHub" feature. This is an opt-in action that you control. Once shared, the conversation
becomes part of the public repository at <code>build_game/conversations/</code>.
</p>
<h3>Server-Side Processing</h3>
<p>
Messages are processed through Vercel serverless functions. Our server logs may contain:
</p>
<ul>
<li>Request metadata (timestamp, method)</li>
<li>Message count and length (for debugging)</li>
<li>First/last message previews (first 100 characters, for debugging only)</li>
</ul>
<p>
These logs are <strong>not publicly accessible</strong> and are used only for system monitoring and debugging.
They do not contain full conversation content and are automatically rotated by Vercel.
</p>
</div>
<div class="privacy-section">
<h2>What Is Verifiable on GitHub</h2>
<h3>Open Source Repository</h3>
<p>
Our entire codebase is open source and available at
<a href="https://github.com/understories/votc" target="_blank" rel="noopener">github.com/understories/votc</a>.
</p>
<h3>You Can Verify:</h3>
<ul>
<li><strong>All client-side code:</strong> HTML, CSS, JavaScript - everything runs in your browser</li>
<li><strong>Serverless function code:</strong> All API endpoints are open source and auditable</li>
<li><strong>No hidden tracking:</strong> Review the code yourself - no analytics, no trackers, no data collection</li>
<li><strong>Data handling logic:</strong> See exactly how messages are processed, sanitized, and sent to AI</li>
<li><strong>Security measures:</strong> Input sanitization, role whitelisting, turn limits - all visible in code</li>
<li><strong>Shared ideas:</strong> Ideas shared to GitHub are publicly visible in <code>build_game/ideas/</code></li>
</ul>
<h3>What's Not in the Repository</h3>
<ul>
<li><strong>API keys:</strong> Stored securely in Vercel environment variables (never in code)</li>
<li><strong>Internal thoughts:</strong> Game design notes are in the repo but don't contain user data</li>
<li><strong>Server logs:</strong> Not committed to the repository</li>
</ul>
</div>
<div class="privacy-section">
<h2>Open Source & Open Commons Best Practices</h2>
<h3>Our Commitment</h3>
<p>
Valley of the Commons follows <strong>open commons</strong> principles:
</p>
<ul>
<li><strong>Transparency:</strong> All code is open source and auditable</li>
<li><strong>No vendor lock-in:</strong> Using open standards and protocols</li>
<li><strong>Community ownership:</strong> Ideas shared become part of the public commons</li>
<li><strong>Minimal data collection:</strong> Only what's necessary for functionality</li>
<li><strong>User control:</strong> You choose what to share, when to share it</li>
</ul>
<h3>Open Source License</h3>
<p>
The codebase is open source. Check the repository for the specific license terms.
</p>
<h3>Contributing</h3>
<p>
Contributions, improvements, and audits are welcome. The repository is public and open for
community participation.
</p>
<h3>Future Improvements</h3>
<ul>
<li>Migration to self-hosted or open-weights models</li>
<li>Enhanced privacy controls</li>
<li>Optional conversation export (user-controlled)</li>
<li>Local-first architecture where possible</li>
</ul>
</div>
<div class="privacy-section">
<h2>Security Measures</h2>
<h3>Input Sanitization</h3>
<ul>
<li>Message content is limited to 500 characters per message</li>
<li>Only 'user' and 'assistant' roles are allowed (prevents system prompt injection)</li>
<li>Empty messages are filtered out</li>
</ul>
<h3>Rate Limiting</h3>
<ul>
<li>Maximum 12 user turns per conversation</li>
<li>Server-side turn counting (prevents client-side manipulation)</li>
</ul>
<h3>API Security</h3>
<ul>
<li>API keys stored in environment variables (never exposed to client)</li>
<li>Serverless functions handle all sensitive operations</li>
<li>No CORS for game chat (same-origin only)</li>
</ul>
</div>
<div class="privacy-section">
<h2>Your Rights & Control</h2>
<h3>You Control:</h3>
<ul>
<li><strong>What you share:</strong> Only share ideas you want to make public</li>
<li><strong>When you share:</strong> Sharing is opt-in, not automatic</li>
<li><strong>Your conversation:</strong> Conversations are ephemeral - close the browser to end the session</li>
</ul>
<h3>No Account Required</h3>
<p>
You can use the game interface without creating an account, providing an email, or any personal information.
</p>
<h3>Questions or Concerns?</h3>
<p>
If you have questions about privacy, data handling, or want to report a concern,
please open an issue on <a href="https://github.com/understories/votc" target="_blank" rel="noopener">GitHub</a>
or contact the project maintainers.
</p>
</div>
<a href="game.html" class="back-link">← Back to Game</a>
</div>
</body>
</html>

58
server.js Normal file
View File

@ -0,0 +1,58 @@
const express = require('express');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// CORS middleware
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
return res.status(200).end();
}
next();
});
// API routes - wrap Vercel serverless functions
const waitlistHandler = require('./api/waitlist');
const gameChatHandler = require('./api/game-chat');
const shareToGithubHandler = require('./api/share-to-github');
// Adapter to convert Vercel handler to Express
const vercelToExpress = (handler) => async (req, res) => {
try {
await handler(req, res);
} catch (error) {
console.error('API Error:', error);
if (!res.headersSent) {
res.status(500).json({ error: 'Internal server error' });
}
}
};
app.all('/api/waitlist', vercelToExpress(waitlistHandler));
app.all('/api/game-chat', vercelToExpress(gameChatHandler));
app.all('/api/share-to-github', vercelToExpress(shareToGithubHandler));
// Static files
app.use(express.static(path.join(__dirname), {
extensions: ['html'],
index: 'index.html'
}));
// SPA fallback - serve index.html for non-API routes
app.get('*', (req, res) => {
if (!req.path.startsWith('/api/')) {
res.sendFile(path.join(__dirname, 'index.html'));
}
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`Valley of the Commons server running on port ${PORT}`);
});

169
speakers.js Normal file
View File

@ -0,0 +1,169 @@
// Dynamically load featured speakers
document.addEventListener('DOMContentLoaded', function() {
const speakersContainer = document.getElementById('speakers-container');
if (!speakersContainer) return;
// Define speakers based on folder structure
const speakers = [
{
name: 'Adam Arvidsson',
folder: 'Adam Arvidsson',
image: 'Arvidsson.png'
},
{
name: 'Charlie Fisher',
folder: 'Charlie Fisher',
image: 'charlie.jpeg'
},
{
name: 'Daniel Figueiredo',
folder: 'DANIEL RICHARD DE OLIVIERA FIGUEIREDO',
image: 'daniel.webp'
},
{
name: 'Emil Fritsch',
folder: 'Emil Fritsch',
image: 'emil.webp',
imagePosition: 'top'
},
{
name: 'Felix Fritsch',
folder: 'Felix Fritsch',
image: 'Fritsch.png'
},
{
name: 'Clara Gromaches',
folder: 'Clara Gromaches',
image: 'clara.jpg'
},
{
name: 'Koss',
folder: 'Koss',
image: 'koss.png'
},
{
name: 'Lorenzo Patuzzo',
folder: 'Lorenzo Patuzzo',
image: 'lorenzo.jpg'
},
{
name: 'Michel Bauwens',
folder: 'Michel Bauwens',
image: 'bauwens.jpeg'
},
{
name: 'Rashmi Abbigeri',
folder: 'Rashmi Abbigeri',
image: 'Abbigeri.png'
},
{
name: 'Una Wang',
folder: 'Una Wang',
image: 'una.jpg'
},
{
name: 'Veronica',
folder: 'Veronica',
image: 'veronica.png'
}
];
// Sort by last name; single-name entries go last, sorted by first name
const withLastName = speakers.filter((speaker) => speaker.name.trim().split(/\s+/).length > 1);
const withoutLastName = speakers.filter((speaker) => speaker.name.trim().split(/\s+/).length === 1);
withLastName.sort((a, b) => {
const aParts = a.name.trim().split(/\s+/);
const bParts = b.name.trim().split(/\s+/);
const aLast = aParts[aParts.length - 1];
const bLast = bParts[bParts.length - 1];
const lastCompare = aLast.localeCompare(bLast, 'en', { sensitivity: 'base' });
if (lastCompare !== 0) return lastCompare;
return a.name.localeCompare(b.name, 'en', { sensitivity: 'base' });
});
withoutLastName.sort((a, b) => {
const aFirst = a.name.trim().split(/\s+/)[0];
const bFirst = b.name.trim().split(/\s+/)[0];
return aFirst.localeCompare(bFirst, 'en', { sensitivity: 'base' });
});
const sortedSpeakers = [...withLastName, ...withoutLastName];
// Load each speaker
sortedSpeakers.forEach(speaker => {
loadSpeaker(speaker);
});
async function loadSpeaker(speaker) {
// Create speaker card
const speakerCard = document.createElement('div');
speakerCard.className = 'speaker-card';
// Create image
const img = document.createElement('img');
img.src = `speakers/${speaker.folder}/${speaker.image}`;
img.alt = speaker.name;
img.className = 'speaker-image';
img.loading = 'lazy';
if (speaker.imagePosition) {
img.style.objectPosition = speaker.imagePosition;
}
// Create name
const name = document.createElement('h3');
name.className = 'speaker-name';
name.textContent = speaker.name;
// Create bio (filled after fetch to preserve order)
const bio = document.createElement('p');
bio.className = 'speaker-bio';
// Create toggle text for mobile
const toggleText = document.createElement('span');
toggleText.className = 'speaker-toggle-text';
toggleText.textContent = 'click to read more';
// Create read more indicator for desktop
const readMoreDesktop = document.createElement('span');
readMoreDesktop.className = 'speaker-read-more-desktop';
readMoreDesktop.textContent = 'read more v';
// Assemble card
speakerCard.appendChild(img);
speakerCard.appendChild(name);
speakerCard.appendChild(readMoreDesktop);
speakerCard.appendChild(toggleText);
speakerCard.appendChild(bio);
// Add click handler for mobile
speakerCard.addEventListener('click', function(e) {
if (window.innerWidth < 769) {
e.preventDefault();
speakerCard.classList.toggle('expanded');
// Update toggle text
if (speakerCard.classList.contains('expanded')) {
toggleText.textContent = 'click to collapse';
} else {
toggleText.textContent = 'click to read more';
}
}
});
// Append immediately to preserve sorted order
speakersContainer.appendChild(speakerCard);
try {
// Fetch bio
const bioResponse = await fetch(`speakers/${speaker.folder}/bio.md`);
if (!bioResponse.ok) {
console.error(`Failed to load bio for ${speaker.name}`);
return;
}
const bioText = await bioResponse.text();
bio.textContent = bioText.trim();
} catch (error) {
console.error(`Error loading speaker ${speaker.name}:`, error);
}
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 KiB

View File

@ -0,0 +1 @@
Adam Arvidsson is Professor of Sociology of Cultural and Communicative Processes at the Università Federico II di Napoli. His research explores emerging forms of cooperation enabled by digital media and their economic and political potential, with a particular focus on the evolving relationship between capitalism and the commons.

View File

@ -0,0 +1 @@
Charlie works on the practical demonstrations for new affordable housing and commons-based landholding approaches. As a researcher, founder, and project advisor, he ran an architecture practice for a decade, ran the Civic Tech studio at Dark Matter Labs, was a key advisor on large scale new housing developments in England, and was a co-founder of Oasa, a Swiss token-issuer for networked land projects.

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@ -0,0 +1 @@
Clara Gromaches is an architect working on housing and land as commons. Her work combines fieldwork and emerging digital technologies to create experimental financial and governance mechanisms that protect and accelerate community-led models of collective ownership and stewardship through Komma.

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@ -0,0 +1 @@
Daniel manages hospitality at the Commons Hub with a unique blend of practicality, intelligence, and charm. His exceptional problem-solving skills and calm approach to tackling tough situations ground our community on a daily basis, and his positive energy lights our way as we transform our valley.

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 KiB

View File

@ -0,0 +1,2 @@
Emil, Co-Founder of the Commons Hub, comes from a civil engineering background and has worked in computer vision, started a green tech company, and built validator nodes for the hub. He entered IT during the 2017 neural network chess engine revolution. In 2020, he saw Hirschwangerhofs potential and organized its first event—a 3-week hackathon and retreat—paving the way for the Commons Hub.

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

View File

@ -0,0 +1 @@
Felix Fritsch, Co-Founder of the Commons Hub and Crypto Commons Association, combines expertise in political and economic theory with a passion for community organizing. Completed his PhD on The Emergence of the Crypto Commons.

1
speakers/Koss/bio.md Normal file
View File

@ -0,0 +1 @@
Koss is the Lead Gardener at Invisible Garden and runs Ecosystem & Community Growth at Swarm. With a mixed background across the arts, grassroots activism, policy/gov relations, Koss is generally curious about our expansion as a multi-planetary civilization and a long time supporter of the Commons Hub.

BIN
speakers/Koss/koss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

View File

@ -0,0 +1 @@
Lorenzo Patuzzo is a Creative Technologist and a Social Entrepreneur. He is part of AKASHA Foundation and Founder of AKASHA Hub, Hubs Network and Green City Lab, promoting collaboration, decentralization and open source philosophy, and facilitating local collaborative projects for social good and for the environment. He has been working in different sectors varying from industrial design, cinema, architecture and distributed global digital infrastructures since 2013. He is now focusing on a network of collaborative R+D places for society improvement as a path towards a coherent and self-managed society.

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -0,0 +1 @@
Michel Bauwens, founder of p2pfoundation & FairCoop, is a Belgian political theorist, writer, and conference speaker on the subjects of technology, culture and business innovation.

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 KiB

View File

@ -0,0 +1 @@
Rashmi Abbigeri is a research engineer at MetaGov working on grant analytics through Open Grants. Her work emphasizes the development of AI-ready datasets, a critical factor for the sustainable scaling of beneficial AI systems across different sectors.

1
speakers/Una Wang/bio.md Normal file
View File

@ -0,0 +1 @@
Una Wang researches machine agency, decentralized governance, and cyber-physical systems in the built environment as a Civil Engineering PhD candidate at ETH Zurich. Her work bridges blockchain technology, autonomous systems, and regenerative systems, exploring how these technologies can transform ownership and governance in physical infrastructure. She led research projects no1s1, co-founded DaoSuisse and Zuitzerland, and advises startups on blockchain integration and decentralized infrastructure.

BIN
speakers/Una Wang/una.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

1
speakers/Veronica/bio.md Normal file
View File

@ -0,0 +1 @@
Veronica keeps organizing pop-up cities despite continuously declaring she is not an event organizer, and previously organized ZuVillage Georgia in 2024 and Zuitzerland in 2025. Her background and passions span education technology, artificial intelligence, decentralized technologies, individual and collective optimization, and truth seeking.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

1386
styles.css

File diff suppressed because it is too large Load Diff

150
test-waitlist.js Normal file
View File

@ -0,0 +1,150 @@
// Test script for waitlist API
// Run with: node test-waitlist.js
// Make sure .env file exists with your credentials
require('dotenv').config();
const { google } = require('googleapis');
async function testWaitlist() {
console.log('🧪 Testing Google Sheets Waitlist Integration...\n');
// Check environment variables
const credentials = process.env.GOOGLE_SERVICE_ACCOUNT;
const spreadsheetId = process.env.GOOGLE_SHEET_ID;
const sheetName = process.env.GOOGLE_SHEET_NAME || 'Waitlist';
console.log('📋 Configuration Check:');
console.log(' ✓ GOOGLE_SERVICE_ACCOUNT:', credentials ? `Set (${credentials.length} chars)` : '❌ MISSING');
console.log(' ✓ GOOGLE_SHEET_ID:', spreadsheetId || '❌ MISSING');
console.log(' ✓ GOOGLE_SHEET_NAME:', sheetName);
console.log('');
if (!credentials || !spreadsheetId) {
console.error('❌ Missing required environment variables!');
console.error(' Create a .env file with GOOGLE_SERVICE_ACCOUNT and GOOGLE_SHEET_ID');
process.exit(1);
}
try {
// Parse credentials
console.log('🔐 Parsing credentials...');
const credentialsTrimmed = credentials.trim();
let serviceAccount;
try {
serviceAccount = JSON.parse(credentialsTrimmed);
console.log(' ✓ Credentials parsed successfully');
console.log(' ✓ Service account email:', serviceAccount.client_email);
} catch (parseError) {
console.error(' ❌ Failed to parse credentials:', parseError.message);
process.exit(1);
}
// Authenticate
console.log('\n🔑 Authenticating with Google...');
const auth = new google.auth.GoogleAuth({
credentials: serviceAccount,
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});
console.log(' ✓ Authentication successful');
// Get sheets client
const sheets = google.sheets({ version: 'v4', auth });
// Test 1: Get spreadsheet metadata
console.log('\n📊 Test 1: Checking spreadsheet access...');
try {
const spreadsheet = await sheets.spreadsheets.get({
spreadsheetId,
});
console.log(' ✓ Spreadsheet found:', spreadsheet.data.properties.title);
console.log(' ✓ Spreadsheet ID:', spreadsheetId);
} catch (error) {
console.error(' ❌ Cannot access spreadsheet:', error.message);
if (error.code === 403) {
console.error(' → Permission denied. Make sure the service account email has Editor access to the sheet.');
console.error(' → Service account email:', serviceAccount.client_email);
} else if (error.code === 404) {
console.error(' → Spreadsheet not found. Check the GOOGLE_SHEET_ID.');
}
process.exit(1);
}
// Test 2: Check if sheet tab exists
console.log('\n📑 Test 2: Checking sheet tab...');
try {
const spreadsheet = await sheets.spreadsheets.get({
spreadsheetId,
});
const sheetTabs = spreadsheet.data.sheets.map(s => s.properties.title);
console.log(' ✓ Available sheet tabs:', sheetTabs.join(', '));
if (!sheetTabs.includes(sheetName)) {
console.error(` ❌ Sheet tab "${sheetName}" not found!`);
console.error(' → Available tabs:', sheetTabs.join(', '));
console.error(' → Update GOOGLE_SHEET_NAME or create a tab named "' + sheetName + '"');
process.exit(1);
}
console.log(` ✓ Sheet tab "${sheetName}" exists`);
} catch (error) {
console.error(' ❌ Error checking sheet tabs:', error.message);
process.exit(1);
}
// Test 3: Try to read from the sheet
console.log('\n📖 Test 3: Reading from sheet...');
try {
const response = await sheets.spreadsheets.values.get({
spreadsheetId,
range: `${sheetName}!A1:C10`,
});
console.log(' ✓ Can read from sheet');
if (response.data.values) {
console.log(' ✓ Current rows:', response.data.values.length);
}
} catch (error) {
console.error(' ❌ Cannot read from sheet:', error.message);
process.exit(1);
}
// Test 4: Try to append a test row
console.log('\n✍ Test 4: Testing write access...');
try {
const testTimestamp = new Date().toISOString();
const testEmail = 'test@example.com';
const testName = 'Test User';
const values = [[testTimestamp, testEmail, testName]];
await sheets.spreadsheets.values.append({
spreadsheetId,
range: `${sheetName}!A:C`,
valueInputOption: 'USER_ENTERED',
insertDataOption: 'INSERT_ROWS',
resource: {
values,
},
});
console.log(' ✓ Successfully wrote test row to sheet');
console.log(' ✓ Check your Google Sheet - you should see a test entry');
} catch (error) {
console.error(' ❌ Cannot write to sheet:', error.message);
if (error.response?.status === 403) {
console.error(' → Permission denied. Service account needs Editor access.');
}
process.exit(1);
}
console.log('\n✅ All tests passed! Your configuration is correct.');
console.log('\n💡 Next steps:');
console.log(' 1. Make sure these same values are set in Vercel environment variables');
console.log(' 2. Deploy to Vercel');
console.log(' 3. Test the form on your live site');
} catch (error) {
console.error('\n❌ Unexpected error:', error.message);
console.error(' Stack:', error.stack);
process.exit(1);
}
}
testWaitlist();

BIN
valley.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

13
vercel.json Normal file
View File

@ -0,0 +1,13 @@
{
"version": 2,
"buildCommand": null,
"outputDirectory": ".",
"framework": null,
"rewrites": [
{
"source": "/((?!api/).*)",
"destination": "/index.html"
}
]
}

159
waitlist.js Normal file
View File

@ -0,0 +1,159 @@
// Smooth scroll to section, ensuring title is visible
function scrollToSection(sectionId, e) {
const section = document.getElementById(sectionId);
if (!section) return;
e.preventDefault();
// Get header height for offset
const header = document.querySelector('.header');
const headerHeight = header ? header.offsetHeight : 0;
const padding = 40; // Extra padding to ensure title is fully visible
// Calculate position to show section with title visible
const sectionRect = section.getBoundingClientRect();
const sectionTop = sectionRect.top + window.pageYOffset;
const viewportHeight = window.innerHeight;
// Scroll to position that shows the section title with proper spacing
// Position section so it starts below header with padding
const scrollPosition = sectionTop - headerHeight - padding;
// Smooth scroll to position
window.scrollTo({
top: Math.max(0, scrollPosition),
behavior: 'smooth'
});
}
// Smooth scroll to waitlist section, centered on page
function scrollToWaitlist(e) {
scrollToSection('waitlist', e);
}
// Loading screen handler
function initLoadingScreen() {
const loadingScreen = document.getElementById('loading-screen');
if (!loadingScreen) return;
const skipButton = document.getElementById('skip-loading');
if (skipButton) {
skipButton.addEventListener('click', function() {
loadingScreen.classList.add('hidden');
setTimeout(function() {
loadingScreen.remove();
}, 1200);
});
}
// Wait for page to fully load
window.addEventListener('load', function() {
// Add delay to allow title animation to complete (6s zoom + 4s title = ~6s total)
setTimeout(function() {
loadingScreen.classList.add('hidden');
// Remove from DOM after transition completes
setTimeout(function() {
loadingScreen.remove();
}, 1200);
}, 6500); // Wait for animations to complete
});
}
// Waitlist form submission handler and scroll setup
document.addEventListener('DOMContentLoaded', function() {
// Initialize loading screen
initLoadingScreen();
// Set up scroll handlers for all anchor links
const waitlistLinks = document.querySelectorAll('a[href="#waitlist"]');
waitlistLinks.forEach(link => {
link.addEventListener('click', scrollToWaitlist);
});
const scheduleLinks = document.querySelectorAll('a[href="#schedule"]');
scheduleLinks.forEach(link => {
link.addEventListener('click', (e) => scrollToSection('schedule', e));
});
const form = document.getElementById('waitlist-form');
const emailInput = document.getElementById('waitlist-email');
const nameInput = document.getElementById('waitlist-name');
const involvementInput = document.getElementById('waitlist-involvement');
const messageDiv = document.getElementById('waitlist-message');
const submitButton = form.querySelector('button[type="submit"]');
if (!form) return;
form.addEventListener('submit', async function(e) {
e.preventDefault();
// Disable form during submission
submitButton.disabled = true;
submitButton.textContent = 'Submitting...';
messageDiv.textContent = '';
messageDiv.className = 'waitlist-message';
const email = emailInput.value.trim();
const name = nameInput.value.trim();
// Basic email validation
if (!email || !email.includes('@')) {
showMessage('Please enter a valid email address.', 'error');
submitButton.disabled = false;
submitButton.textContent = 'Submit';
return;
}
// Name is now required
if (!name || name.trim() === '') {
showMessage('Please enter your name.', 'error');
submitButton.disabled = false;
submitButton.textContent = 'Submit';
return;
}
const involvement = involvementInput.value.trim();
// Involvement is required
if (!involvement || involvement === '') {
showMessage('Please describe your desired involvement.', 'error');
submitButton.disabled = false;
submitButton.textContent = 'Submit';
return;
}
try {
const response = await fetch('/api/waitlist', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, name, involvement }),
});
const data = await response.json();
if (response.ok && data.success) {
showMessage('Thank you! Your information has been submitted.', 'success');
form.reset();
} else {
showMessage(data.error || 'Something went wrong. Please try again.', 'error');
}
} catch (error) {
console.error('Error submitting form:', error);
showMessage('Network error. Please check your connection and try again.', 'error');
} finally {
submitButton.disabled = false;
submitButton.textContent = 'Submit';
}
});
function showMessage(message, type) {
messageDiv.textContent = message;
messageDiv.className = `waitlist-message ${type}`;
// Scroll message into view if needed
messageDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
});