@palettelab/cli 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +11 -7
  2. package/bin/{palette.js → pltt.js} +1 -1
  3. package/lib/bundler.js +73 -4
  4. package/lib/cli.js +37 -12
  5. package/lib/commands/build.js +2 -0
  6. package/lib/commands/dev.js +37 -2
  7. package/lib/commands/doctor.js +143 -0
  8. package/lib/commands/init.js +45 -13
  9. package/lib/commands/logs.js +99 -0
  10. package/lib/commands/package.js +64 -0
  11. package/lib/commands/publish.js +50 -6
  12. package/lib/commands/status.js +80 -0
  13. package/lib/commands/test.js +376 -0
  14. package/lib/environments.js +1 -1
  15. package/lib/manifest.js +253 -8
  16. package/package.json +7 -6
  17. package/platform-dev/docker-compose.yml +4 -1
  18. package/template-fallback/backend/api/main.py +9 -3
  19. package/template-fallback/palette-plugin.json +24 -1
  20. package/template-fallback/pyproject.toml +1 -1
  21. package/template-fallback/templates/agent-tool/README.md +4 -0
  22. package/template-fallback/templates/agent-tool/backend/api/main.py +14 -0
  23. package/template-fallback/templates/agent-tool/backend/tools/echo.py +15 -0
  24. package/template-fallback/templates/agent-tool/package.json +5 -0
  25. package/template-fallback/templates/agent-tool/palette-plugin.json +29 -0
  26. package/template-fallback/templates/agent-tool/pyproject.toml +5 -0
  27. package/template-fallback/templates/dashboard/README.md +3 -0
  28. package/template-fallback/templates/dashboard/backend/api/main.py +23 -0
  29. package/template-fallback/templates/dashboard/frontend/src/index.tsx +46 -0
  30. package/template-fallback/templates/dashboard/package.json +9 -0
  31. package/template-fallback/templates/dashboard/palette-plugin.json +26 -0
  32. package/template-fallback/templates/dashboard/pyproject.toml +5 -0
  33. package/template-fallback/templates/database/README.md +7 -0
  34. package/template-fallback/templates/database/backend/api/main.py +38 -0
  35. package/template-fallback/templates/database/backend/api/models.py +11 -0
  36. package/template-fallback/templates/database/backend/migrations/001_init.py +26 -0
  37. package/template-fallback/templates/database/frontend/src/index.tsx +57 -0
  38. package/template-fallback/templates/database/package.json +6 -0
  39. package/template-fallback/templates/database/palette-plugin.json +26 -0
  40. package/template-fallback/templates/database/pyproject.toml +5 -0
  41. package/template-fallback/templates/external-service/README.md +4 -0
  42. package/template-fallback/templates/external-service/backend/api/main.py +28 -0
  43. package/template-fallback/templates/external-service/frontend/src/index.tsx +26 -0
  44. package/template-fallback/templates/external-service/package.json +6 -0
  45. package/template-fallback/templates/external-service/palette-plugin.json +26 -0
  46. package/template-fallback/templates/external-service/pyproject.toml +5 -0
  47. package/template-fallback/templates/frontend-only/README.md +7 -0
  48. package/template-fallback/templates/frontend-only/frontend/src/index.tsx +16 -0
  49. package/template-fallback/templates/frontend-only/package.json +9 -0
  50. package/template-fallback/templates/frontend-only/palette-plugin.json +25 -0
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "@palettelab/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Developer CLI for building Palette platform plugins — no platform source access required.",
5
5
  "bin": {
6
- "palette": "./bin/palette.js"
6
+ "pltt": "bin/pltt.js"
7
7
  },
8
8
  "files": [
9
9
  "bin",
10
10
  "lib",
11
11
  "platform-dev",
12
12
  "template-fallback",
13
+ "!template-fallback/**/__pycache__",
14
+ "!template-fallback/**/*.pyc",
13
15
  "palette.config.example.json",
14
16
  "README.md"
15
17
  ],
@@ -20,17 +22,16 @@
20
22
  "esbuild": "^0.24.0"
21
23
  },
22
24
  "publishConfig": {
23
- "registry": "https://registry.npmjs.org/",
24
- "access": "public"
25
+ "registry": "https://npm.pkg.github.com"
25
26
  },
26
27
  "repository": {
27
28
  "type": "git",
28
- "url": "https://github.com/palette-lab/virtual-organisation.git",
29
+ "url": "git+https://github.com/palette-lab/virtual-organisation.git",
29
30
  "directory": "sdk/cli-npm"
30
31
  },
31
32
  "license": "MIT",
32
33
  "keywords": [
33
- "palette",
34
+ "pltt",
34
35
  "plugin",
35
36
  "sdk",
36
37
  "cli"
@@ -1,4 +1,4 @@
1
- # Bundled docker-compose for `palette dev`. The CLI references this file
1
+ # Bundled docker-compose for `pltt dev`. The CLI references this file
2
2
  # from the installed npm package. Third-party developers never edit it.
3
3
  #
4
4
  # Usage (handled by @palettelab/cli):
@@ -20,6 +20,9 @@ services:
20
20
  REDIS_URL: "redis://redis:6379/0"
21
21
  JWT_SECRET: "dev-secret-do-not-use-in-prod"
22
22
  FRONTEND_URL: "http://localhost:${PALETTE_FRONTEND_PORT:-3000}"
23
+ BACKEND_BASE_URL: "http://localhost:${PALETTE_BACKEND_PORT:-8000}"
24
+ STORAGE_BACKEND: "local"
25
+ LOCAL_STORAGE_DIR: "/srv/storage"
23
26
  # Disable optional features that need real credentials
24
27
  RAG_ENABLED: "false"
25
28
  OPENAI_API_KEY: ""
@@ -18,12 +18,18 @@ The PluginContext provides:
18
18
 
19
19
  from fastapi import Depends
20
20
 
21
- from palette_sdk import PluginRouter, PluginContext, get_plugin_context, SuccessResponse
21
+ from palette_sdk import (
22
+ PluginRouter,
23
+ PluginContext,
24
+ SuccessResponse,
25
+ get_plugin_context,
26
+ require_permission,
27
+ )
22
28
 
23
29
  router = PluginRouter(tags=["my-plugin"])
24
30
 
25
31
 
26
- @router.get("/status")
32
+ @router.get("/status", dependencies=[require_permission("tasks:read")])
27
33
  async def get_status(ctx: PluginContext = Depends(get_plugin_context)):
28
34
  """Example endpoint — returns plugin status."""
29
35
  return SuccessResponse(
@@ -36,7 +42,7 @@ async def get_status(ctx: PluginContext = Depends(get_plugin_context)):
36
42
  )
37
43
 
38
44
 
39
- @router.get("/hello")
45
+ @router.get("/hello", dependencies=[require_permission("tasks:read")])
40
46
  async def hello(ctx: PluginContext = Depends(get_plugin_context)):
41
47
  """Example endpoint — simple hello."""
42
48
  return {"message": f"Hello from plugin! User: {ctx.user_id}"}
@@ -1,4 +1,5 @@
1
1
  {
2
+ "manifest_version": "1",
2
3
  "id": "my-plugin",
3
4
  "name": "My Plugin",
4
5
  "version": "1.0.0",
@@ -11,8 +12,30 @@
11
12
  "bg": "linear-gradient(135deg, #6366F1, #8B5CF6)",
12
13
  "text": "#fff"
13
14
  },
15
+ "sdk": {
16
+ "frontend": "^0.1.0",
17
+ "backend": "^0.1.0"
18
+ },
19
+ "platform": {
20
+ "min_version": "0.1.0"
21
+ },
22
+ "capabilities": {
23
+ "frontend": true,
24
+ "backend": true,
25
+ "database": false,
26
+ "webhooks": false,
27
+ "scheduled_jobs": false,
28
+ "file_uploads": false,
29
+ "external_network": []
30
+ },
31
+ "public_routes": [],
32
+ "scheduled_jobs": [],
33
+ "rate_limit": {
34
+ "per_minute": 60
35
+ },
14
36
  "frontend": {
15
- "entry": "./frontend/src/index.tsx"
37
+ "entry": "./frontend/src/index.tsx",
38
+ "sandbox": true
16
39
  },
17
40
  "backend": {
18
41
  "entry": "./backend/api/main.py"
@@ -5,7 +5,7 @@ description = "A Palette platform plugin"
5
5
  requires-python = ">=3.12"
6
6
  # palette-sdk is installed from the private platform repo. The
7
7
  # platform-dev container already has it on PYTHONPATH, so you do not
8
- # need to install it locally to run `palette dev`. If you want to run
8
+ # need to install it locally to run `pltt dev`. If you want to run
9
9
  # `pytest` on the backend outside the container, add this dependency
10
10
  # (replace $GH_PAT with your GitHub PAT with read:packages):
11
11
  #
@@ -0,0 +1,4 @@
1
+ # Agent-tool plugin
2
+
3
+ Defines a callable tool (`echo`) under `backend/tools/`. Any agent in the org
4
+ can invoke it once installed.
@@ -0,0 +1,14 @@
1
+ """Agent-tool plugin backend.
2
+
3
+ This plugin's primary surface is the tool defined in backend/tools/echo.py.
4
+ The router exists so superadmin health checks have something to ping.
5
+ """
6
+
7
+ from palette_sdk import PluginRouter
8
+
9
+ router = PluginRouter(tags=["my-agent-tool"])
10
+
11
+
12
+ @router.get("/health")
13
+ async def health() -> dict:
14
+ return {"status": "ok"}
@@ -0,0 +1,15 @@
1
+ """Echo tool — agents can call this to echo a message back."""
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class EchoInput(BaseModel):
7
+ message: str
8
+
9
+
10
+ class EchoOutput(BaseModel):
11
+ echoed: str
12
+
13
+
14
+ async def run(inp: EchoInput) -> EchoOutput:
15
+ return EchoOutput(echoed=inp.message)
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "my-agent-tool",
3
+ "version": "1.0.0",
4
+ "private": true
5
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "manifest_version": "1",
3
+ "id": "my-agent-tool",
4
+ "name": "My Agent Tool",
5
+ "version": "1.0.0",
6
+ "developer": "Your Team",
7
+ "category": "Agents",
8
+ "tagline": "A tool an agent can call",
9
+ "description": "Registers a callable tool that any agent in the org can invoke.",
10
+ "icon": "Toolbox",
11
+ "gradient": { "bg": "linear-gradient(135deg, #F59E0B, #EF4444)", "text": "#fff" },
12
+ "sdk": { "backend": "^0.1.0" },
13
+ "platform": { "min_version": "0.1.0" },
14
+ "capabilities": {
15
+ "frontend": false,
16
+ "backend": true,
17
+ "database": false,
18
+ "webhooks": false,
19
+ "scheduled_jobs": false,
20
+ "file_uploads": false,
21
+ "external_network": []
22
+ },
23
+ "backend": { "entry": "./backend/api/main.py" },
24
+ "public_routes": ["/health"],
25
+ "tools": [
26
+ { "name": "echo", "description": "Echo a string back", "entry": "./backend/tools/echo.py" }
27
+ ],
28
+ "permissions": ["tasks:read", "tasks:write"]
29
+ }
@@ -0,0 +1,5 @@
1
+ [project]
2
+ name = "my-agent-tool"
3
+ version = "1.0.0"
4
+ requires-python = ">=3.12"
5
+ dependencies = ["palette-sdk", "pydantic"]
@@ -0,0 +1,3 @@
1
+ # Dashboard widget plugin
2
+
3
+ Exposes a backend `/series` endpoint and renders the data as a bar chart.
@@ -0,0 +1,23 @@
1
+ """Dashboard widget backend.
2
+
3
+ Exposes a single data-source endpoint that the widget renders. Real plugins
4
+ would source this from the org's data rooms or installed data sources.
5
+ """
6
+
7
+ from fastapi import Depends
8
+
9
+ from palette_sdk import PluginRouter, PluginContext, get_plugin_context, require_permission
10
+
11
+ router = PluginRouter(tags=["my-dashboard"])
12
+
13
+
14
+ @router.get("/series", dependencies=[require_permission("data_rooms:read")])
15
+ async def series(ctx: PluginContext = Depends(get_plugin_context)) -> list[dict]:
16
+ """Stub data source — replace with a query against the org's tables."""
17
+ return [
18
+ {"label": "Mon", "value": 12},
19
+ {"label": "Tue", "value": 19},
20
+ {"label": "Wed", "value": 8},
21
+ {"label": "Thu", "value": 22},
22
+ {"label": "Fri", "value": 17},
23
+ ]
@@ -0,0 +1,46 @@
1
+ "use client"
2
+
3
+ import { useEffect, useState } from "react"
4
+ import { usePlatform } from "@palettelab/sdk"
5
+ import type { PluginComponentProps } from "@palettelab/sdk"
6
+
7
+ type Series = { label: string; value: number }
8
+
9
+ export default function DashboardWidget(_props: PluginComponentProps) {
10
+ const { apiFetch } = usePlatform()
11
+ const [data, setData] = useState<Series[]>([])
12
+ const [loading, setLoading] = useState(true)
13
+
14
+ useEffect(() => {
15
+ apiFetch("/api/v1/plugins/my-dashboard/series")
16
+ .then((r) => r.json())
17
+ .then(setData)
18
+ .finally(() => setLoading(false))
19
+ }, [apiFetch])
20
+
21
+ const max = Math.max(1, ...data.map((d) => d.value))
22
+
23
+ return (
24
+ <div className="p-6 space-y-4">
25
+ <h1 className="text-2xl font-bold">Dashboard Widget</h1>
26
+ {loading ? (
27
+ <p className="text-muted-foreground">Loading…</p>
28
+ ) : (
29
+ <div className="space-y-2">
30
+ {data.map((d) => (
31
+ <div key={d.label} className="flex items-center gap-3">
32
+ <span className="w-24 text-sm">{d.label}</span>
33
+ <div className="flex-1 h-3 rounded bg-muted">
34
+ <div
35
+ className="h-3 rounded bg-primary"
36
+ style={{ width: `${(d.value / max) * 100}%` }}
37
+ />
38
+ </div>
39
+ <span className="text-sm tabular-nums">{d.value}</span>
40
+ </div>
41
+ ))}
42
+ </div>
43
+ )}
44
+ </div>
45
+ )
46
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "my-dashboard",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "dependencies": {
6
+ "@palettelab/sdk": "^0.1.0",
7
+ "react": "^19.0.0"
8
+ }
9
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "manifest_version": "1",
3
+ "id": "my-dashboard",
4
+ "name": "My Dashboard Widget",
5
+ "version": "1.0.0",
6
+ "developer": "Your Team",
7
+ "category": "Analytics",
8
+ "tagline": "A dashboard widget",
9
+ "description": "A widget that exposes a dashboard data source and renders a chart from it.",
10
+ "icon": "ChartBar",
11
+ "gradient": { "bg": "linear-gradient(135deg, #06B6D4, #6366F1)", "text": "#fff" },
12
+ "sdk": { "frontend": "^0.1.0", "backend": "^0.1.0" },
13
+ "platform": { "min_version": "0.1.0" },
14
+ "capabilities": {
15
+ "frontend": true,
16
+ "backend": true,
17
+ "database": false,
18
+ "webhooks": false,
19
+ "scheduled_jobs": false,
20
+ "file_uploads": false,
21
+ "external_network": []
22
+ },
23
+ "frontend": { "entry": "./frontend/src/index.tsx", "sandbox": true },
24
+ "backend": { "entry": "./backend/api/main.py" },
25
+ "permissions": ["tasks:read", "data_rooms:read"]
26
+ }
@@ -0,0 +1,5 @@
1
+ [project]
2
+ name = "my-dashboard"
3
+ version = "1.0.0"
4
+ requires-python = ">=3.12"
5
+ dependencies = ["palette-sdk"]
@@ -0,0 +1,7 @@
1
+ # Database plugin
2
+
3
+ Org-isolated notes table with row-level security.
4
+
5
+ ```bash
6
+ pltt dev # applies migrations on container start
7
+ ```
@@ -0,0 +1,38 @@
1
+ """Database plugin backend — org-scoped notes."""
2
+
3
+ from fastapi import Depends, HTTPException
4
+ from pydantic import BaseModel
5
+ from sqlalchemy import select
6
+
7
+ from palette_sdk import PluginRouter, PluginContext, get_plugin_context, require_permission
8
+ from models import Note
9
+
10
+ router = PluginRouter(tags=["my-db-plugin"])
11
+
12
+
13
+ class NoteIn(BaseModel):
14
+ body: str
15
+
16
+
17
+ @router.get("/notes", dependencies=[require_permission("resources:read")])
18
+ async def list_notes(ctx: PluginContext = Depends(get_plugin_context)) -> list[dict]:
19
+ rows = (
20
+ await ctx.db.execute(
21
+ select(Note).where(Note.organization_id == ctx.organization_id).order_by(Note.id.desc())
22
+ )
23
+ ).scalars().all()
24
+ return [{"id": r.id, "body": r.body} for r in rows]
25
+
26
+
27
+ @router.post("/notes", dependencies=[require_permission("resources:write")])
28
+ async def create_note(
29
+ body: NoteIn,
30
+ ctx: PluginContext = Depends(get_plugin_context),
31
+ ) -> dict:
32
+ if not body.body.strip():
33
+ raise HTTPException(status_code=400, detail="body required")
34
+ note = Note(organization_id=ctx.organization_id, body=body.body)
35
+ ctx.db.add(note)
36
+ await ctx.db.commit()
37
+ await ctx.db.refresh(note)
38
+ return {"id": note.id, "body": note.body}
@@ -0,0 +1,11 @@
1
+ from sqlalchemy import Integer, Text
2
+ from sqlalchemy.orm import Mapped, mapped_column
3
+
4
+ from palette_sdk.db import OrgScopedTable
5
+
6
+
7
+ class Note(OrgScopedTable):
8
+ __tablename__ = "notes"
9
+
10
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
11
+ body: Mapped[str] = mapped_column(Text, nullable=False)
@@ -0,0 +1,26 @@
1
+ """Initial migration — creates notes table with org RLS."""
2
+
3
+ from alembic import op
4
+ import sqlalchemy as sa
5
+ from sqlalchemy.dialects import postgresql
6
+
7
+ from palette_sdk.db import ensure_org_rls
8
+
9
+ revision = "001_init"
10
+ down_revision = None
11
+
12
+
13
+ def upgrade() -> None:
14
+ op.create_table(
15
+ "notes",
16
+ sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
17
+ sa.Column("organization_id", postgresql.UUID(as_uuid=True), nullable=False),
18
+ sa.Column("body", sa.Text(), nullable=False),
19
+ )
20
+ op.create_index("ix_notes_organization_id", "notes", ["organization_id"])
21
+ ensure_org_rls("notes")
22
+
23
+
24
+ def downgrade() -> None:
25
+ op.drop_index("ix_notes_organization_id", table_name="notes")
26
+ op.drop_table("notes")
@@ -0,0 +1,57 @@
1
+ "use client"
2
+
3
+ import { useEffect, useState } from "react"
4
+ import { usePlatform } from "@palettelab/sdk"
5
+ import type { PluginComponentProps } from "@palettelab/sdk"
6
+
7
+ type Note = { id: number; body: string }
8
+
9
+ export default function NotesApp(_props: PluginComponentProps) {
10
+ const { apiFetch, showToast } = usePlatform()
11
+ const [notes, setNotes] = useState<Note[]>([])
12
+ const [body, setBody] = useState("")
13
+
14
+ async function load() {
15
+ const r = await apiFetch("/api/v1/plugins/my-db-plugin/notes")
16
+ setNotes(await r.json())
17
+ }
18
+ useEffect(() => {
19
+ load()
20
+ }, [])
21
+
22
+ async function add() {
23
+ if (!body.trim()) return
24
+ const r = await apiFetch("/api/v1/plugins/my-db-plugin/notes", {
25
+ method: "POST",
26
+ headers: { "Content-Type": "application/json" },
27
+ body: JSON.stringify({ body }),
28
+ })
29
+ if (r.ok) {
30
+ setBody("")
31
+ showToast("Note saved", "success")
32
+ load()
33
+ }
34
+ }
35
+
36
+ return (
37
+ <div className="p-6 space-y-3">
38
+ <h1 className="text-2xl font-bold">Notes</h1>
39
+ <div className="flex gap-2">
40
+ <input
41
+ className="border rounded px-2 py-1 text-sm flex-1"
42
+ value={body}
43
+ onChange={(e) => setBody(e.target.value)}
44
+ placeholder="New note…"
45
+ />
46
+ <button onClick={add} className="px-3 py-1 rounded bg-primary text-primary-foreground text-sm">
47
+ Add
48
+ </button>
49
+ </div>
50
+ <ul className="space-y-1 text-sm">
51
+ {notes.map((n) => (
52
+ <li key={n.id} className="border rounded px-2 py-1">{n.body}</li>
53
+ ))}
54
+ </ul>
55
+ </div>
56
+ )
57
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "my-db-plugin",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "dependencies": { "@palettelab/sdk": "^0.1.0", "react": "^19.0.0" }
6
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "manifest_version": "1",
3
+ "id": "my-db-plugin",
4
+ "name": "My Database Plugin",
5
+ "version": "1.0.0",
6
+ "developer": "Your Team",
7
+ "category": "Productivity",
8
+ "tagline": "Org-isolated database CRUD",
9
+ "description": "Stores notes per organization with RLS-enforced isolation.",
10
+ "icon": "Database",
11
+ "gradient": { "bg": "linear-gradient(135deg, #8B5CF6, #EC4899)", "text": "#fff" },
12
+ "sdk": { "frontend": "^0.1.0", "backend": "^0.1.0" },
13
+ "platform": { "min_version": "0.1.0" },
14
+ "capabilities": {
15
+ "frontend": true,
16
+ "backend": true,
17
+ "database": true,
18
+ "webhooks": false,
19
+ "scheduled_jobs": false,
20
+ "file_uploads": false,
21
+ "external_network": []
22
+ },
23
+ "frontend": { "entry": "./frontend/src/index.tsx", "sandbox": true },
24
+ "backend": { "entry": "./backend/api/main.py" },
25
+ "permissions": ["resources:read", "resources:write"]
26
+ }
@@ -0,0 +1,5 @@
1
+ [project]
2
+ name = "my-db-plugin"
3
+ version = "1.0.0"
4
+ requires-python = ">=3.12"
5
+ dependencies = ["palette-sdk", "sqlalchemy", "alembic"]
@@ -0,0 +1,4 @@
1
+ # External-service plugin
2
+
3
+ Calls a third-party API. Notice the manifest declares `capabilities.external_network`
4
+ — the platform enforces this allowlist at runtime.
@@ -0,0 +1,28 @@
1
+ """External-service plugin backend.
2
+
3
+ Reads a per-org config token (set when the org installs the plugin) and
4
+ forwards a request to a declared external host. The host MUST be listed
5
+ in capabilities.external_network in the manifest — the platform enforces
6
+ the allowlist at runtime.
7
+ """
8
+
9
+ import os
10
+ from fastapi import Depends, HTTPException
11
+
12
+ from palette_sdk import PluginRouter, PluginContext, get_plugin_context, require_permission
13
+
14
+ router = PluginRouter(tags=["my-external-svc"])
15
+
16
+ EXTERNAL_HOST = "https://api.example.com"
17
+
18
+
19
+ @router.get("/proxy", dependencies=[require_permission("resources:read")])
20
+ async def proxy(ctx: PluginContext = Depends(get_plugin_context)) -> dict:
21
+ import httpx
22
+
23
+ token = os.environ.get("EXAMPLE_API_TOKEN")
24
+ if not token:
25
+ raise HTTPException(status_code=500, detail="EXAMPLE_API_TOKEN not configured")
26
+ async with httpx.AsyncClient() as client:
27
+ r = await client.get(f"{EXTERNAL_HOST}/v1/ping", headers={"Authorization": f"Bearer {token}"})
28
+ return {"upstream_status": r.status_code, "body": r.text[:200]}
@@ -0,0 +1,26 @@
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { usePlatform } from "@palettelab/sdk"
5
+ import type { PluginComponentProps } from "@palettelab/sdk"
6
+
7
+ export default function ExternalServiceWidget(_props: PluginComponentProps) {
8
+ const { apiFetch } = usePlatform()
9
+ const [result, setResult] = useState<string>("")
10
+
11
+ return (
12
+ <div className="p-6 space-y-3">
13
+ <h1 className="text-2xl font-bold">External Service</h1>
14
+ <button
15
+ className="px-3 py-1.5 rounded bg-primary text-primary-foreground text-sm"
16
+ onClick={async () => {
17
+ const r = await apiFetch("/api/v1/plugins/my-external-svc/proxy")
18
+ setResult(await r.text())
19
+ }}
20
+ >
21
+ Call api.example.com
22
+ </button>
23
+ {result && <pre className="text-xs">{result}</pre>}
24
+ </div>
25
+ )
26
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "my-external-svc",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "dependencies": { "@palettelab/sdk": "^0.1.0", "react": "^19.0.0" }
6
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "manifest_version": "1",
3
+ "id": "my-external-svc",
4
+ "name": "My External Service",
5
+ "version": "1.0.0",
6
+ "developer": "Your Team",
7
+ "category": "Integrations",
8
+ "tagline": "Calls a third-party API",
9
+ "description": "Demonstrates declared external_network access and a scoped per-org config token.",
10
+ "icon": "CloudArrowUp",
11
+ "gradient": { "bg": "linear-gradient(135deg, #10B981, #06B6D4)", "text": "#fff" },
12
+ "sdk": { "frontend": "^0.1.0", "backend": "^0.1.0" },
13
+ "platform": { "min_version": "0.1.0" },
14
+ "capabilities": {
15
+ "frontend": true,
16
+ "backend": true,
17
+ "database": false,
18
+ "webhooks": false,
19
+ "scheduled_jobs": false,
20
+ "file_uploads": false,
21
+ "external_network": ["api.example.com"]
22
+ },
23
+ "frontend": { "entry": "./frontend/src/index.tsx", "sandbox": true },
24
+ "backend": { "entry": "./backend/api/main.py" },
25
+ "permissions": ["resources:read"]
26
+ }
@@ -0,0 +1,5 @@
1
+ [project]
2
+ name = "my-external-svc"
3
+ version = "1.0.0"
4
+ requires-python = ">=3.12"
5
+ dependencies = ["palette-sdk", "httpx"]
@@ -0,0 +1,7 @@
1
+ # Frontend-only plugin
2
+
3
+ Pure React widget. Runs inside the platform iframe sandbox.
4
+
5
+ ```bash
6
+ pltt dev
7
+ ```
@@ -0,0 +1,16 @@
1
+ "use client"
2
+
3
+ import { usePlatform } from "@palettelab/sdk"
4
+ import type { PluginComponentProps } from "@palettelab/sdk"
5
+
6
+ export default function FrontendOnlyPlugin(_props: PluginComponentProps) {
7
+ const { user } = usePlatform()
8
+ return (
9
+ <div className="p-6">
10
+ <h1 className="text-2xl font-bold">Hello, {user.name}</h1>
11
+ <p className="text-muted-foreground mt-2">
12
+ This plugin runs entirely in the browser sandbox — no backend.
13
+ </p>
14
+ </div>
15
+ )
16
+ }