@palettelab/cli 0.3.0 → 0.3.2
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.
- package/README.md +103 -10
- package/bin/{palette.js → pltt.js} +1 -1
- package/lib/bundler.js +73 -4
- package/lib/cli.js +37 -12
- package/lib/commands/build.js +2 -0
- package/lib/commands/dev.js +37 -2
- package/lib/commands/doctor.js +143 -0
- package/lib/commands/init.js +45 -13
- package/lib/commands/logs.js +99 -0
- package/lib/commands/package.js +64 -0
- package/lib/commands/publish.js +50 -6
- package/lib/commands/status.js +80 -0
- package/lib/commands/test.js +376 -0
- package/lib/environments.js +1 -1
- package/lib/manifest.js +253 -8
- package/package.json +7 -6
- package/platform-dev/docker-compose.yml +4 -1
- package/template-fallback/backend/api/main.py +9 -3
- package/template-fallback/palette-plugin.json +24 -1
- package/template-fallback/pyproject.toml +1 -1
- package/template-fallback/templates/agent-tool/README.md +4 -0
- package/template-fallback/templates/agent-tool/backend/api/main.py +14 -0
- package/template-fallback/templates/agent-tool/backend/tools/echo.py +15 -0
- package/template-fallback/templates/agent-tool/package.json +5 -0
- package/template-fallback/templates/agent-tool/palette-plugin.json +29 -0
- package/template-fallback/templates/agent-tool/pyproject.toml +5 -0
- package/template-fallback/templates/dashboard/README.md +3 -0
- package/template-fallback/templates/dashboard/backend/api/main.py +23 -0
- package/template-fallback/templates/dashboard/frontend/src/index.tsx +46 -0
- package/template-fallback/templates/dashboard/package.json +9 -0
- package/template-fallback/templates/dashboard/palette-plugin.json +26 -0
- package/template-fallback/templates/dashboard/pyproject.toml +5 -0
- package/template-fallback/templates/database/README.md +7 -0
- package/template-fallback/templates/database/backend/api/main.py +38 -0
- package/template-fallback/templates/database/backend/api/models.py +11 -0
- package/template-fallback/templates/database/backend/migrations/001_init.py +26 -0
- package/template-fallback/templates/database/frontend/src/index.tsx +57 -0
- package/template-fallback/templates/database/package.json +6 -0
- package/template-fallback/templates/database/palette-plugin.json +26 -0
- package/template-fallback/templates/database/pyproject.toml +5 -0
- package/template-fallback/templates/external-service/README.md +4 -0
- package/template-fallback/templates/external-service/backend/api/main.py +28 -0
- package/template-fallback/templates/external-service/frontend/src/index.tsx +26 -0
- package/template-fallback/templates/external-service/package.json +6 -0
- package/template-fallback/templates/external-service/palette-plugin.json +26 -0
- package/template-fallback/templates/external-service/pyproject.toml +5 -0
- package/template-fallback/templates/frontend-only/README.md +7 -0
- package/template-fallback/templates/frontend-only/frontend/src/index.tsx +16 -0
- package/template-fallback/templates/frontend-only/package.json +9 -0
- 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.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Developer CLI for building Palette platform plugins — no platform source access required.",
|
|
5
5
|
"bin": {
|
|
6
|
-
"
|
|
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://
|
|
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
|
-
"
|
|
34
|
+
"pltt",
|
|
34
35
|
"plugin",
|
|
35
36
|
"sdk",
|
|
36
37
|
"cli"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Bundled docker-compose for `
|
|
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
|
|
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 `
|
|
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,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,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,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,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,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,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,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,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,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
|
+
}
|