Initial commit: Product OS platform
- Control Plane API with Gemini integration - Executors: Deploy, Analytics, Marketing - MCP Adapter for Continue integration - VSCode/VSCodium extension - Tool registry and run tracking - In-memory storage for local dev - Terraform infrastructure setup
This commit is contained in:
22
platform/backend/executors/analytics/package.json
Normal file
22
platform/backend/executors/analytics/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@productos/analytics-executor",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^10.0.0",
|
||||
"@fastify/sensible": "^6.0.0",
|
||||
"fastify": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
91
platform/backend/executors/analytics/src/index.ts
Normal file
91
platform/backend/executors/analytics/src/index.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import Fastify from "fastify";
|
||||
import cors from "@fastify/cors";
|
||||
import sensible from "@fastify/sensible";
|
||||
|
||||
const app = Fastify({ logger: true });
|
||||
|
||||
await app.register(cors, { origin: true });
|
||||
await app.register(sensible);
|
||||
|
||||
app.get("/healthz", async () => ({ ok: true, executor: "analytics" }));
|
||||
|
||||
/**
|
||||
* Get funnel summary
|
||||
* In production: queries BigQuery
|
||||
*/
|
||||
app.post("/execute/analytics/funnel", async (req) => {
|
||||
const body = req.body as any;
|
||||
const { input } = body;
|
||||
|
||||
console.log(`📊 Funnel request:`, input);
|
||||
|
||||
// Mock funnel data
|
||||
const steps = [
|
||||
{ event_name: "page_view", users: 10000, conversion_from_prev: 1.0 },
|
||||
{ event_name: "signup_start", users: 3200, conversion_from_prev: 0.32 },
|
||||
{ event_name: "signup_complete", users: 2100, conversion_from_prev: 0.66 },
|
||||
{ event_name: "first_action", users: 1400, conversion_from_prev: 0.67 },
|
||||
{ event_name: "subscription", users: 420, conversion_from_prev: 0.30 }
|
||||
];
|
||||
|
||||
return {
|
||||
funnel_name: input.funnel?.name ?? "default_funnel",
|
||||
range_days: input.range_days,
|
||||
steps,
|
||||
overall_conversion: 0.042,
|
||||
generated_at: new Date().toISOString()
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Get top drivers for a metric
|
||||
*/
|
||||
app.post("/execute/analytics/top_drivers", async (req) => {
|
||||
const body = req.body as any;
|
||||
const { input } = body;
|
||||
|
||||
console.log(`🔍 Top drivers request:`, input);
|
||||
|
||||
// Mock driver analysis
|
||||
const drivers = [
|
||||
{ name: "completed_onboarding", score: 0.85, direction: "positive", evidence: "Users who complete onboarding convert 3.2x more", confidence: 0.92 },
|
||||
{ name: "used_feature_x", score: 0.72, direction: "positive", evidence: "Feature X usage correlates with 2.5x retention", confidence: 0.88 },
|
||||
{ name: "time_to_first_value", score: 0.68, direction: "negative", evidence: "Each additional day reduces conversion by 12%", confidence: 0.85 },
|
||||
{ name: "invited_team_member", score: 0.61, direction: "positive", evidence: "Team invites increase stickiness by 40%", confidence: 0.79 },
|
||||
{ name: "mobile_signup", score: 0.45, direction: "negative", evidence: "Mobile signups have 25% lower activation", confidence: 0.71 }
|
||||
];
|
||||
|
||||
return {
|
||||
target: input.target,
|
||||
range_days: input.range_days,
|
||||
drivers,
|
||||
generated_at: new Date().toISOString()
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Write an insight
|
||||
*/
|
||||
app.post("/execute/analytics/write_insight", async (req) => {
|
||||
const body = req.body as any;
|
||||
const { input, run_id } = body;
|
||||
|
||||
console.log(`💡 Write insight:`, input.insight?.title);
|
||||
|
||||
const insightId = `insight_${Date.now().toString(36)}`;
|
||||
|
||||
return {
|
||||
insight_id: insightId,
|
||||
stored: {
|
||||
bigquery: { dataset: "insights", table: "insights_v1" },
|
||||
firestore: { collection: "insights", doc_id: insightId },
|
||||
gcs: { bucket: "productos-artifacts", prefix: `insights/${insightId}` }
|
||||
},
|
||||
created_at: new Date().toISOString()
|
||||
};
|
||||
});
|
||||
|
||||
const port = Number(process.env.PORT ?? 8091);
|
||||
app.listen({ port, host: "0.0.0.0" }).then(() => {
|
||||
console.log(`📈 Analytics Executor running on http://localhost:${port}`);
|
||||
});
|
||||
13
platform/backend/executors/analytics/tsconfig.json
Normal file
13
platform/backend/executors/analytics/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
23
platform/backend/executors/deploy/package.json
Normal file
23
platform/backend/executors/deploy/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@productos/deploy-executor",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^10.0.0",
|
||||
"@fastify/sensible": "^6.0.0",
|
||||
"fastify": "^5.0.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
91
platform/backend/executors/deploy/src/index.ts
Normal file
91
platform/backend/executors/deploy/src/index.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import Fastify from "fastify";
|
||||
import cors from "@fastify/cors";
|
||||
import sensible from "@fastify/sensible";
|
||||
|
||||
const app = Fastify({ logger: true });
|
||||
|
||||
await app.register(cors, { origin: true });
|
||||
await app.register(sensible);
|
||||
|
||||
// Health check
|
||||
app.get("/healthz", async () => ({ ok: true, executor: "deploy" }));
|
||||
|
||||
/**
|
||||
* Deploy a Cloud Run service
|
||||
* In production: triggers Cloud Build, deploys to Cloud Run
|
||||
* In dev: returns mock response
|
||||
*/
|
||||
app.post("/execute/cloudrun/deploy", async (req) => {
|
||||
const body = req.body as any;
|
||||
const { run_id, tenant_id, input } = body;
|
||||
|
||||
console.log(`🚀 Deploy request:`, { run_id, tenant_id, input });
|
||||
|
||||
// Simulate deployment time
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
|
||||
// In production, this would:
|
||||
// 1. Clone the repo
|
||||
// 2. Trigger Cloud Build
|
||||
// 3. Deploy to Cloud Run
|
||||
// 4. Return the service URL
|
||||
|
||||
const mockRevision = `${input.service_name}-${Date.now().toString(36)}`;
|
||||
const mockUrl = `https://${input.service_name}-abc123.a.run.app`;
|
||||
|
||||
console.log(`✅ Deploy complete:`, { revision: mockRevision, url: mockUrl });
|
||||
|
||||
return {
|
||||
service_url: mockUrl,
|
||||
revision: mockRevision,
|
||||
build_id: `build-${Date.now()}`,
|
||||
deployed_at: new Date().toISOString(),
|
||||
region: input.region ?? "us-central1",
|
||||
env: input.env
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Get Cloud Run service status
|
||||
*/
|
||||
app.post("/execute/cloudrun/status", async (req) => {
|
||||
const body = req.body as any;
|
||||
const { input } = body;
|
||||
|
||||
console.log(`📊 Status request:`, input);
|
||||
|
||||
// Mock status response
|
||||
return {
|
||||
service_name: input.service_name,
|
||||
region: input.region,
|
||||
service_url: `https://${input.service_name}-abc123.a.run.app`,
|
||||
latest_ready_revision: `${input.service_name}-v1`,
|
||||
status: "ready",
|
||||
last_deploy_time: new Date().toISOString(),
|
||||
traffic: [{ revision: `${input.service_name}-v1`, percent: 100 }]
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Rollback to a previous revision
|
||||
*/
|
||||
app.post("/execute/cloudrun/rollback", async (req) => {
|
||||
const body = req.body as any;
|
||||
const { input } = body;
|
||||
|
||||
console.log(`⏪ Rollback request:`, input);
|
||||
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
|
||||
return {
|
||||
service_name: input.service_name,
|
||||
rolled_back_to: input.target_revision ?? "previous",
|
||||
status: "ready",
|
||||
rolled_back_at: new Date().toISOString()
|
||||
};
|
||||
});
|
||||
|
||||
const port = Number(process.env.PORT ?? 8090);
|
||||
app.listen({ port, host: "0.0.0.0" }).then(() => {
|
||||
console.log(`🔧 Deploy Executor running on http://localhost:${port}`);
|
||||
});
|
||||
13
platform/backend/executors/deploy/tsconfig.json
Normal file
13
platform/backend/executors/deploy/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
22
platform/backend/executors/marketing/package.json
Normal file
22
platform/backend/executors/marketing/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@productos/marketing-executor",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^10.0.0",
|
||||
"@fastify/sensible": "^6.0.0",
|
||||
"fastify": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
88
platform/backend/executors/marketing/src/index.ts
Normal file
88
platform/backend/executors/marketing/src/index.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import Fastify from "fastify";
|
||||
import cors from "@fastify/cors";
|
||||
import sensible from "@fastify/sensible";
|
||||
|
||||
const app = Fastify({ logger: true });
|
||||
|
||||
await app.register(cors, { origin: true });
|
||||
await app.register(sensible);
|
||||
|
||||
app.get("/healthz", async () => ({ ok: true, executor: "marketing" }));
|
||||
|
||||
/**
|
||||
* Generate channel posts from a brief
|
||||
* In production: calls Gemini API
|
||||
*/
|
||||
app.post("/execute/marketing/generate", async (req) => {
|
||||
const body = req.body as any;
|
||||
const { input } = body;
|
||||
|
||||
console.log(`✍️ Generate posts:`, input.brief?.goal);
|
||||
|
||||
await new Promise(r => setTimeout(r, 1000)); // Simulate AI generation time
|
||||
|
||||
const channels = (input.channels ?? ["x", "linkedin"]).map((channel: string) => ({
|
||||
channel,
|
||||
posts: [
|
||||
{
|
||||
text: `🚀 Exciting news! ${input.brief?.product ?? "Our product"} just got even better. ${input.brief?.key_points?.[0] ?? "Check it out!"}\n\n${input.brief?.call_to_action ?? "Learn more"} 👇\n${input.brief?.landing_page_url ?? "https://example.com"}`,
|
||||
hashtags: ["#ProductUpdate", "#SaaS", "#Innovation"],
|
||||
media_suggestions: ["product-screenshot.png", "feature-demo.gif"]
|
||||
},
|
||||
{
|
||||
text: `${input.brief?.audience ?? "Teams"} asked, we listened! Introducing ${input.brief?.product ?? "new features"} that will transform how you work.\n\n✨ ${input.brief?.key_points?.join("\n✨ ") ?? "Amazing new capabilities"}\n\nTry it today!`,
|
||||
hashtags: ["#ProductLaunch", "#Productivity"],
|
||||
media_suggestions: ["comparison-chart.png"]
|
||||
},
|
||||
{
|
||||
text: `Did you know? ${input.brief?.key_points?.[0] ?? "Our latest update"} can save you hours every week.\n\nHere's how ${input.brief?.product ?? "it"} works:\n1️⃣ Set it up in minutes\n2️⃣ Let automation do the work\n3️⃣ Focus on what matters\n\n${input.brief?.offer ?? "Start free today!"}`,
|
||||
hashtags: ["#Automation", "#WorkSmarter"],
|
||||
media_suggestions: ["how-it-works.mp4"]
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
return {
|
||||
channels,
|
||||
brief_summary: input.brief?.goal,
|
||||
generated_at: new Date().toISOString(),
|
||||
variations_per_channel: 3
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Get brand profile
|
||||
*/
|
||||
app.post("/execute/brand/get", async (req) => {
|
||||
const body = req.body as any;
|
||||
const { input } = body;
|
||||
|
||||
console.log(`🎨 Get brand profile:`, input.profile_id);
|
||||
|
||||
return {
|
||||
profile_id: input.profile_id ?? "default",
|
||||
brand: {
|
||||
name: "Product OS",
|
||||
voice: {
|
||||
tone: ["professional", "friendly", "innovative"],
|
||||
style_notes: ["Use active voice", "Keep sentences short", "Be specific with benefits"],
|
||||
do: ["Use data and metrics", "Include calls to action", "Highlight customer success"],
|
||||
dont: ["Make unverified claims", "Use jargon", "Be overly salesy"]
|
||||
},
|
||||
audience: {
|
||||
primary: "SaaS founders and product teams",
|
||||
secondary: "Growth marketers and developers"
|
||||
},
|
||||
claims_policy: {
|
||||
forbidden_claims: ["#1 in the market", "Guaranteed results"],
|
||||
required_disclaimers: [],
|
||||
compliance_notes: ["All metrics should be verifiable"]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const port = Number(process.env.PORT ?? 8093);
|
||||
app.listen({ port, host: "0.0.0.0" }).then(() => {
|
||||
console.log(`📣 Marketing Executor running on http://localhost:${port}`);
|
||||
});
|
||||
13
platform/backend/executors/marketing/tsconfig.json
Normal file
13
platform/backend/executors/marketing/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user