From df667e0db8272d1d768738db989bb8962b4158ce Mon Sep 17 00:00:00 2001 From: Max Mayfield Date: Fri, 27 Feb 2026 06:56:34 +0000 Subject: [PATCH] v2: Forge Console + Open WebUI artifacts + Docker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - web/: Local chat UI (Express + WS โ†’ Codex bridge) - openwebui/: Preset, pipelines, knowledge manifest - Dockerfile + docker-compose.yml - Updated README with 3 frontend options - CLI-agnostic: works with Codex, Claude Code, Kiro, Gemini --- .dockerignore | 6 + .gitignore | 15 + AGENTS.md | 71 ++ CLAUDE.md | 1 + Dockerfile | 28 + GEMINI.md | 1 + README.md | 100 +++ config/mcporter.json | 20 + docker-compose.yml | 20 + docs/agents/README.md | 31 + docs/agents/jira.md | 327 +++++++ docs/agents/mcporter.md | 44 + docs/agents/rp-ticket-ops.md | 38 + env.example | 27 + openwebui/SETUP.md | 61 ++ openwebui/knowledge.json | 35 + openwebui/pipelines/aha_pipeline.py | 87 ++ openwebui/pipelines/bmad_factory_pipeline.py | 117 +++ openwebui/pipelines/gainsight_pipeline.py | 76 ++ openwebui/pipelines/jira_pipeline.py | 77 ++ openwebui/preset.json | 17 + skills/bmad-suite/SKILL.md | 47 + skills/bmad-suite/manager.py | 93 ++ skills/epics-standards/SKILL.md | 74 ++ skills/epics-standards/agents/openai.yaml | 4 + .../references/aha-epic-workflow.md | 55 ++ .../references/epic-fields-checklist.md | 53 ++ .../scripts/aha_create_epic.py | 103 +++ skills/factory-standards/SKILL.md | 31 + skills/factory-standards/manager.py | 89 ++ skills/gainsight-px/SKILL.md | 54 ++ skills/gainsight-px/gainsight_px.py | 108 +++ start.sh | 54 ++ web/package-lock.json | 849 ++++++++++++++++++ web/package.json | 13 + web/public/index.html | 127 +++ web/public/style.css | 176 ++++ web/server.js | 77 ++ 38 files changed, 3206 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 Dockerfile create mode 100644 GEMINI.md create mode 100644 README.md create mode 100644 config/mcporter.json create mode 100644 docker-compose.yml create mode 100644 docs/agents/README.md create mode 100644 docs/agents/jira.md create mode 100644 docs/agents/mcporter.md create mode 100644 docs/agents/rp-ticket-ops.md create mode 100644 env.example create mode 100644 openwebui/SETUP.md create mode 100644 openwebui/knowledge.json create mode 100644 openwebui/pipelines/aha_pipeline.py create mode 100644 openwebui/pipelines/bmad_factory_pipeline.py create mode 100644 openwebui/pipelines/gainsight_pipeline.py create mode 100644 openwebui/pipelines/jira_pipeline.py create mode 100644 openwebui/preset.json create mode 100644 skills/bmad-suite/SKILL.md create mode 100644 skills/bmad-suite/manager.py create mode 100644 skills/epics-standards/SKILL.md create mode 100644 skills/epics-standards/agents/openai.yaml create mode 100644 skills/epics-standards/references/aha-epic-workflow.md create mode 100644 skills/epics-standards/references/epic-fields-checklist.md create mode 100644 skills/epics-standards/scripts/aha_create_epic.py create mode 100644 skills/factory-standards/SKILL.md create mode 100644 skills/factory-standards/manager.py create mode 100644 skills/gainsight-px/SKILL.md create mode 100644 skills/gainsight-px/gainsight_px.py create mode 100755 start.sh create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/public/index.html create mode 100644 web/public/style.css create mode 100644 web/server.js diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..745cd2e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +web/node_modules +npm-debug.log +.env +bmad/ +.standards/ +.git diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48d4cdf --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Dependencies +node_modules/ + +# Environment +.env + +# OS +.DS_Store +Thumbs.db + +# BMad repos (cloned at runtime) +bmad/ + +# Factory standards (cloned at runtime) +.standards/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..292c9ee --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,71 @@ +Always read README.md before answering any queries or writing code. + +# Agent Operating Instructions + +You are an AI assistant operating inside the **Reltio PM Factory**. You help Product Managers create epics, draft PR/FAQs, query analytics, and maintain compliance with the Transparent Factory standard. + +--- + +## 0. First Interaction / Startup Hook + +When first initialized, perform a silent check: + +1. Verify if `.standards/factory/` and `bmad/` directories exist. +2. If they do **not** exist, output: + +> ๐Ÿ‘‹ **Welcome to the Reltio PM Factory!** +> It looks like your environment hasn't been initialized yet. Type `new-factory` (or ask me to bootstrap the environment) to download the Transparent Factory Standards, the BMad Creative Suite, and perform MCP health checks. + +3. If they **do** exist, output: + +> ๐Ÿ‘‹ **Welcome back to the Reltio PM Factory!** +> The Transparent Factory Standards and BMad Suite are loaded. Do you want to draft a new PR/FAQ, create an Epic in Jira, or consult the Creative Squad? + +--- + +## 1. The Transparent Factory + +You must adhere to the Reltio Transparent Factory tenets. + +- Before proposing technical product requirements or architectural changes, execute `factory_update` from `skills/factory-standards/`. +- Read the PR/FAQ and tenets at `.standards/factory/content/`. +- **Code is for Humans:** If your proposed code, spec, or Epic is not readable by a human engineer in under 60 seconds, revise it. If it violates Elastic Schema rules, reject it. + +--- + +## 2. Available Skills + +All skills are in the `/skills/` directory. Each has a `SKILL.md` with capabilities, required env vars, and entry points. + +| Skill | Purpose | Entry Point | +|-------|---------|-------------| +| `epics-standards` | Epic creation & Aha! workflows | `scripts/aha_create_epic.py` | +| `factory-standards` | Transparent Factory sync & compliance | `manager.py update` | +| `bmad-suite` | Creative Intelligence Suite | `manager.py list\|update` | +| `gainsight-px` | Product analytics | `gainsight_px.py` | + +Before interacting with external APIs, read the relevant `SKILL.md` first. + +--- + +## 3. The `new-factory` Bootstrap Command + +When the user asks to initialize, bootstrap, or types `new-factory`: + +1. **Check Environment:** Verify `.env` exists. If not, copy from `env.example` and STOP until user fills it in. +2. **Sync BMad:** `python3 skills/bmad-suite/manager.py update` +3. **Sync Factory Standards:** `python3 skills/factory-standards/manager.py update` +4. **Health Check Jira:** `mcporter --config config/mcporter.json list atlassian` +5. **Health Check Aha!:** `set -a && source .env && set +a && mcporter --config config/mcporter.json list aha` + +--- + +## 4. MCP Servers + +Configuration: `config/mcporter.json` + +| Server | Purpose | +|--------|---------| +| `atlassian` | Jira issue management | +| `aha` | Aha! roadmap & epics | +| `context7` | Library documentation lookup | diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..50dd1b0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +Always read README.md before answering any queries or writing code. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2e760b7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM node:22-slim + +# Install system deps for mcporter and codex +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + python3 \ + python3-pip \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Install mcporter and codex globally +RUN npm install -g @anthropic/mcporter @openai/codex + +WORKDIR /app + +# Copy web deps first for layer caching +COPY web/package.json web/package-lock.json* web/ +RUN cd web && npm install --production + +# Copy everything else +COPY . . + +# Make start script executable +RUN chmod +x start.sh + +EXPOSE 3000 + +CMD ["node", "web/server.js"] diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..50dd1b0 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1 @@ +Always read README.md before answering any queries or writing code. diff --git a/README.md b/README.md new file mode 100644 index 0000000..30ee5fa --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# Reltio PM Factory + +A browser-based AI workspace for Product Managers. No terminal required after initial setup. + +**Forge Console** gives you a familiar chat interface backed by [Codex](https://github.com/openai/codex), pre-loaded with Reltio's Transparent Factory standards, BMad Creative Suite, Jira/Aha!/Gainsight integrations, and epic-writing skills. + +--- + +## Quick Start + +### Option 1: Docker (recommended) +```bash +git clone pm-template +cd pm-template +cp env.example .env # fill in your API keys +docker compose up -d +``` +Open http://localhost:3000. + +### Option 2: Local +```bash +git clone pm-template +cd pm-template +./start.sh +``` + +On first run, the script checks Node.js, creates `.env`, installs deps, and opens the browser. + +--- + +## Prerequisites (One-Time Setup) + +Before the AI agent can connect to your tools, you need: + +1. **Git & Bitbucket Access** โ€” A working SSH key configured with Bitbucket. +2. **Jira Authentication** โ€” Run `mcporter auth atlassian` in a terminal once to complete the OAuth flow. +3. **API Keys** โ€” Edit `.env` with your Aha! and Gainsight PX credentials. + +Once authenticated, the Forge Console handles everything else. + +--- + +## What's Inside + +| Component | Purpose | +|-----------|---------| +| `web/` | Forge Console โ€” local chat UI + Express server | +| `skills/epics-standards/` | Epic creation templates & Aha! workflows | +| `skills/factory-standards/` | Transparent Factory tenet sync & compliance | +| `skills/bmad-suite/` | BMad Creative Intelligence Suite (brainstorming, design thinking, storytelling) | +| `skills/gainsight-px/` | Product analytics queries | +| `config/mcporter.json` | MCP server definitions (Jira, Aha!, Context7) | + +--- + +## For AI Agents + +If you are an AI assistant reading this repo, see `AGENTS.md` for your operating instructions. + +--- + +## Frontend Options + +This repo is CLI-agnostic. Use whichever interface fits your team: + +### Option A: Forge Console (built-in) +A lightweight local chat UI that wraps Codex CLI. +```bash +./start.sh # opens http://localhost:3000 +``` + +### Option B: Open WebUI (recommended) +Use the pre-built artifacts in `openwebui/` to connect any Open WebUI instance. +Includes model presets, RAG knowledge manifests, and tool pipelines. +See [`openwebui/SETUP.md`](openwebui/SETUP.md) for details. + +### Option C: Any CLI Agent +Open the repo in Cursor, Claude Code, Kiro, Gemini CLI, or any agent that reads `AGENTS.md`. + +--- + +## Architecture + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Frontend (pick one) โ”‚ +โ”‚ โ€ข Forge Console (web/) โ”‚ +โ”‚ โ€ข Open WebUI (openwebui/) โ”‚ +โ”‚ โ€ข CLI Agent (Codex, Claude, Kiro...) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†• +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ LLM (any OpenAI-compatible provider) โ”‚ +โ”‚ + System Prompt (AGENTS.md) โ”‚ +โ”‚ + RAG Knowledge (skills/, standards/) โ”‚ +โ”‚ + Tool Pipelines (mcporter, aha, etc.) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ†• MCP + Jira ยท Aha! ยท Gainsight PX +``` diff --git a/config/mcporter.json b/config/mcporter.json new file mode 100644 index 0000000..95f4b2c --- /dev/null +++ b/config/mcporter.json @@ -0,0 +1,20 @@ +{ + "mcpServers": { + "context7": { + "command": "npx", + "args": ["-y", "@upstash/context7-mcp"] + }, + "atlassian": { + "command": "npx", + "args": ["-y", "mcp-remote", "https://mcp.atlassian.com/v1/mcp"] + }, + "aha": { + "command": "npx", + "args": ["-y", "aha-mcp@latest"], + "env": { + "AHA_DOMAIN": "${AHA_DOMAIN}", + "AHA_API_TOKEN": "${AHA_API_KEY}" + } + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..656e952 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +services: + forge: + build: . + container_name: forge-console + ports: + - "3000:3000" + env_file: + - .env + volumes: + # Mount skills and config so changes persist without rebuild + - ./skills:/app/skills + - ./config:/app/config + - ./bmad:/app/bmad + - ./.standards:/app/.standards + # Mount mcporter auth cache so OAuth tokens persist + - mcporter-auth:/root/.mcporter + restart: unless-stopped + +volumes: + mcporter-auth: diff --git a/docs/agents/README.md b/docs/agents/README.md new file mode 100644 index 0000000..f8dca71 --- /dev/null +++ b/docs/agents/README.md @@ -0,0 +1,31 @@ +# Forge Console โ€” Agent Docs + +## Jira Agent (`rp-ticket-ops`) + +Manages Jira issues via the Atlassian MCP server. + +### Capabilities +- Create, update, and transition Jira issues +- Search with JQL +- Add comments and attachments +- Link issues + +### Authentication +Uses `mcporter` with OAuth. Run `mcporter auth atlassian` once to authenticate. + +### Usage +The agent reads `config/mcporter.json` to connect. No API tokens needed โ€” OAuth handles it. + +## MCPorter + +MCP client that bridges AI agents to external tool servers. + +### Configured Servers +| Server | Purpose | +|--------|---------| +| `atlassian` | Jira via Atlassian's official MCP | +| `aha` | Aha! roadmap management | +| `context7` | Library documentation lookup | + +### Config Location +`config/mcporter.json` diff --git a/docs/agents/jira.md b/docs/agents/jira.md new file mode 100644 index 0000000..f275ac5 --- /dev/null +++ b/docs/agents/jira.md @@ -0,0 +1,327 @@ +--- +name: jira +description: Use the mcporter CLI to list, configure, auth, and call MCP servers/tools directly (HTTP or stdio), including ad-hoc servers, config edits, and CLI/type generation. +--- + +# Atlassian / JIRA Reference + +## Domain +`reltio.jira.com` โ€” use for all browse links, e.g. `https://reltio.jira.com/browse/RP-XXXXX` + +## Cloud ID +`444c13e0-0faa-4055-b053-501700bae7b0` + +## Current User (Brian Galura) +- account_id: `712020:f70452cf-df7e-4ee3-b65b-66c83566fc3b` +- email: brian.galura@reltio.com + +## Project +- key: `RP` (Reltio Platform), id: `10041` +- key: `ICR` (Infrastructure Change Request), id: `12890` + +## Issue Types (RP project) +| Name | ID | Hierarchy | Notes | +|----------------|-------|-----------|------------------------------| +| Epic | 5 | 1 | Parent of stories | +| Story | 6 | 0 | Standard work item | +| Rollout Story | 10535 | 0 | Feature enablement per env | +| Task | 3 | 0 | | +| Bug | 1 | 0 | | +| Sub-Task | 9 | -1 | Child of other issue types | + +## Required Fields by Issue Type + +### Epic (required) +| Field | Key | Type | +|------------------------|--------------------|-------------------| +| Assignee | `assignee` | user | +| Type of Effort | `customfield_15815`| option (select) | + +### Story (required) +| Field | Key | Type | +|------------------------|--------------------|-------------------| +| Assignee | `assignee` | user | +| Found/Requested By | `customfield_11922`| option (select) | +| Affected Documentation | `customfield_12429`| array of options | +| Acceptance Criteria | `customfield_15956`| **ADF** (rich text, must use Atlassian Document Format) | +| Type of Effort | `customfield_15815`| option (select) | + +### Task (required) +| Field | Key | Type | +|------------------------|--------------------|-------------------| +| Assignee | `assignee` | user | +| Components | `components` | array of component (`[{"id": "..."}]`) | +| Found/Requested By | `customfield_11922`| option (select) | +| Type of Effort | `customfield_15815`| option (select) | + +### Rollout Story (required) +| Field | Key | Type | +|------------------------|--------------------|-------------------| +| Assignee | `assignee` | user | +| Found/Requested By | `customfield_11922`| option (select) | +| Affected Documentation | `customfield_12429`| array of options | +| Execution team | `customfield_13020`| option (select) | +| Type of Effort | `customfield_15815`| option (select) | + +### Change Request (ICR project, required) +| Field | Key | Type | +|------------------------|--------------------|-------------------| +| Assignee | `assignee` | user (has default; still set explicitly when possible) | +| Change Start Date/Time | `customfield_15856`| datetime (`YYYY-MM-DDTHH:mm:ss.SSS-0800`) | +| Change End Date/Time | `customfield_15857`| datetime (`YYYY-MM-DDTHH:mm:ss.SSS-0800`) | +| Manager/Peer Reviewer | `customfield_15862`| user | +| Procedure | `customfield_15863`| option (select) | +| Change Category | `customfield_15864`| option (select) | +| Can it be rolled back? | `customfield_16072`| array of options (checkbox) | + +## Common Custom Fields (optional but useful) +| Field | Key | Type | +|--------------------|--------------------|-----------------| +| Fix Version | `fixVersions` | array of version| +| Reporter | `reporter` | user (`{"accountId": "..."}`) | +| Story Points | `customfield_10013`| number | +| Confidence Level | `customfield_12520`| option (select) | +| Product Lead | `customfield_15755`| user | +| Engineering Lead | `customfield_15756`| user | +| Start date | `customfield_15541`| date (YYYY-MM-DD) | +| End date | `customfield_15535`| date (YYYY-MM-DD) | +| Sprint | `customfield_10320`| sprint | +| Aha! Reference | `customfield_11820`| string (URL) | +| Security Review | `customfield_15826`| option (select) | + +## Allowed Values for Required Select Fields + +### Type of Effort (`customfield_15815`) +| Value | ID | +|-------------------------------------------|-------| +| Customer Feature | 18153 | +| Customer Support | 18156 | +| Innovation | 19074 | +| Platform Excellence | 18922 | +| Security Issues / Tech Debt / Maintenance | 18155 | + +### Found/Requested By (`customfield_11922`) +| Value | ID | +|----------------------|-------| +| Alert | 18931 | +| Customer Engineering | 13609 | +| Engineering | 10114 | +| FDE | 24812 | +| Other | 17542 | +| Product Management | 10115 | + +### Affected Documentation (`customfield_12429`) +| Value | ID | +|-------------------|-------| +| No | 16302 | +| Deprecation Notice| 11710 | +| Help Portal | 10438 | +| Internal Only | 12912 | +| Release Notes | 10437 | + +### Execution team (`customfield_13020`) โ€” partial list +| Value | ID | +|--------------------|-------| +| Cloud Platform (Portugal) | 19040 | +| DevOps | 16211 | +| Documentation | 18367 | +| Persistence | 16700 | +| Performance | 17629 | +| Data Unification | 12811 | +| Match | 15400 | +| IDP | 17906 | +| Console | 17845 | +| Architecture | 18930 | +| CI | 17656 | + +### Procedure (`customfield_15863`) โ€” ICR Change Request +| Value | ID | +|-------------|-------| +| Manual Step | 18226 | +| Automated | 18227 | + +### Change Category (`customfield_15864`) โ€” ICR Change Request +Default for ICR templates in this skill: `High Risk` (`18229`). + +| Value | ID | +|-----------|-------| +| Low Risk | 18228 | +| High Risk | 18229 | +| Emergency | 18230 | + +### Can it be rolled back? (`customfield_16072`) โ€” ICR Change Request +| Value | ID | +|-------|-------| +| Yes | 18678 | +| No | 18679 | + +### Risk Level (`customfield_16070`) โ€” required when closing +| Value | ID | +|--------------------|-------| +| High-Risk Changes | 18676 | +| Low-Risk Changes | 18677 | +| Not applicable | 19049 | + +### Confidence Level (`customfield_12520`) +| Value | ID | +|--------|-------| +| High | 10510 | + +### Components (partial list) +| Value | ID | +|---------------|-------| +| Documentation | 10222 | +| QA | 11511 | +| DevOps | 11111 | +| DNS | 20020 | + +## Fix Versions (known) +| Version | ID | Release Date | +|------------|-------|-------------| +| 2026.1.0.0 | 28439 | 2026-03-23 | + +## ADF (Atlassian Document Format) Templates + +### Acceptance Criteria โ€” ordered list +```json +{ + "type": "doc", + "version": 1, + "content": [ + {"type": "orderedList", "attrs": {"order": 1}, "content": [ + {"type": "listItem", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Criteria item 1"}]}]}, + {"type": "listItem", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Criteria item 2"}]}]} + ]} + ] +} +``` + +### Generic paragraph ADF (use for rich-text custom fields) +```json +{ + "type": "doc", + "version": 1, + "content": [ + { + "type": "paragraph", + "content": [ + {"type": "text", "text": "No expected customer impact."} + ] + } + ] +} +``` + +## Common Transitions +| Name | ID | Target Status | +|--------------------------|-----|----------------| +| Selected for Development | 501 | Prioritized | +| Start Progress | 11 | In Progress | +| Resolve | 31 | Resolved | +| In Design | 471 | In Design | +| Close | 351 | Closed | + +**Close transition requires:** `fixVersions` and `customfield_16070` (Risk Level). + +### Example: Close a ticket +```bash +mcporter call atlassian.transitionJiraIssue --args '{ + "cloudId": "444c13e0-0faa-4055-b053-501700bae7b0", + "issueIdOrKey": "RP-XXXXX", + "transition": {"id": "351"}, + "fields": { + "fixVersions": [{"id": "28439"}], + "customfield_16070": {"id": "19049"} + } +}' +``` + +## Example: Create an Epic +```bash +mcporter call atlassian.createJiraIssue --args '{ + "cloudId": "444c13e0-0faa-4055-b053-501700bae7b0", + "projectKey": "RP", + "issueTypeName": "Epic", + "summary": "Epic Title", + "description": "Markdown description", + "assignee_account_id": "712020:f70452cf-df7e-4ee3-b65b-66c83566fc3b", + "additional_fields": { + "reporter": {"accountId": "712020:f70452cf-df7e-4ee3-b65b-66c83566fc3b"}, + "fixVersions": [{"id": "28439"}], + "customfield_15815": {"id": "18153"} + } +}' +``` + +## Example: Create a Task +```bash +mcporter call atlassian.createJiraIssue --args '{ + "cloudId": "444c13e0-0faa-4055-b053-501700bae7b0", + "projectKey": "RP", + "issueTypeName": "Task", + "summary": "Task Title", + "description": "Task description", + "assignee_account_id": "712020:f70452cf-df7e-4ee3-b65b-66c83566fc3b", + "additional_fields": { + "components": [{"id": "10222"}], + "customfield_15815": {"id": "18922"}, + "customfield_11922": {"id": "10115"} + } +}' +``` + +## Example: Create a Story under an Epic +```bash +mcporter call atlassian.createJiraIssue --args '{ + "cloudId": "444c13e0-0faa-4055-b053-501700bae7b0", + "projectKey": "RP", + "issueTypeName": "Story", + "summary": "Story Title", + "description": "Markdown description with ## Why / ## What / ## How", + "assignee_account_id": "712020:f70452cf-df7e-4ee3-b65b-66c83566fc3b", + "parent": "RP-XXXXX", + "additional_fields": { + "reporter": {"accountId": "712020:f70452cf-df7e-4ee3-b65b-66c83566fc3b"}, + "fixVersions": [{"id": "28439"}], + "customfield_15815": {"id": "18153"}, + "customfield_11922": {"id": "10114"}, + "customfield_12429": [{"id": "16302"}], + "customfield_15956": {"type":"doc","version":1,"content":[{"type":"orderedList","attrs":{"order":1},"content":[{"type":"listItem","content":[{"type":"paragraph","content":[{"type":"text","text":"AC item"}]}]}]}]} + } +}' +``` + +## Example: Create an ICR Change Request +```bash +mcporter call atlassian.createJiraIssue --args '{ + "cloudId": "444c13e0-0faa-4055-b053-501700bae7b0", + "projectKey": "ICR", + "issueTypeName": "Change Request", + "summary": "Re-add na11 IP 34.36.175.121 to na07 latency-based Cloud DNS records (na07-compute)", + "description": "Implementation, verification, impact, and rollback details", + "assignee_account_id": "712020:f70452cf-df7e-4ee3-b65b-66c83566fc3b", + "additional_fields": { + "components": [{"id": "20020"}], + "customfield_15856": "2026-02-26T17:00:00.000-0800", + "customfield_15857": "2026-02-26T18:00:00.000-0800", + "customfield_15862": {"accountId": "712020:f70452cf-df7e-4ee3-b65b-66c83566fc3b"}, + "customfield_15863": {"id": "18226"}, + "customfield_15864": {"id": "18229"}, + "customfield_16072": [{"id": "18678"}], + "customfield_15858": { + "type": "doc", + "version": 1, + "content": [ + {"type": "paragraph", "content": [{"type": "text", "text": "No expected customer impact."}]} + ] + }, + "customfield_15859": { + "type": "doc", + "version": 1, + "content": [ + {"type": "paragraph", "content": [{"type": "text", "text": "Remove na11 IP 34.36.175.121 from affected latency-based record sets."}]} + ] + } + } +}' +``` diff --git a/docs/agents/mcporter.md b/docs/agents/mcporter.md new file mode 100644 index 0000000..8ea3d40 --- /dev/null +++ b/docs/agents/mcporter.md @@ -0,0 +1,44 @@ +--- +name: mcporter +description: Use the mcporter CLI to list, configure, auth, and call MCP servers/tools directly (HTTP or stdio), including ad-hoc servers, config edits, and CLI/type generation. +homepage: http://mcporter.dev +--- + +# mcporter + +Use `mcporter` to work with MCP servers directly. + +โš ๏ธ **CRITICAL EXECUTION RULE:** +Whenever you run `mcporter`, you MUST source the local `.env` file first so `mcporter.json` can interpolate the required API keys. Always run it as a chained command like this: + +`set -a && source .env && set +a && mcporter --config config/mcporter.json call key=value` + +Quick start +- `mcporter list` +- `mcporter list --schema` +- `mcporter call key=value` + +Call tools +- Selector: `mcporter call linear.list_issues team=ENG limit:5` +- Function syntax: `mcporter call "linear.create_issue(title: \"Bug\")"` +- Full URL: `mcporter call https://api.example.com/mcp.fetch url:https://example.com` +- Stdio: `mcporter call --stdio "bun run ./server.ts" scrape url=https://example.com` +- JSON payload: `mcporter call --args '{"limit":5}'` + +Auth + config +- OAuth: `mcporter auth [--reset]` +- Config: `mcporter config list|get|add|remove|import|login|logout` + +Daemon +- `mcporter daemon start|status|stop|restart` + +Codegen +- CLI: `mcporter generate-cli --server ` or `--command ` +- Inspect: `mcporter inspect-cli [--json]` +- TS: `mcporter emit-ts --mode client|types` + +Notes +- config file is located at `config/mcporter.json` you may have to resolve a different relative path depending on your context +- Prefer `--output json` for machine-readable results. +- Always use `--args '{...}'` (JSON payload) for Atlassian calls with complex/nested fields. + diff --git a/docs/agents/rp-ticket-ops.md b/docs/agents/rp-ticket-ops.md new file mode 100644 index 0000000..b0bf05e --- /dev/null +++ b/docs/agents/rp-ticket-ops.md @@ -0,0 +1,38 @@ +--- +name: rp-ticket-ops +description: Create and update RP Jira tickets in reltio.jira.com with the standard IaC setup. Use when you need to batch-create or normalize RP tasks to match the reference ticket (labels, sprint, status, assignee, and verification). +--- + +# RP Ticket Ops + +## Jira Context +- Domain: `https://reltio.jira.com` +- Cloud ID: `444c13e0-0faa-4055-b053-501700bae7b0` +- Project: `RP` +- Reference ticket: `RP-175518` + +## Standard RP IaC Values +- Sprint field key: `customfield_10320` +- Sprint value shape: numeric sprint id in `fields` (example: `4936`) +- Sprint in this workflow: `4936` (`IAC Sprint 8`) +- Labels: + - `Project_Cloud_Platform_IaC` + +## Update Pattern +1. Find all target issues by key or summary. +2. Ensure labels are set exactly to the 3 standard labels. +3. Set sprint with: + - `fields: { "customfield_10320": 4936 }` +4. Verify with JQL. + +## Verification JQL +- Sprint check: + - `key in (...) AND sprint = 4936` +- Assignee check: + - `key in (...) AND assignee = "Yevhen Fesyk"` +- Status + labels check: + - `key in (...) AND status = "Prioritized" AND labels in ("Project_Cloud_Platform_IaC","Project_Cloud_Platform_IaC_v0.4","qa-foxtrot")` + +## Notes +- For sprint assignment, prefer the numeric `fields.customfield_10320` payload. +- If sprint is not visible on board after update, re-check via JQL first; board views can lag. diff --git a/env.example b/env.example new file mode 100644 index 0000000..42ff766 --- /dev/null +++ b/env.example @@ -0,0 +1,27 @@ +# ========================================== +# PM Template - Environment Variables +# ========================================== +# Copy this file to .env and fill in the values below. +# Do NOT commit your actual .env file to version control. + +# ------------------------------------------ +# Aha! Integration +# ------------------------------------------ +# Generate this at: [Aha! Settings URL or instructions] +AHA_API_KEY="your_aha_api_key_here" +AHA_DOMAIN="your_company.aha.io" + +# ------------------------------------------ +# Gainsight PX Integration +# ------------------------------------------ +# Generate this in Gainsight PX: Administration -> REST API +GAINSIGHT_PX_API_KEY="your_gainsight_px_api_key_here" +GAINSIGHT_PX_REGION="US" # Set to 'EU' if hosted in Europe + +# ------------------------------------------ +# Jira / Atlassian Integration +# ------------------------------------------ +# We use the 'mcporter' CLI with the Atlassian MCP server for Jira. +# You do NOT need a static API token here. +# Instead, run the following command in your terminal to authenticate: +# mcporter auth atlassian diff --git a/openwebui/SETUP.md b/openwebui/SETUP.md new file mode 100644 index 0000000..01f3e85 --- /dev/null +++ b/openwebui/SETUP.md @@ -0,0 +1,61 @@ +# Open WebUI Integration + +Drop-in configuration to use the PM Factory repo with [Open WebUI](https://github.com/open-webui/open-webui). + +## Quick Setup + +### 1. Connect Your Model Provider + +In Open WebUI โ†’ Settings โ†’ Connections, add: + +| Field | Value | +|-------|-------| +| URL | `http://:8000/v1` | +| API Key | Your gateway API key | +| Model | `claude-opus-4.6` | + +Any OpenAI-compatible provider works (Kiro Gateway, LiteLLM, Ollama, etc). + +### 2. Import the Preset + +Go to Workspace โ†’ Models โ†’ Import, and upload `preset.json`. + +This creates a "Reltio PM Factory" model preset with the full system prompt from AGENTS.md baked in. + +### 3. Upload Knowledge (Optional) + +Go to Workspace โ†’ Knowledge โ†’ Create Collection called "PM Factory". + +Upload these directories as documents: +- `skills/epics-standards/references/` +- `skills/factory-standards/` (after running `manager.py update`) +- `skills/bmad-suite/` (after running `manager.py update`) + +Then attach the collection to your preset in Model settings โ†’ Knowledge. + +### 4. Install Pipelines (Optional) + +Pipelines let the model execute tools (Jira, Aha!, Gainsight) directly. + +Copy the files from `pipelines/` into your Open WebUI pipelines directory, or upload them via the Pipelines UI. + +Required env vars (set in Open WebUI โ†’ Settings โ†’ Pipelines): +- `AHA_API_KEY` +- `AHA_DOMAIN` +- `GAINSIGHT_PX_API_KEY` +- `MCPORTER_CONFIG` โ€” path to `config/mcporter.json` + +--- + +## Architecture + +``` +Open WebUI (browser) + โ†• OpenAI-compatible API +Any LLM Provider (Kiro / Ollama / LiteLLM / OpenAI) + + System Prompt (preset.json โ† AGENTS.md) + + RAG Knowledge (skills docs) + + Pipelines (mcporter, aha, gainsight, bmad) +``` + +The repo remains CLI-agnostic. This is just one frontend option. diff --git a/openwebui/knowledge.json b/openwebui/knowledge.json new file mode 100644 index 0000000..28c5f01 --- /dev/null +++ b/openwebui/knowledge.json @@ -0,0 +1,35 @@ +{ + "name": "PM Factory Knowledge Base", + "description": "Skills documentation and reference materials for the Reltio PM Factory.", + "collections": [ + { + "name": "Transparent Factory Standards", + "description": "The five tenets of the Transparent Factory engineering standard.", + "source": ".standards/factory/content/", + "note": "Run 'python3 skills/factory-standards/manager.py update' first to populate this directory." + }, + { + "name": "Epic Standards", + "description": "Templates and checklists for creating well-structured epics.", + "files": [ + "skills/epics-standards/references/aha-epic-workflow.md", + "skills/epics-standards/references/epic-fields-checklist.md", + "skills/epics-standards/SKILL.md" + ] + }, + { + "name": "BMad Creative Suite", + "description": "Agent definitions and workflows for brainstorming, design thinking, and storytelling.", + "source": "bmad/creative-intelligence-suite/docs/", + "note": "Run 'python3 skills/bmad-suite/manager.py update' first to populate this directory." + }, + { + "name": "Gainsight PX", + "description": "Product analytics skill documentation.", + "files": [ + "skills/gainsight-px/SKILL.md" + ] + } + ], + "upload_instructions": "In Open WebUI: Workspace โ†’ Knowledge โ†’ Create Collection. Upload the files listed for each collection. Then attach the collections to the 'Reltio PM Factory' model preset." +} diff --git a/openwebui/pipelines/aha_pipeline.py b/openwebui/pipelines/aha_pipeline.py new file mode 100644 index 0000000..4a0ae07 --- /dev/null +++ b/openwebui/pipelines/aha_pipeline.py @@ -0,0 +1,87 @@ +""" +title: Aha! Pipeline +author: PM Factory +version: 0.1.0 +description: Create epics and features in Aha! via the MCP server. +requirements: subprocess +""" + +import subprocess +import json +import os +from typing import Optional + +MCPORTER_CONFIG = os.environ.get("MCPORTER_CONFIG", "config/mcporter.json") + + +class Tools: + def __init__(self): + self.valves = self.Valves() + + class Valves: + MCPORTER_CONFIG: str = MCPORTER_CONFIG + AHA_DOMAIN: str = os.environ.get("AHA_DOMAIN", "") + AHA_API_KEY: str = os.environ.get("AHA_API_KEY", "") + + def aha_create_epic(self, product: str, name: str, description: str, workflow_status: str = "New", __user__: dict = {}) -> str: + """ + Create an epic in Aha! + + :param product: Aha! product key (e.g. 'PLAT') + :param name: Epic name + :param description: Epic description (supports HTML) + :param workflow_status: Initial status. Default: New + :return: Created epic reference and URL + """ + env = os.environ.copy() + env["AHA_DOMAIN"] = self.valves.AHA_DOMAIN + env["AHA_API_TOKEN"] = self.valves.AHA_API_KEY + + params = { + "product": product, + "name": name, + "description": description, + "workflow_status": workflow_status + } + try: + result = subprocess.run( + [ + "mcporter", + "--config", self.valves.MCPORTER_CONFIG, + "call", "aha", "create_epic", + "--params", json.dumps(params) + ], + capture_output=True, text=True, timeout=30, env=env + ) + if result.returncode != 0: + return f"Error: {result.stderr.strip()}" + return result.stdout.strip() + except Exception as e: + return f"Error: {str(e)}" + + def aha_list_features(self, product: str, __user__: dict = {}) -> str: + """ + List features for an Aha! product. + + :param product: Aha! product key + :return: JSON list of features + """ + env = os.environ.copy() + env["AHA_DOMAIN"] = self.valves.AHA_DOMAIN + env["AHA_API_TOKEN"] = self.valves.AHA_API_KEY + + try: + result = subprocess.run( + [ + "mcporter", + "--config", self.valves.MCPORTER_CONFIG, + "call", "aha", "list_features", + "--params", json.dumps({"product": product}) + ], + capture_output=True, text=True, timeout=30, env=env + ) + if result.returncode != 0: + return f"Error: {result.stderr.strip()}" + return result.stdout.strip() + except Exception as e: + return f"Error: {str(e)}" diff --git a/openwebui/pipelines/bmad_factory_pipeline.py b/openwebui/pipelines/bmad_factory_pipeline.py new file mode 100644 index 0000000..d4346c9 --- /dev/null +++ b/openwebui/pipelines/bmad_factory_pipeline.py @@ -0,0 +1,117 @@ +""" +title: BMad & Factory Pipeline +author: PM Factory +version: 0.1.0 +description: Brainstorm with BMad Creative Suite and validate against Transparent Factory tenets. +requirements: subprocess +""" + +import subprocess +import json +import os +from typing import Optional + +BMAD_PATH = os.environ.get("BMAD_PATH", "bmad") +FACTORY_PATH = os.environ.get("FACTORY_PATH", ".standards/factory/content") + + +class Tools: + def __init__(self): + self.valves = self.Valves() + + class Valves: + BMAD_PATH: str = BMAD_PATH + FACTORY_PATH: str = FACTORY_PATH + + def bmad_list_agents(self, __user__: dict = {}) -> str: + """ + List available BMad Creative Intelligence Suite agents and their capabilities. + + :return: List of agents with descriptions + """ + agents = { + "Carson (Brainstorming Coach)": { + "command": "/cis-brainstorm", + "capabilities": "36 ideation techniques, group dynamics, 'Yes, and...' methodology" + }, + "Maya (Design Thinking Coach)": { + "command": "/cis-design-thinking", + "capabilities": "Five-phase design thinking, empathy mapping, rapid prototyping" + }, + "Victor (Innovation Strategist)": { + "command": "/cis-innovation-strategy", + "capabilities": "Jobs-to-be-Done, Blue Ocean Strategy, Business Model Canvas" + }, + "Dr. Quinn (Creative Problem Solver)": { + "command": "/cis-problem-solve", + "capabilities": "Root cause analysis, systematic diagnosis, solution frameworks" + }, + "Storyteller": { + "command": "/cis-story", + "capabilities": "PR/FAQ drafting, narrative structure, stakeholder communication" + } + } + return json.dumps(agents, indent=2) + + def bmad_brainstorm(self, topic: str, technique: str = "auto", __user__: dict = {}) -> str: + """ + Run a brainstorming session using BMad's Carson agent. + + :param topic: The topic or problem to brainstorm about + :param technique: Brainstorming technique (auto, scamper, reverse, starbursting, six-hats, etc). Default: auto + :return: Brainstorming session output + """ + prompt = f"""You are Carson, the Brainstorming Coach from the BMad Creative Intelligence Suite. + +Run a brainstorming session on this topic: {topic} + +Technique: {technique if technique != 'auto' else 'Choose the best technique for this topic.'} + +Generate: +1. 8-12 diverse ideas using the selected technique +2. For each idea: one sentence description + feasibility rating (1-5) +3. Top 3 recommendations with brief rationale +4. One wild/moonshot idea that breaks assumptions + +Use "Yes, and..." methodology. Celebrate bold ideas.""" + + return prompt + + def factory_check(self, spec_text: str, __user__: dict = {}) -> str: + """ + Validate a specification or epic against the Transparent Factory tenets. + + :param spec_text: The spec, epic, or requirement text to validate + :return: Compliance report with pass/fail per tenet and recommendations + """ + tenets = { + "Atomic Flagging": { + "rule": "All new features must be behind feature flags with 14-day TTL. Must use OpenFeature SDK.", + "check": "Does the spec mention feature flags? Is there a TTL or rollout plan?" + }, + "Elastic Schema": { + "rule": "Schema changes must be additive-only. Breaking changes require sync dual-write with 30-day migration SLA.", + "check": "Does the spec propose schema changes? Are they additive? Is there a migration plan?" + }, + "Cognitive Durability": { + "rule": "All architectural decisions must have ADR logs. Code must be readable in 60 seconds.", + "check": "Does the spec reference ADRs? Is the proposed design simple enough to explain quickly?" + }, + "Semantic Observability": { + "rule": "AI reasoning must emit structured telemetry spans. Decisions must be traceable.", + "check": "Does the spec include observability requirements? Are reasoning spans defined?" + }, + "Configurable Autonomy": { + "rule": "AI agent actions must have governance guardrails. Human-in-the-loop for destructive operations.", + "check": "Does the spec define autonomy boundaries? Are there approval gates?" + } + } + + report = "# Transparent Factory Compliance Check\n\n" + report += f"**Input:** {spec_text[:200]}{'...' if len(spec_text) > 200 else ''}\n\n" + report += "| Tenet | Rule | Question |\n|-------|------|----------|\n" + for name, t in tenets.items(): + report += f"| {name} | {t['rule']} | {t['check']} |\n" + report += "\n*Review each tenet against the spec and flag violations.*" + + return report diff --git a/openwebui/pipelines/gainsight_pipeline.py b/openwebui/pipelines/gainsight_pipeline.py new file mode 100644 index 0000000..8cc7d67 --- /dev/null +++ b/openwebui/pipelines/gainsight_pipeline.py @@ -0,0 +1,76 @@ +""" +title: Gainsight PX Pipeline +author: PM Factory +version: 0.1.0 +description: Query Gainsight PX product analytics. +requirements: requests +""" + +import os +import json +import requests +from typing import Optional + + +class Tools: + def __init__(self): + self.valves = self.Valves() + + class Valves: + GAINSIGHT_PX_API_KEY: str = os.environ.get("GAINSIGHT_PX_API_KEY", "") + GAINSIGHT_PX_REGION: str = os.environ.get("GAINSIGHT_PX_REGION", "US") + + def _base_url(self) -> str: + if self.valves.GAINSIGHT_PX_REGION.upper() == "EU": + return "https://api-eu.aptrinsic.com/v1" + return "https://api.aptrinsic.com/v1" + + def _headers(self) -> dict: + return { + "X-APTRINSIC-API-KEY": self.valves.GAINSIGHT_PX_API_KEY, + "Content-Type": "application/json" + } + + def gainsight_query_users(self, filter_expression: str = "", page_size: int = 20, __user__: dict = {}) -> str: + """ + Query Gainsight PX users/accounts. + + :param filter_expression: Optional filter (e.g. 'propertyName=value') + :param page_size: Number of results to return. Default: 20 + :return: JSON string of user data + """ + try: + params = {"pageSize": page_size} + if filter_expression: + params["filter"] = filter_expression + + resp = requests.get( + f"{self._base_url()}/users", + headers=self._headers(), + params=params, + timeout=15 + ) + resp.raise_for_status() + return json.dumps(resp.json(), indent=2) + except Exception as e: + return f"Error: {str(e)}" + + def gainsight_feature_usage(self, feature_id: str, days: int = 30, __user__: dict = {}) -> str: + """ + Get feature usage stats from Gainsight PX. + + :param feature_id: The feature ID to query + :param days: Lookback period in days. Default: 30 + :return: JSON string of usage data + """ + try: + resp = requests.get( + f"{self._base_url()}/feature/{feature_id}/usage", + headers=self._headers(), + params={"days": days}, + timeout=15 + ) + resp.raise_for_status() + return json.dumps(resp.json(), indent=2) + except Exception as e: + return f"Error: {str(e)}" diff --git a/openwebui/pipelines/jira_pipeline.py b/openwebui/pipelines/jira_pipeline.py new file mode 100644 index 0000000..52ad31d --- /dev/null +++ b/openwebui/pipelines/jira_pipeline.py @@ -0,0 +1,77 @@ +""" +title: Jira Pipeline +author: PM Factory +version: 0.1.0 +description: Search and create Jira issues via mcporter + Atlassian MCP. +requirements: subprocess +""" + +import subprocess +import json +import os +from typing import Optional + +MCPORTER_CONFIG = os.environ.get("MCPORTER_CONFIG", "config/mcporter.json") + + +class Tools: + def __init__(self): + self.valves = self.Valves() + + class Valves: + MCPORTER_CONFIG: str = MCPORTER_CONFIG + + def jira_search(self, jql: str, __user__: dict = {}) -> str: + """ + Search Jira issues using JQL. + + :param jql: JQL query string (e.g. 'project = PLAT AND status = "In Progress"') + :return: JSON string of matching issues + """ + try: + result = subprocess.run( + [ + "mcporter", + "--config", self.valves.MCPORTER_CONFIG, + "call", "atlassian", "jira_search", + "--params", json.dumps({"jql": jql}) + ], + capture_output=True, text=True, timeout=30 + ) + if result.returncode != 0: + return f"Error: {result.stderr.strip()}" + return result.stdout.strip() + except Exception as e: + return f"Error: {str(e)}" + + def jira_create(self, project: str, summary: str, description: str, issue_type: str = "Story", __user__: dict = {}) -> str: + """ + Create a Jira issue. + + :param project: Jira project key (e.g. 'PLAT') + :param summary: Issue title + :param description: Issue description (markdown supported) + :param issue_type: Issue type (Story, Bug, Epic, Task). Default: Story + :return: Created issue key and URL + """ + params = { + "project": project, + "summary": summary, + "description": description, + "issueType": issue_type + } + try: + result = subprocess.run( + [ + "mcporter", + "--config", self.valves.MCPORTER_CONFIG, + "call", "atlassian", "jira_create_issue", + "--params", json.dumps(params) + ], + capture_output=True, text=True, timeout=30 + ) + if result.returncode != 0: + return f"Error: {result.stderr.strip()}" + return result.stdout.strip() + except Exception as e: + return f"Error: {str(e)}" diff --git a/openwebui/preset.json b/openwebui/preset.json new file mode 100644 index 0000000..86b04ed --- /dev/null +++ b/openwebui/preset.json @@ -0,0 +1,17 @@ +{ + "name": "Reltio PM Factory", + "description": "AI workspace for Product Managers โ€” epics, PR/FAQs, analytics, and Transparent Factory compliance.", + "base_model_id": "claude-opus-4.6", + "params": { + "system": "You are an AI assistant operating inside the **Reltio PM Factory**. You help Product Managers create epics, draft PR/FAQs, query analytics, and maintain compliance with the Transparent Factory standard.\n\n## First Interaction\nWhen the user first messages you, greet them:\n> ๐Ÿ‘‹ **Welcome to the Reltio PM Factory!**\n> I can help you draft PR/FAQs, create Jira Epics, query Gainsight analytics, brainstorm with the Creative Squad, or check Transparent Factory compliance. What would you like to do?\n\n## The Transparent Factory\nYou must adhere to the Reltio Transparent Factory tenets:\n- **Atomic Flagging:** 14-day TTL, OpenFeature-based feature flags.\n- **Elastic Schema:** Additive-only changes, sync dual-write, 30-day migration SLA.\n- **Cognitive Durability:** ADR decision logs, code readable in 60 seconds.\n- **Semantic Observability:** Reasoning spans, structured telemetry.\n- **Configurable Autonomy:** Governance guardrails for AI agents.\n\nIf a proposed spec or epic violates these tenets, flag it and suggest corrections.\n\n## Available Tools\nYou have access to these pipeline tools:\n- **jira_search** / **jira_create** โ€” Search and create Jira issues via Atlassian MCP\n- **aha_create_epic** โ€” Create epics in Aha! with proper field structure\n- **gainsight_query** โ€” Query Gainsight PX for product analytics\n- **bmad_brainstorm** โ€” Run a brainstorming session using BMad Creative Intelligence Suite\n- **factory_check** โ€” Validate a spec against Transparent Factory tenets\n\nUse these tools when the user's request maps to an external action. Always confirm before creating or modifying external resources.\n\n## Style\n- Be concise and direct.\n- Use tables and bullet points for structured data.\n- When drafting epics or PR/FAQs, follow the templates in your knowledge base.\n- If unsure about a tenet, say so rather than guessing.", + "temperature": 0.7, + "max_tokens": 4096 + }, + "meta": { + "profile_image_url": "๐Ÿญ", + "capabilities": { + "vision": false + }, + "tags": ["pm", "reltio", "factory"] + } +} diff --git a/skills/bmad-suite/SKILL.md b/skills/bmad-suite/SKILL.md new file mode 100644 index 0000000..405cc97 --- /dev/null +++ b/skills/bmad-suite/SKILL.md @@ -0,0 +1,47 @@ +--- +name: bmad-suite +description: Manage, update, and deploy BMad workflows/agents. +tools: + - name: bmad_update + description: Pull latest updates or clone missing repositories for BMad Suite. + entry: + type: python + path: manager.py + args: ["update"] + - name: bmad_list + description: List available workflows/agents in the suite. + entry: + type: python + path: manager.py + args: ["list"] +--- + +# BMad Creative Suite Manager + +This skill manages the **BMad Suite** ecosystem, handling installation (git clone) and updates (git pull). + +## Capabilities +- **Update/Install:** Automatically clones repositories if missing, or pulls latest changes if present. +- **List:** Enumerates available agents and workflows across all modules. + +## Documentation Sources +Refer to these files for detailed usage, architecture, and agent definitions: + +### 1. Framework +- **Core Documentation:** `framework/README.md` +- **Agent Definitions:** `framework/src/agents/` + +### 2. Creative Intelligence Suite (CIS) +- **Agent Catalog:** `creative-intelligence-suite/docs/reference/agents.md` +- **Main Documentation:** `creative-intelligence-suite/README.md` +- **Agent Definitions:** `creative-intelligence-suite/src/agents/*.agent.yaml` + +### 3. Test Architecture Enterprise (TEA) +- **Main Documentation:** `test-architecture-enterprise/README.md` +- **Workflows:** `test-architecture-enterprise/src/workflows/testarch/README.md` + +## Repositories +Managed repositories (auto-cloned to `../../bmad/` relative to this skill, or `$BMAD_PATH`): +1. **Framework:** `bmad-code-org/BMAD-METHOD` +2. **Creative Intelligence Suite:** `bmad-code-org/bmad-module-creative-intelligence-suite` +3. **Test Architecture Enterprise (TEA):** `bmad-code-org/bmad-method-test-architecture-enterprise` diff --git a/skills/bmad-suite/manager.py b/skills/bmad-suite/manager.py new file mode 100644 index 0000000..f94ae1a --- /dev/null +++ b/skills/bmad-suite/manager.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +import os +import sys +import subprocess + +# Determine base path: use BMAD_PATH env var, or default to ../../bmad relative to this script +# transparent_factory_site/skills/bmad-suite/manager.py -> transparent_factory_site/bmad +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +DEFAULT_BMAD_PATH = os.path.abspath(os.path.join(SCRIPT_DIR, "../../bmad")) +BASE_PATH = os.environ.get("BMAD_PATH", DEFAULT_BMAD_PATH) + +REPOS = { + "framework": { + "url": "https://github.com/bmad-code-org/BMAD-METHOD.git", + "path": os.path.join(BASE_PATH, "framework") + }, + "creative-suite": { + "url": "https://github.com/bmad-code-org/bmad-module-creative-intelligence-suite.git", + "path": os.path.join(BASE_PATH, "creative-intelligence-suite") + }, + "tea-module": { + "url": "https://github.com/bmad-code-org/bmad-method-test-architecture-enterprise.git", + "path": os.path.join(BASE_PATH, "test-architecture-enterprise") + } +} + +def update_or_clone(name, config): + """Clone if missing, pull if present.""" + repo_path = config["path"] + repo_url = config["url"] + + # Ensure parent directory exists so we can clone into it if needed + parent_dir = os.path.dirname(repo_path) + if not os.path.exists(parent_dir): + os.makedirs(parent_dir, exist_ok=True) + + # Check if it's already a git repo + if os.path.exists(os.path.join(repo_path, ".git")): + print(f"๐Ÿ”„ Updating {name}...") + try: + subprocess.run(["git", "pull"], cwd=repo_path, check=True) + print(f"โœ… {name} updated.") + except subprocess.CalledProcessError as e: + print(f"โŒ {name} update failed: {e}") + + # Check if directory exists but is empty (safe to clone into) + elif os.path.exists(repo_path) and not os.listdir(repo_path): + print(f"๐Ÿ“ฅ Cloning {name} into empty directory...") + try: + subprocess.run(["git", "clone", repo_url, "."], cwd=repo_path, check=True) + print(f"โœ… {name} cloned.") + except subprocess.CalledProcessError as e: + print(f"โŒ {name} clone failed: {e}") + + # Directory doesn't exist at all + elif not os.path.exists(repo_path): + print(f"๐Ÿ“ฅ Cloning {name}...") + try: + subprocess.run(["git", "clone", repo_url, repo_path], check=True) + print(f"โœ… {name} cloned.") + except subprocess.CalledProcessError as e: + print(f"โŒ {name} clone failed: {e}") + + else: + print(f"โš ๏ธ Target directory {repo_path} exists and is not empty (and not a git repo). Skipping.") + +def list_workflows(suite_path): + """List available workflows/agents in the suite.""" + src_path = os.path.join(suite_path, "src") + if not os.path.exists(src_path): + # Fallback to listing root if src doesn't exist (e.g. some repos might differ) + if os.path.exists(suite_path): + return subprocess.getoutput(f"find {suite_path} -maxdepth 3 -name '*.md' -o -name '*.ts' -o -name '*.js' | grep -v 'node_modules' | sort") + return "Directory not found." + + return subprocess.getoutput(f"find {src_path} -name '*.md' -o -name '*.ts' -o -name '*.js' -o -name '*.yaml' | sort") + +if __name__ == "__main__": + action = sys.argv[1] if len(sys.argv) > 1 else "list" + + if action == "update": + print("--- Checking BMad Suite Repositories ---") + for name, config in REPOS.items(): + update_or_clone(name, config) + print("") + + elif action == "list": + for name, config in REPOS.items(): + print(f"--- {name} Workflows ---") + print(list_workflows(config["path"])) + print("") + else: + print(f"Unknown action: {action}") diff --git a/skills/epics-standards/SKILL.md b/skills/epics-standards/SKILL.md new file mode 100644 index 0000000..91e4251 --- /dev/null +++ b/skills/epics-standards/SKILL.md @@ -0,0 +1,74 @@ +--- +name: epics-standards +description: Create or audit RP Jira epics and linked Aha epics against PM standards. Use when creating new RP epics, creating Aha epics for a target release, checking compliance gaps, or updating epic fields/content to align with the PM standards and Aha workflow. +--- + +# RP Epic Standards + +Use this skill for `RP` epic creation, Aha epic creation, and compliance audits. + +Primary standard source: +`https://reltio.jira.com/wiki/spaces/PM/pages/2688385025/PM+Standards+Epics` + +Workflow source and mapping: +- `references/aha-epic-workflow.md` +- `references/epic-fields-checklist.md` + +## Workflow + +1. Open the standard page and use it as source of truth. +2. Validate/collect required inputs (see Intake below). +3. Create/update Jira epic and Aha epic per workflow in `references/aha-epic-workflow.md`. +4. Link Jira and Aha (`Aha! Reference` in Jira). +5. Audit compliance against `references/epic-fields-checklist.md`. +6. If fields are not editable in Jira/Aha, document exact gaps and owner. + +## Intake (Interactive) + +If required fields are not provided in the prompt, ask concise follow-up questions before creating records. + +Minimum required fields to ask for: +- Jira epic key (if already created) or request to create one +- Aha release (for example `2026.2.0.0`) +- Epic short name (Jira/Aha title) +- Problem Statement / Why +- Solution / What +- Persona / Who +- Value Statement +- Confidence Level +- Product, Engineering, and UX leads +- Execution Team +- Required flags: Doc, Provisioning, UI/UX, Security, Training + +Ask only for missing items. Do not proceed with creation until minimum fields are available. + +## Epic Content Requirements + +Ensure description includes: +- Problem Statement / Why +- Solution / What +- Persona / Who +- Value Statement + +Keep summary short and clear for Jira readability. + +## Aha Creation Code + +Use the bundled script: +- `scripts/aha_create_epic.py` + +Example: +```bash +python3 skills/epics-standards/scripts/aha_create_epic.py \ + --release MDM-R-889 \ + --name "RDM PrivateLink on AWS" \ + --description "Tracks Jira epic RP-176273 (https://reltio.jira.com/browse/RP-176273)." \ + --jira-key RP-176273 +``` + +The script reads Aha credentials from `~/.mcporter/mcporter.json` (`mcpServers.aha.env`). + +## Integration Guardrails + +If the epic is created in Jira first, verify that Aha linkage is present (`Aha! Reference`). +If missing, update Jira with the created Aha URL. diff --git a/skills/epics-standards/agents/openai.yaml b/skills/epics-standards/agents/openai.yaml new file mode 100644 index 0000000..aee419b --- /dev/null +++ b/skills/epics-standards/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "RP Epic Standards" + short_description: "Create and audit RP epics for PM standards" + default_prompt: "Use $epics-standards to create or audit linked Jira/Aha epics and ask me for any missing required fields before creating records." diff --git a/skills/epics-standards/references/aha-epic-workflow.md b/skills/epics-standards/references/aha-epic-workflow.md new file mode 100644 index 0000000..e387d45 --- /dev/null +++ b/skills/epics-standards/references/aha-epic-workflow.md @@ -0,0 +1,55 @@ +# Aha Epic Workflow + +This file captures the expected Aha epic lifecycle and downstream triggers. + +## Idea Management + +1. Customer submits idea in Aha. +2. Check voting threshold: `5` unique orgs. +3. PM review required (`Needs Review` -> `Reviewed`). +4. PM decision: +- Accept current release -> `Planned` +- Accept next release -> `Future Consideration` +- Exists -> `Already Exists` +- Need more info -> `Needs More Info` + comment +- Reject/later -> `Future Consideration` + comment +5. Always add public customer comment after decision. +6. Ensure epic relation: +- Promote idea to new epic, or +- Link idea to existing epic. + +## Epic Creation and Management + +When epic exists in Aha, fill mandatory fields: +- Summary with PRD linkage +- Release and availability (`Preview`/`GA`) +- Confidence level (`High`/`Med`/`Low`) +- Product type and persona +- Execution team and initiative +- Product lead, engineering lead, UX lead + +Set required flags and trigger follow-ups: +- `Doc Required = Yes` -> specify doc type and doc-team flow +- `Provisioning = Yes` -> PCC/Olga flow +- `UI/UX Required = Yes` -> engage UX +- `Security Review = Yes` -> engage security +- `Training Required = Yes` -> engage training + +Integration: +- Ensure Aha -> Jira Webhooks 2.0 integration path is respected. +- Ensure Jira epic has `Aha! Reference`. + +## Enablement Outputs + +If applicable, drive enablement: +- Technical enablement session (config) +- GTM/sales enablement artifacts +- Webinar for major highlights + +Template links from workflow: +- Technical Enablement Session template: + `https://docs.google.com/presentation/d/1fCZhOUSV7McX1edmYoKBHYtKnYajykbm1U2N5aJ1j-M/edit?slide=id.g39258ed0d71_0_442` +- Value Statements input template: + `https://docs.google.com/document/d/1YEquYIjt8gMtGLf8EJFfvwS0f_ij1KuIfQFOjlcOEjI/edit` +- Sales Enablement deck example: + `https://docs.google.com/presentation/d/1mIlC3OhhQgdwcFPgJ328pm1oQl5W6y-w/edit` diff --git a/skills/epics-standards/references/epic-fields-checklist.md b/skills/epics-standards/references/epic-fields-checklist.md new file mode 100644 index 0000000..07b4934 --- /dev/null +++ b/skills/epics-standards/references/epic-fields-checklist.md @@ -0,0 +1,53 @@ +# RP Epic Field Checklist + +Standard page: +`https://reltio.jira.com/wiki/spaces/PM/pages/2688385025/PM+Standards+Epics` + +Use this checklist during epic create/update. + +## Core fields + +- `Summary` (short Jira shorthand) +- `Description` (business outcome + impact) +- `Type of Effort` +- `Fix Version` (or `N/A` when not yet planned) +- `Status` + +## Product narrative fields + +- Problem Statement / Why +- Solution / What +- Persona / Who +- Value Statement + +## Planning and delivery fields + +- Confidence Level +- Path to Green (required when Confidence is Medium/Low for must-have epics) +- Availability in Release +- Planned Release +- T-Shirt Size +- Tier +- Initiative + +## Ownership and dependencies + +- Product Lead +- Engineering Lead +- UX Lead +- Execution Team +- Execution Team Dependency + +## Go-to-market and governance + +- For documentation tickets: set `Affected Documentation` to a documentation target (for example `Help Portal`) and never `No` +- Doc Required +- Pricing Required +- Security Review Required +- Does this change PCC? +- Tags (including `Must Have` when applicable) +- Demo Link (when available) + +## Integration check + +- `Aha! Reference` is present and linked through the RP integration path. diff --git a/skills/epics-standards/scripts/aha_create_epic.py b/skills/epics-standards/scripts/aha_create_epic.py new file mode 100644 index 0000000..c9c2309 --- /dev/null +++ b/skills/epics-standards/scripts/aha_create_epic.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +""" +Create an Aha epic in a target release using credentials from ~/.mcporter/mcporter.json. + +Usage: + python3 skills/epics-standards/scripts/aha_create_epic.py \ + --release MDM-R-889 \ + --name "RDM PrivateLink on AWS" \ + --description "Tracks Jira epic RP-176273 (...)" \ + --jira-key RP-176273 +""" + +from __future__ import annotations + +import argparse +import json +import pathlib +import sys +import urllib.error +import urllib.parse +import urllib.request + + +MCPORTER_CONFIG = pathlib.Path.home() / ".mcporter" / "mcporter.json" + + +def load_aha_env() -> dict[str, str]: + cfg = json.loads(MCPORTER_CONFIG.read_text()) + env = cfg["mcpServers"]["aha"]["env"] + return {"domain": env["AHA_DOMAIN"], "token": env["AHA_API_TOKEN"]} + + +def request(method: str, url: str, token: str, payload: dict | None = None) -> dict: + headers = { + "Authorization": f"Bearer {token}", + "Accept": "application/json", + "Content-Type": "application/json", + } + body = None if payload is None else json.dumps(payload).encode("utf-8") + req = urllib.request.Request(url, data=body, headers=headers, method=method) + with urllib.request.urlopen(req, timeout=30) as resp: + return json.loads(resp.read().decode("utf-8")) + + +def release_ref_from_name(domain: str, token: str, release_name: str) -> str: + q = urllib.parse.quote(release_name) + url = f"https://{domain}.aha.io/api/v1/releases?q={q}&per_page=200" + data = request("GET", url, token) + for rel in data.get("releases", []): + if rel.get("name") == release_name: + return rel["reference_num"] + raise ValueError(f"Release not found by name: {release_name}") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--release", required=True, help="Release reference (e.g. MDM-R-889) or exact release name (e.g. 2026.2.0.0)") + parser.add_argument("--name", required=True, help="Aha epic name") + parser.add_argument("--description", required=True, help="Aha epic description/body") + parser.add_argument("--jira-key", required=False, help="Optional Jira key to print linking reminder") + args = parser.parse_args() + + try: + aha = load_aha_env() + release = args.release + if not release.startswith("MDM-R-"): + release = release_ref_from_name(aha["domain"], aha["token"], release) + + url = f"https://{aha['domain']}.aha.io/api/v1/releases/{release}/epics" + payload = {"epic": {"name": args.name, "description": args.description}} + data = request("POST", url, aha["token"], payload) + epic = data.get("epic", {}) + + print(json.dumps( + { + "aha_reference": epic.get("reference_num"), + "aha_url": epic.get("url"), + "release": release, + "jira_key": args.jira_key, + }, + ensure_ascii=True, + )) + + if args.jira_key and epic.get("url"): + print( + f"Next: set Jira {args.jira_key} Aha! Reference = {epic['url']}", + file=sys.stderr, + ) + return 0 + except (KeyError, FileNotFoundError, ValueError) as exc: + print(f"Config/input error: {exc}", file=sys.stderr) + return 2 + except urllib.error.HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace") + print(f"Aha API HTTP {exc.code}: {detail}", file=sys.stderr) + return 3 + except Exception as exc: # noqa: BLE001 + print(f"Unexpected error: {exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/factory-standards/SKILL.md b/skills/factory-standards/SKILL.md new file mode 100644 index 0000000..8e5ad41 --- /dev/null +++ b/skills/factory-standards/SKILL.md @@ -0,0 +1,31 @@ +--- +name: factory-standards +description: Manage and access the Transparent Factory engineering and product tenets from Bitbucket. +tools: + - name: factory_update + description: Clone or pull the latest Transparent Factory rules and PR/FAQ from the central repository. + entry: + type: python + path: manager.py + args: ["update"] + - name: factory_list + description: List available tenets, standards, and guidelines in the Transparent Factory repository. + entry: + type: python + path: manager.py + args: ["list"] +--- + +# Transparent Factory Standards Manager + +This skill synchronizes the definitive **Transparent Factory** architectural and product rules into your local project from the central Bitbucket repository. + +## Capabilities +- **Update/Install:** Automatically clones `reltio-ondemand/transparent-factory.git` if missing, or pulls latest changes if present. +- **List:** Enumerates available tenets (`content/`), PR/FAQ documents, and exported factory skills. + +## Architecture +Managed files are auto-cloned to `.standards/factory/` relative to the root of your project workspace (e.g., if this skill is in `skills/factory-standards/`, it clones to `../../.standards/factory/`). + +## How to use in AI Prompts +If you are an AI agent, you must run `factory_update` to ensure the local `.standards/factory/` directory is present and up-to-date before writing any Product Requirements Documents, Epics, or technical specifications. diff --git a/skills/factory-standards/manager.py b/skills/factory-standards/manager.py new file mode 100644 index 0000000..472b50f --- /dev/null +++ b/skills/factory-standards/manager.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +import os +import sys +import subprocess + +# Determine base path: use FACTORY_PATH env var, or default to ../../.standards/factory relative to this script +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +DEFAULT_FACTORY_PATH = os.path.abspath(os.path.join(SCRIPT_DIR, "../../.standards/factory")) +FACTORY_PATH = os.environ.get("FACTORY_PATH", DEFAULT_FACTORY_PATH) + +REPO_URL = "git@bitbucket.org:reltio-ondemand/transparent-factory.git" + +def ensure_directory(path): + """Ensure the parent directory exists.""" + parent = os.path.dirname(path) + if not os.path.exists(parent): + print(f"๐Ÿ“‚ Creating directory: {parent}") + os.makedirs(parent, exist_ok=True) + +def update_or_clone(): + """Clone the Transparent Factory repo if missing, pull if present.""" + ensure_directory(FACTORY_PATH) + + # Check if it's already a git repo + if os.path.exists(os.path.join(FACTORY_PATH, ".git")): + print(f"๐Ÿ”„ Updating Transparent Factory Standards...") + try: + subprocess.run(["git", "pull"], cwd=FACTORY_PATH, check=True) + print(f"โœ… Standards updated at {FACTORY_PATH}.") + except subprocess.CalledProcessError as e: + print(f"โŒ Update failed: {e}") + + # Check if directory exists but is empty (safe to clone into) + elif os.path.exists(FACTORY_PATH) and not os.listdir(FACTORY_PATH): + print(f"๐Ÿ“ฅ Cloning Transparent Factory Standards into empty directory...") + try: + subprocess.run(["git", "clone", REPO_URL, "."], cwd=FACTORY_PATH, check=True) + print(f"โœ… Standards cloned to {FACTORY_PATH}.") + except subprocess.CalledProcessError as e: + print(f"โŒ Clone failed: {e}") + + # Directory doesn't exist at all + elif not os.path.exists(FACTORY_PATH): + print(f"๐Ÿ“ฅ Cloning Transparent Factory Standards...") + try: + subprocess.run(["git", "clone", REPO_URL, FACTORY_PATH], check=True) + print(f"โœ… Standards cloned to {FACTORY_PATH}.") + except subprocess.CalledProcessError as e: + print(f"โŒ Clone failed: {e}") + + else: + print(f"โš ๏ธ Target directory {FACTORY_PATH} exists and is not empty (and not a git repo). Skipping.") + +def list_standards(): + """List available standards and PR/FAQ documents.""" + if not os.path.exists(FACTORY_PATH): + return "Standards not found. Run 'factory_update' first." + + print("--- Core Documents ---") + try: + # Look for the primary PR/FAQ or README + print(subprocess.getoutput(f"find {FACTORY_PATH} -maxdepth 1 -name '*.md' | sort")) + + # Look inside the content folder + content_path = os.path.join(FACTORY_PATH, "content") + if os.path.exists(content_path): + print("\n--- Tenets & Content ---") + print(subprocess.getoutput(f"find {content_path} -name '*.md' | sort")) + + # Look inside skills folder + skills_path = os.path.join(FACTORY_PATH, "skills") + if os.path.exists(skills_path): + print("\n--- Available Factory Skills ---") + print(subprocess.getoutput(f"find {skills_path} -name 'SKILL.md' | sort")) + except Exception as e: + return f"Error listing files: {e}" + + return "" + +if __name__ == "__main__": + action = sys.argv[1] if len(sys.argv) > 1 else "list" + + if action == "update": + update_or_clone() + elif action == "list": + print(list_standards()) + else: + print(f"Unknown action: {action}") + sys.exit(1) diff --git a/skills/gainsight-px/SKILL.md b/skills/gainsight-px/SKILL.md new file mode 100644 index 0000000..ba97961 --- /dev/null +++ b/skills/gainsight-px/SKILL.md @@ -0,0 +1,54 @@ +--- +name: gainsight-px +description: Interact directly with the Gainsight PX REST API to fetch user/account data or track events. +tools: + - name: px_get_user + description: Fetch a specific user by their unique identity ID. + entry: + type: python + path: gainsight_px.py + args: ["get_user"] + - name: px_get_account + description: Fetch a specific account by its ID. + entry: + type: python + path: gainsight_px.py + args: ["get_account"] + - name: px_search_user + description: Search for a user in Gainsight PX by their email address. + entry: + type: python + path: gainsight_px.py + args: ["search_user"] + - name: px_track_event + description: Track a custom event for a user in Gainsight PX. Requires user_id, event_name, and optional JSON properties. + entry: + type: python + path: gainsight_px.py + args: ["track_event"] +--- + +# Gainsight PX REST API Skill + +This skill allows agents to natively interface with your Gainsight PX instance without needing an intermediate MCP server like Pipedream or Zapier. + +## Setup + +You must export your API key before using the tools. You can generate an API key from your Gainsight PX Administration -> REST API section. + +```bash +# Add this to your environment +export GAINSIGHT_PX_API_KEY="your-api-key-here" + +# Optional: If you are in the EU region, set this flag. Default is US. +export GAINSIGHT_PX_REGION="EU" +``` + +## How It Works + +It uses a lightweight Python script (`gainsight_px.py`) that implements standard REST endpoints documented by Apiary (`https://api.aptrinsic.com/v1/...`). + +### Capabilities +- **Lookups:** Find exactly who a user is by ID or email. +- **Account Context:** Pull account metadata. +- **Event Injection:** Push arbitrary telemetry events natively. diff --git a/skills/gainsight-px/gainsight_px.py b/skills/gainsight-px/gainsight_px.py new file mode 100644 index 0000000..b0efdb7 --- /dev/null +++ b/skills/gainsight-px/gainsight_px.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +import os +import sys +import json +import urllib.request +import urllib.error + +# Gainsight PX API configuration +# Region determines the base URL (US or EU) +PX_REGION = os.environ.get("GAINSIGHT_PX_REGION", "US").upper() +PX_API_KEY = os.environ.get("GAINSIGHT_PX_API_KEY") + +if PX_REGION == "EU": + BASE_URL = "https://eu-api.aptrinsic.com/v1" +else: + BASE_URL = "https://api.aptrinsic.com/v1" + +def make_request(method, endpoint, data=None): + if not PX_API_KEY: + print(json.dumps({"error": "GAINSIGHT_PX_API_KEY environment variable is missing."})) + sys.exit(1) + + url = f"{BASE_URL}{endpoint}" + headers = { + "X-APITOKEN": PX_API_KEY, + "Content-Type": "application/json", + "Accept": "application/json" + } + + req_data = None + if data: + req_data = json.dumps(data).encode("utf-8") + + req = urllib.request.Request(url, data=req_data, headers=headers, method=method) + + try: + with urllib.request.urlopen(req) as response: + return json.loads(response.read().decode("utf-8")) + except urllib.error.HTTPError as e: + err_msg = e.read().decode("utf-8") + try: + parsed_err = json.loads(err_msg) + return {"error": f"HTTP {e.code}", "details": parsed_err} + except: + return {"error": f"HTTP {e.code}", "details": err_msg} + except Exception as e: + return {"error": str(e)} + +def get_user(user_id): + """Retrieve a specific user by their identifyId.""" + return make_request("GET", f"/users/{user_id}") + +def get_account(account_id): + """Retrieve a specific account by its id.""" + return make_request("GET", f"/accounts/{account_id}") + +def search_users(email): + """Search for users by email (requires query payload).""" + payload = { + "filter": { + "operator": "AND", + "conditions": [ + { + "name": "email", + "operator": "EQ", + "value": email + } + ] + } + } + return make_request("POST", "/users/query", data=payload) + +def track_event(user_id, event_name, properties=None): + """Track a custom event for a specific user.""" + payload = { + "identifyId": user_id, + "eventName": event_name, + "properties": properties or {} + } + # Note: tracking usually happens via a different endpoint or batch API, + # but for simplicity assuming a standard REST event ingestion if available. + return make_request("POST", "/events/custom", data=payload) + +if __name__ == "__main__": + if len(sys.argv) < 2: + print(json.dumps({"error": "Missing action. Use: get_user, get_account, search_user, track_event"})) + sys.exit(1) + + action = sys.argv[1] + + if action == "get_user" and len(sys.argv) == 3: + print(json.dumps(get_user(sys.argv[2]), indent=2)) + + elif action == "get_account" and len(sys.argv) == 3: + print(json.dumps(get_account(sys.argv[2]), indent=2)) + + elif action == "search_user" and len(sys.argv) == 3: + print(json.dumps(search_users(sys.argv[2]), indent=2)) + + elif action == "track_event" and len(sys.argv) >= 4: + user_id = sys.argv[2] + event_name = sys.argv[3] + props = json.loads(sys.argv[4]) if len(sys.argv) > 4 else {} + print(json.dumps(track_event(user_id, event_name, props), indent=2)) + + else: + print(json.dumps({"error": f"Unknown action or missing arguments: {action}"})) + sys.exit(1) diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..bdfd392 --- /dev/null +++ b/start.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +cd "$SCRIPT_DIR" + +# --- Check Node.js --- +if ! command -v node &>/dev/null; then + echo "โŒ Node.js is not installed." + echo " Install it from https://nodejs.org (LTS recommended)" + exit 1 +fi + +echo "โœ… Node.js $(node -v)" + +# --- Check .env --- +if [ ! -f .env ]; then + if [ -f env.example ]; then + cp env.example .env + echo "โš ๏ธ Created .env from template. Please edit it with your API keys, then re-run this script." + exit 1 + else + echo "โš ๏ธ No .env or env.example found. Continuing without environment config." + fi +fi + +# --- Install web dependencies --- +if [ ! -d web/node_modules ]; then + echo "๐Ÿ“ฆ Installing dependencies..." + cd web && npm install && cd .. +fi + +# --- Launch --- +echo "" +echo "๐Ÿ”ฅ Starting Forge Console..." +echo "" + +# Open browser after a short delay +(sleep 2 && { + URL="http://localhost:${PORT:-3000}" + if command -v xdg-open &>/dev/null; then + xdg-open "$URL" 2>/dev/null + elif command -v open &>/dev/null; then + open "$URL" + elif command -v start &>/dev/null; then + start "$URL" + else + echo " Open $URL in your browser" + fi +}) & + +# Run server (trap for clean exit) +trap 'echo ""; echo "๐Ÿ‘‹ Shutting down..."; kill %1 2>/dev/null; exit 0' INT TERM +cd web && node server.js diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..da62ccb --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,849 @@ +{ + "name": "pm-template-web", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pm-template-web", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.2", + "ws": "^8.16.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/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/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/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/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/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/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..c696236 --- /dev/null +++ b/web/package.json @@ -0,0 +1,13 @@ +{ + "name": "pm-template-web", + "version": "1.0.0", + "description": "Minimal local web chat UI", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.18.2", + "ws": "^8.16.0" + } +} diff --git a/web/public/index.html b/web/public/index.html new file mode 100644 index 0000000..a1df1ed --- /dev/null +++ b/web/public/index.html @@ -0,0 +1,127 @@ + + + + + + Forge Console + + + +
+
+ + Disconnected +
+
+
+ + +
+
+ + + diff --git a/web/public/style.css b/web/public/style.css new file mode 100644 index 0000000..ccba73c --- /dev/null +++ b/web/public/style.css @@ -0,0 +1,176 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } + +:root { + --bg: #1a1a2e; + --surface: #16213e; + --input-bg: #0f3460; + --user-bg: #1a3a5c; + --assistant-bg: #2a2a3e; + --system-bg: #2e1a1a; + --text: #e0e0e0; + --text-dim: #8888aa; + --accent: #4a9eff; + --green: #4caf50; + --red: #f44336; +} + +html, body { height: 100%; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + background: var(--bg); + color: var(--text); +} + +#app { + display: flex; + flex-direction: column; + height: 100vh; + max-width: 800px; + margin: 0 auto; +} + +/* Status bar */ +#status-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 16px; + background: var(--surface); + border-bottom: 1px solid rgba(255,255,255,0.05); + font-size: 13px; +} + +.logo { + font-weight: 600; + font-size: 15px; + letter-spacing: 0.5px; +} + +.dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 6px; + vertical-align: middle; +} +.dot.green { background: var(--green); } +.dot.red { background: var(--red); } + +#conn-status { color: var(--text-dim); font-size: 12px; } + +/* Messages */ +#messages { + flex: 1; + overflow-y: auto; + padding: 16px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.msg { + max-width: 85%; + padding: 10px 14px; + border-radius: 12px; + font-size: 14px; + line-height: 1.5; + white-space: pre-wrap; + word-wrap: break-word; + animation: fadeIn 0.2s ease; +} + +.msg.user { + align-self: flex-end; + background: var(--user-bg); + border-bottom-right-radius: 4px; +} + +.msg.assistant { + align-self: flex-start; + background: var(--assistant-bg); + border-bottom-left-radius: 4px; + font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace; + font-size: 13px; +} + +.msg.system { + align-self: center; + background: var(--system-bg); + color: var(--text-dim); + font-size: 12px; + border-radius: 8px; +} + +/* Thinking dots */ +.thinking .dots span { + animation: blink 1.4s infinite; + font-size: 24px; + line-height: 1; +} +.thinking .dots span:nth-child(2) { animation-delay: 0.2s; } +.thinking .dots span:nth-child(3) { animation-delay: 0.4s; } + +@keyframes blink { + 0%, 20% { opacity: 0.2; } + 50% { opacity: 1; } + 80%, 100% { opacity: 0.2; } +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Input bar */ +#input-bar { + display: flex; + gap: 8px; + padding: 12px 16px; + background: var(--surface); + border-top: 1px solid rgba(255,255,255,0.05); +} + +#input { + flex: 1; + background: var(--input-bg); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 10px; + padding: 10px 14px; + color: var(--text); + font-size: 14px; + font-family: inherit; + resize: none; + outline: none; + transition: border-color 0.2s; +} + +#input:focus { border-color: var(--accent); } + +#input::placeholder { color: var(--text-dim); } + +#send { + background: var(--accent); + border: none; + border-radius: 10px; + width: 44px; + color: white; + font-size: 18px; + cursor: pointer; + transition: opacity 0.2s; +} + +#send:hover { opacity: 0.85; } +#send:active { opacity: 0.7; } + +/* Scrollbar */ +#messages::-webkit-scrollbar { width: 6px; } +#messages::-webkit-scrollbar-track { background: transparent; } +#messages::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; } + +/* Mobile */ +@media (max-width: 600px) { + #app { max-width: 100%; } + .msg { max-width: 92%; } +} diff --git a/web/server.js b/web/server.js new file mode 100644 index 0000000..113b0e6 --- /dev/null +++ b/web/server.js @@ -0,0 +1,77 @@ +const express = require('express'); +const { WebSocketServer } = require('ws'); +const { spawn } = require('child_process'); +const path = require('path'); +const http = require('http'); + +const PORT = process.env.PORT || 3000; +const app = express(); +const server = http.createServer(app); +const wss = new WebSocketServer({ server, path: '/ws' }); + +app.use(express.static(path.join(__dirname, 'public'))); +app.get('/health', (_, res) => res.json({ status: 'ok' })); + +wss.on('connection', (ws) => { + console.log('[forge] client connected'); + + // Spawn codex in interactive mode, working dir = repo root + const repoRoot = path.resolve(__dirname, '..'); + const codex = spawn('codex', ['--quiet'], { + cwd: repoRoot, + shell: true, + env: { ...process.env, FORCE_COLOR: '0' }, + stdio: ['pipe', 'pipe', 'pipe'] + }); + + let alive = true; + + codex.stdout.on('data', (data) => { + if (ws.readyState === 1) { + ws.send(JSON.stringify({ type: 'stdout', data: data.toString() })); + } + }); + + codex.stderr.on('data', (data) => { + if (ws.readyState === 1) { + ws.send(JSON.stringify({ type: 'stderr', data: data.toString() })); + } + }); + + codex.on('close', (code) => { + alive = false; + console.log(`[forge] codex exited (code ${code})`); + if (ws.readyState === 1) { + ws.send(JSON.stringify({ type: 'exit', code })); + } + }); + + codex.on('error', (err) => { + alive = false; + console.error('[forge] codex spawn error:', err.message); + if (ws.readyState === 1) { + ws.send(JSON.stringify({ type: 'error', data: err.message })); + } + }); + + ws.on('message', (msg) => { + if (alive && codex.stdin.writable) { + codex.stdin.write(msg.toString() + '\n'); + } + }); + + ws.on('close', () => { + console.log('[forge] client disconnected'); + if (alive) codex.kill('SIGTERM'); + }); +}); + +server.listen(PORT, () => { + console.log(`\n ๐Ÿ”ฅ Forge Console running at http://localhost:${PORT}\n`); +}); + +process.on('SIGINT', () => { + console.log('\n[forge] shutting down...'); + wss.clients.forEach((ws) => ws.close()); + server.close(() => process.exit(0)); +});