diff --git a/templates/config.json b/templates/config.json
index 4d013eb0..755d15e3 100644
--- a/templates/config.json
+++ b/templates/config.json
@@ -21,6 +21,34 @@
},
"tags": ["AI Agents", "Developer Tools", "Automation"]
},
+ {
+ "id": "codegraph",
+ "name": "colbymchenry/codegraph",
+ "description": "Pre-indexed code knowledge graph for Claude Code, Codex, Gemini, Cursor, OpenCode, AntiGravity, Kiro, and Hermes Agent — fewer tokens, fewer tool calls, 100% local",
+ "repo": "https://github.com/Phala-Network/phala-cloud/tree/main/templates/prebuilt/codegraph",
+ "author": "colbymchenry",
+ "icon": "codegraph.svg",
+ "envs": [
+ {
+ "key": "CODEGRAPH_PACKAGE_VERSION",
+ "required": false,
+ "default": "0.9.7",
+ "description": "Pinned @colbymchenry/codegraph npm package version installed by the demo service at container startup."
+ },
+ {
+ "key": "CODEGRAPH_DEMO_QUERY",
+ "required": false,
+ "default": "cart checkout flow",
+ "description": "Task string passed to the local codegraph context command by the /demo endpoint."
+ }
+ ],
+ "defaultResource": {
+ "vCPU": 1,
+ "memory": 2048,
+ "diskSize": 10
+ },
+ "tags": ["AI Apps & Workflows", "Developer Tools", "MCP Servers"]
+ },
{
"id": "VibeVM",
"name": "VibeVM",
diff --git a/templates/icons/codegraph.svg b/templates/icons/codegraph.svg
new file mode 100644
index 00000000..2dc71723
--- /dev/null
+++ b/templates/icons/codegraph.svg
@@ -0,0 +1,8 @@
+
diff --git a/templates/prebuilt/codegraph/README.md b/templates/prebuilt/codegraph/README.md
new file mode 100644
index 00000000..51b2d7a3
--- /dev/null
+++ b/templates/prebuilt/codegraph/README.md
@@ -0,0 +1,124 @@
+# colbymchenry/codegraph
+
+Deploy a CPU-safe CodeGraph verifier on Phala Cloud.
+
+## Overview
+
+CodeGraph is a local code-intelligence and knowledge-graph tool for AI coding agents. It indexes a project into a `.codegraph/` SQLite database, then exposes fast symbol search, call graph, context-building, impact analysis, and MCP tools for Claude Code, Codex, Gemini, Cursor, OpenCode, AntiGravity, Kiro, and Hermes Agent.
+
+This template does not run a hosted LLM, browser auth flow, IDE extension, or training stack. It installs the real `@colbymchenry/codegraph` npm package, creates a small bundled TypeScript project, runs `codegraph init -i`, and serves deterministic HTTP endpoints that exercise the CodeGraph CLI locally.
+
+No external model provider, API key, GPU, model weight download, privileged mode, host networking, host bind mount, Docker socket, or `env_file` is used.
+
+## Metadata
+
+- Template id: `codegraph`
+- Display name: `colbymchenry/codegraph`
+- Category: AI Apps & Workflows
+- Description: Pre-indexed code knowledge graph for Claude Code, Codex, Gemini, Cursor, OpenCode, AntiGravity, Kiro, and Hermes Agent — fewer tokens, fewer tool calls, 100% local
+- Upstream repository: https://github.com/colbymchenry/codegraph
+- Upstream documentation: https://colbymchenry.github.io/codegraph/
+- npm package: https://www.npmjs.com/package/@colbymchenry/codegraph
+- Icon source: upstream `site/public/favicon.svg` from https://github.com/colbymchenry/codegraph/blob/main/site/public/favicon.svg
+- Upstream author: Colby McHenry / `colbymchenry`
+
+## What This Template Runs
+
+- `app`: A `node:24-bookworm-slim` HTTP service.
+- On startup, the service installs `@colbymchenry/codegraph` with npm.
+- The service writes a tiny TypeScript checkout sample under `/workspace/codegraph-demo`.
+- The service runs `codegraph init -i` to build the local `.codegraph/` index.
+- Runtime checks use `codegraph status --json`, `codegraph files --json`, `codegraph query --json`, `codegraph callers --json`, `codegraph callees --json`, `codegraph impact --json`, and `codegraph context`.
+
+The default project is intentionally small so it starts on a CPU-only `tdx.small` deployment and verifies CodeGraph without credentials or external agent tooling.
+
+## Ports
+
+- `8080`: Public HTTP endpoint for health, demo, and model-list checks.
+
+## Environment Variables
+
+No credentials are required.
+
+| Variable | Required | Default | Description |
+| --- | --- | --- | --- |
+| `CODEGRAPH_PACKAGE_VERSION` | No | `0.9.7` | Pinned `@colbymchenry/codegraph` npm package version installed by the demo service at container startup. |
+| `CODEGRAPH_DEMO_QUERY` | No | `cart checkout flow` | Task string passed to `codegraph context` by the `/demo` endpoint. |
+
+## Deploy
+
+1. Deploy the `codegraph` template on Phala Cloud.
+2. Keep the default CPU-only resources for the verifier.
+3. Optionally set `CODEGRAPH_PACKAGE_VERSION` to another published package version.
+4. Open `https:///healthz` after startup completes.
+
+The first startup downloads the npm package from the npm registry. The service then builds the local demo index and starts the HTTP wrapper.
+
+## Usage Endpoints
+
+- `GET /healthz`: Returns `200` when CodeGraph installed, indexed the local sample, and the wrapper is ready.
+- `GET /demo`: Runs deterministic local CodeGraph checks against the bundled sample project and returns JSON with index status, files, symbol search, callers, callees, impact analysis, and a context preview.
+- `GET /v1/models`: Returns an OpenAI-style metadata list for compatibility checks. This is not an LLM model endpoint.
+- `GET /`: Same readiness payload as `/healthz`.
+
+Example:
+
+```bash
+curl -fsS https:///healthz
+curl -fsS https:///demo
+curl -fsS https:///v1/models
+```
+
+Expected `/demo` fields include:
+
+```json
+{
+ "ok": true,
+ "cpuOnly": true,
+ "remoteModelCalls": false,
+ "modelDownloaded": false,
+ "symbolSearch": {
+ "search": "Cart"
+ },
+ "contextPreview": {
+ "containsCheckoutSummary": true
+ }
+}
+```
+
+## Smoke Verification
+
+Run locally from the parent worktree to verify the template:
+
+```bash
+docker compose -f sdks/templates/prebuilt/codegraph/docker-compose.yml up -d
+curl -fsS http://localhost:8080/healthz
+curl -fsS http://localhost:8080/demo
+curl -fsS http://localhost:8080/v1/models
+docker compose -f sdks/templates/prebuilt/codegraph/docker-compose.yml down
+```
+
+Template validation commands from the parent worktree:
+
+```bash
+python sdks/templates/validate.py
+git -C sdks diff --check origin/main...HEAD
+docker compose -f sdks/templates/prebuilt/codegraph/docker-compose.yml config >/dev/null
+```
+
+## Production Notes
+
+- The HTTP server in this template is a verifier, not the upstream MCP transport. CodeGraph's agent integration is the stdio MCP server launched with `codegraph serve --mcp`.
+- The template does not configure Claude Code, Codex, Gemini, Cursor, OpenCode, AntiGravity, Kiro, or Hermes Agent. For production agent use, follow the upstream installer or MCP configuration docs.
+- To index a real private repository on Phala Cloud, adapt the image or startup command to fetch or include the repository inside the container. Do not rely on host bind mounts.
+- Add repository credentials only through Phala environment configuration, keep them as placeholders in template metadata, and avoid writing secrets into compose files or READMEs.
+- Pin `CODEGRAPH_PACKAGE_VERSION` for reproducible deployments.
+- The demo endpoints are unauthenticated. Add an authenticated reverse proxy before exposing private code intelligence APIs.
+
+## Cleanup
+
+For a local test run from the parent worktree, stop and remove the container with:
+
+```bash
+docker compose -f sdks/templates/prebuilt/codegraph/docker-compose.yml down
+```
diff --git a/templates/prebuilt/codegraph/docker-compose.yml b/templates/prebuilt/codegraph/docker-compose.yml
new file mode 100644
index 00000000..b028ca33
--- /dev/null
+++ b/templates/prebuilt/codegraph/docker-compose.yml
@@ -0,0 +1,359 @@
+services:
+ app:
+ image: node:24-bookworm-slim
+ ports:
+ - "8080:8080"
+ environment:
+ - CODEGRAPH_PACKAGE_VERSION=${CODEGRAPH_PACKAGE_VERSION:-0.9.7}
+ - CODEGRAPH_DEMO_QUERY=${CODEGRAPH_DEMO_QUERY:-cart checkout flow}
+ - NODE_ENV=production
+ - NPM_CONFIG_AUDIT=false
+ - NPM_CONFIG_FUND=false
+ - NPM_CONFIG_UPDATE_NOTIFIER=false
+ - NO_COLOR=1
+ - PORT=8080
+ command:
+ - /bin/sh
+ - -lc
+ - |
+ npm install --global --omit=dev "@colbymchenry/codegraph@$${CODEGRAPH_PACKAGE_VERSION}"
+ exec node /app/server.mjs
+ configs:
+ - source: server_mjs
+ target: /app/server.mjs
+ healthcheck:
+ test:
+ - CMD
+ - node
+ - -e
+ - fetch("http://127.0.0.1:8080/healthz").then((r) => { if (!r.ok) process.exit(1); }).catch(() => process.exit(1))
+ interval: 30s
+ timeout: 10s
+ retries: 5
+ start_period: 180s
+ restart: unless-stopped
+
+configs:
+ server_mjs:
+ content: |
+ import { execFile } from "node:child_process";
+ import { createServer } from "node:http";
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
+ import path from "node:path";
+ import process from "node:process";
+
+ const STARTED_AT = Date.now();
+ const UPSTREAM_REPO = "https://github.com/colbymchenry/codegraph";
+ const PACKAGE_NAME = "@colbymchenry/codegraph";
+ const REQUESTED_VERSION = process.env.CODEGRAPH_PACKAGE_VERSION || "0.9.7";
+ const DEMO_QUERY = process.env.CODEGRAPH_DEMO_QUERY || "cart checkout flow";
+ const PORT = Number(process.env.PORT || "8080");
+ const PROJECT_DIR = "/workspace/codegraph-demo";
+
+ let bootStatus = {
+ ok: false,
+ error: null,
+ packageVersion: null,
+ status: null,
+ bootDurationMs: null,
+ };
+
+ function projectFile(relativePath, lines) {
+ const fullPath = path.join(PROJECT_DIR, relativePath);
+ mkdirSync(path.dirname(fullPath), { recursive: true });
+ writeFileSync(fullPath, lines.join("\n") + "\n", "utf8");
+ }
+
+ function prepareDemoProject() {
+ rmSync(PROJECT_DIR, { recursive: true, force: true });
+ mkdirSync(PROJECT_DIR, { recursive: true });
+
+ projectFile("package.json", [
+ "{",
+ " \"name\": \"codegraph-phala-demo\",",
+ " \"private\": true,",
+ " \"type\": \"module\",",
+ " \"scripts\": {",
+ " \"test\": \"node --test test/checkout.test.ts\"",
+ " }",
+ "}",
+ ]);
+
+ projectFile("src/cart.ts", [
+ "export type CartItem = {",
+ " sku: string;",
+ " quantity: number;",
+ " priceCents: number;",
+ "};",
+ "",
+ "export class Cart {",
+ " private items: CartItem[] = [];",
+ "",
+ " addItem(item: CartItem) {",
+ " this.items.push(item);",
+ " }",
+ "",
+ " subtotalCents() {",
+ " return this.items.reduce((sum, item) => sum + item.priceCents * item.quantity, 0);",
+ " }",
+ "",
+ " itemCount() {",
+ " return this.items.length;",
+ " }",
+ "}",
+ ]);
+
+ projectFile("src/pricing.ts", [
+ "export function priceForSku(sku: string) {",
+ " const catalog: Record = {",
+ " hoodie: 6400,",
+ " tee: 2800,",
+ " };",
+ " return catalog[sku] || 0;",
+ "}",
+ "",
+ "export function applyTax(subtotalCents: number) {",
+ " return Math.round(subtotalCents * 1.0825);",
+ "}",
+ ]);
+
+ projectFile("src/checkout.ts", [
+ "import { Cart } from \"./cart\";",
+ "import { applyTax, priceForSku } from \"./pricing\";",
+ "",
+ "export function createCheckoutSummary() {",
+ " const cart = new Cart();",
+ " cart.addItem({ sku: \"hoodie\", quantity: 1, priceCents: priceForSku(\"hoodie\") });",
+ " cart.addItem({ sku: \"tee\", quantity: 2, priceCents: priceForSku(\"tee\") });",
+ " const subtotalCents = cart.subtotalCents();",
+ " return {",
+ " itemCount: cart.itemCount(),",
+ " subtotalCents,",
+ " totalCents: applyTax(subtotalCents),",
+ " };",
+ "}",
+ ]);
+
+ projectFile("test/checkout.test.ts", [
+ "import assert from \"node:assert/strict\";",
+ "import { createCheckoutSummary } from \"../src/checkout\";",
+ "",
+ "const summary = createCheckoutSummary();",
+ "assert.equal(summary.itemCount, 2);",
+ "assert.equal(summary.subtotalCents, 12000);",
+ ]);
+ }
+
+ function runCodeGraph(args, options = {}) {
+ return new Promise((resolve, reject) => {
+ execFile("codegraph", args, {
+ cwd: options.cwd || PROJECT_DIR,
+ timeout: options.timeout || 30000,
+ maxBuffer: 10 * 1024 * 1024,
+ env: {
+ ...process.env,
+ CI: "1",
+ CODEGRAPH_NO_DAEMON: "1",
+ CODEGRAPH_NO_WATCH: "1",
+ NO_COLOR: "1",
+ TERM: "dumb",
+ },
+ }, (error, stdout, stderr) => {
+ const result = {
+ command: ["codegraph", ...args].join(" "),
+ stdout: stdout.trim(),
+ stderr: stderr.trim(),
+ };
+
+ if (error) {
+ const message = result.stderr || error.message || "command failed";
+ const wrapped = new Error(result.command + " failed: " + message);
+ wrapped.result = result;
+ reject(wrapped);
+ return;
+ }
+
+ resolve(result);
+ });
+ });
+ }
+
+ async function runJson(args) {
+ const result = await runCodeGraph(args);
+ try {
+ return JSON.parse(result.stdout);
+ } catch (error) {
+ throw new Error("Failed to parse JSON from " + result.command + ": " + error.message);
+ }
+ }
+
+ async function bootstrap() {
+ const started = Date.now();
+ try {
+ prepareDemoProject();
+ const version = await runCodeGraph(["--version"], { cwd: "/workspace" });
+ await runCodeGraph(["init", "-i"], { timeout: 90000 });
+ const status = await runJson(["status", "--json"]);
+ bootStatus = {
+ ok: true,
+ error: null,
+ packageVersion: version.stdout,
+ status,
+ bootDurationMs: Date.now() - started,
+ };
+ } catch (error) {
+ bootStatus = {
+ ok: false,
+ error: error.message,
+ packageVersion: null,
+ status: null,
+ bootDurationMs: Date.now() - started,
+ };
+ console.error(error);
+ }
+ }
+
+ function basePayload() {
+ return {
+ service: "codegraph-local-verifier",
+ upstream: UPSTREAM_REPO,
+ package: PACKAGE_NAME,
+ requestedVersion: REQUESTED_VERSION,
+ packageVersion: bootStatus.packageVersion,
+ projectPath: PROJECT_DIR,
+ uptimeSeconds: Math.round((Date.now() - STARTED_AT) / 10) / 100,
+ credentialsRequired: false,
+ cpuOnly: true,
+ remoteModelCalls: false,
+ modelDownloaded: false,
+ };
+ }
+
+ function writeJson(response, statusCode, payload) {
+ const body = JSON.stringify(payload, null, 2) + "\n";
+ response.writeHead(statusCode, {
+ "Content-Type": "application/json; charset=utf-8",
+ "Content-Length": Buffer.byteLength(body),
+ "Cache-Control": "no-store",
+ "X-Content-Type-Options": "nosniff",
+ });
+ response.end(body);
+ }
+
+ function notReady(response) {
+ writeJson(response, 500, {
+ ...basePayload(),
+ ok: false,
+ status: "boot_failed",
+ error: bootStatus.error,
+ bootDurationMs: bootStatus.bootDurationMs,
+ });
+ }
+
+ async function handleDemo(response) {
+ if (!bootStatus.ok) {
+ notReady(response);
+ return;
+ }
+
+ const status = await runJson(["status", "--json"]);
+ const files = await runJson(["files", "--json"]);
+ const query = await runJson(["query", "Cart", "--json"]);
+ const callers = await runJson(["callers", "addItem", "--json"]);
+ const callees = await runJson(["callees", "createCheckoutSummary", "--json"]);
+ const impact = await runJson(["impact", "priceForSku", "--json"]);
+ const context = await runCodeGraph(["context", DEMO_QUERY, "--max-nodes", "5"]);
+
+ writeJson(response, 200, {
+ ...basePayload(),
+ ok: true,
+ check: "CodeGraph CLI indexes and queries a local TypeScript project",
+ demoQuery: DEMO_QUERY,
+ status,
+ files,
+ symbolSearch: {
+ search: "Cart",
+ resultCount: Array.isArray(query) ? query.length : 0,
+ topResult: Array.isArray(query) && query.length > 0 ? query[0] : null,
+ },
+ callers,
+ callees,
+ impact,
+ contextPreview: {
+ characters: context.stdout.length,
+ containsCheckoutSummary: context.stdout.includes("createCheckoutSummary"),
+ markdown: context.stdout.slice(0, 1600),
+ },
+ });
+ }
+
+ function handleModels(response) {
+ writeJson(response, 200, {
+ object: "list",
+ data: [
+ {
+ id: "codegraph-local-knowledge-graph",
+ object: "model",
+ created: Math.floor(STARTED_AT / 1000),
+ owned_by: "colbymchenry/codegraph",
+ capabilities: [
+ "local-code-index",
+ "symbol-search",
+ "call-graph",
+ "impact-analysis",
+ "mcp-server",
+ ],
+ },
+ ],
+ credentialsRequired: false,
+ remoteModelCalls: false,
+ });
+ }
+
+ async function route(request, response) {
+ const url = new URL(request.url || "/", "http://127.0.0.1");
+
+ if (request.method !== "GET") {
+ writeJson(response, 405, { ok: false, error: "method_not_allowed" });
+ return;
+ }
+
+ if (url.pathname === "/" || url.pathname === "/healthz") {
+ writeJson(response, bootStatus.ok ? 200 : 500, {
+ ...basePayload(),
+ ok: bootStatus.ok,
+ status: bootStatus.ok ? "ready" : "boot_failed",
+ bootDurationMs: bootStatus.bootDurationMs,
+ codegraphStatus: bootStatus.status,
+ error: bootStatus.error,
+ });
+ return;
+ }
+
+ if (url.pathname === "/demo") {
+ await handleDemo(response);
+ return;
+ }
+
+ if (url.pathname === "/v1/models") {
+ handleModels(response);
+ return;
+ }
+
+ writeJson(response, 404, { ok: false, error: "not_found" });
+ }
+
+ await bootstrap();
+
+ createServer((request, response) => {
+ route(request, response).catch((error) => {
+ console.error(error);
+ writeJson(response, 500, {
+ ...basePayload(),
+ ok: false,
+ error: error.message,
+ });
+ });
+ }).listen(PORT, "0.0.0.0", () => {
+ console.log("CodeGraph verifier listening on port " + PORT);
+ });