@meistrari/tela-skills 1.4.0 → 1.6.0

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 (37) hide show
  1. package/README.md +31 -12
  2. package/cli.ts +28 -20
  3. package/package.json +1 -1
  4. package/skill/TELA_LOCALHOST_SKILL.md +8 -6
  5. package/skill/TELA_SKILL.md +56 -28
  6. package/skill/canvas.test.ts +149 -0
  7. package/skill/canvas.ts +49 -23
  8. package/skill/common.ts +41 -22
  9. package/skill/create-canvas.md +9 -20
  10. package/skill/create-project.md +1 -3
  11. package/skill/index.ts +53 -24
  12. package/skill/localhost-utils.test.ts +98 -2
  13. package/skill/localhost-utils.ts +245 -16
  14. package/skill/setup-session.md +46 -10
  15. package/skill/test-cases.ts +21 -6
  16. package/skill/update-canvas.md +10 -20
  17. package/skill/workflow-builder-types.ts +1 -0
  18. package/skill/workflow-builders.ts +19 -2
  19. package/skill/workflow-graph-builder.test.ts +438 -0
  20. package/skill/workflow-graph-builder.ts +17 -2
  21. package/skill/workflow-module-rules.test.ts +340 -0
  22. package/skill/workflow-module-rules.ts +226 -0
  23. package/skill/workflow-node-builders.ts +60 -3
  24. package/skill/workflow-nodes.md +245 -0
  25. package/skill/workflow-references.md +140 -0
  26. package/skill/workflow-references.test.ts +137 -0
  27. package/skill/workflow-references.ts +99 -0
  28. package/skill/workflow-run-builders.ts +14 -4
  29. package/skill/workflow-spec-builders.ts +15 -112
  30. package/skill/workflow-updates.ts +168 -0
  31. package/skill/workflow.md +176 -167
  32. package/skill/workflows.test.ts +809 -0
  33. package/skill/workflows.ts +746 -147
  34. package/skill/workflow-nodes-v2-v3.md +0 -746
  35. package/skill/workflow-references-v2-v3.md +0 -169
  36. package/skill/workflow-v2-create-update.md +0 -54
  37. package/skill/workflow-v3-create-update.md +0 -65
package/README.md CHANGED
@@ -42,27 +42,33 @@ The CLI (`cli.ts`) is an interactive installer that sets up Tela Skills for Clau
42
42
 
43
43
  ### What it does
44
44
 
45
- 1. **Environment selection** — Choose between Production (`api.tela.com`) or Staging (`staging.api.tela.com`).
45
+ 1. **Environment selection** — Choose between Production (`api.tela.com`), Staging (`api.telastaging.com`), or Localhost (Docker-based local development).
46
46
  2. **Skill installation** — Copies skill source files from `skill/` to `~/.claude/skills/tela/`.
47
47
  3. **Agent deployment** — Copies agent definitions from `agents/` to `~/.claude/agents/` (if available).
48
48
  4. **Dependency installation** — Installs `@meistrari/vault-sdk` into the skills directory via Bun.
49
- 5. **Authentication** — Browser auth via a local callback after redirecting to the Tela backoffice.
50
- 6. **Session persistence** — Saves credentials to `~/.tela/session.production` or `~/.tela/session.staging` and environment config to `~/.tela/environment` and `~/.tela/.env`.
49
+ 5. **Authentication** — Browser auth for Production/Staging; Docker discovery + JWT generation for Localhost.
50
+ 6. **Session persistence** — Saves credentials to `~/.tela/session.production`, `~/.tela/session.staging`, or `~/.tela/session.local` and environment config to `~/.tela/environment` and `~/.tela/.env`.
51
51
 
52
52
  ### Usage
53
53
 
54
54
  ```bash
55
- # Install and configure
55
+ # Install and configure (interactive)
56
56
  bunx @meistrari/tela-skills
57
57
 
58
+ # Non-interactive localhost setup
59
+ bunx @meistrari/tela-skills --env localhost --repos-path /path/to/tela --workspace <slug-or-id> --yes
60
+
58
61
  # Test locally during development
59
62
  bun run cli.ts
60
63
 
61
64
  # Verify skill loads correctly
62
65
  bun --preload skill/preload.ts -e "console.log(Object.keys(tela))"
63
66
 
64
- # Localhost (API :4000, App :3000)
67
+ # Run commands against localhost
65
68
  bun localhost.ts "console.log(await tela.listProjects())"
69
+
70
+ # List available localhost workspaces
71
+ bun list-workspaces.ts
66
72
  ```
67
73
 
68
74
  ### File layout after installation
@@ -75,10 +81,22 @@ bun localhost.ts "console.log(await tela.listProjects())"
75
81
  ~/.tela/
76
82
  session.production # API token (production)
77
83
  session.staging # API token (staging)
78
- environment # "production" or "staging"
84
+ session.local # JWT token (localhost, 30-day expiry)
85
+ environment # "production", "staging", or "localhost"
79
86
  .env # TELA_API_URL, TELA_APP_URL
87
+ tela-repo-path # Path to tela monorepo (localhost only, for key discovery)
80
88
  ```
81
89
 
90
+ ### Localhost environment
91
+
92
+ For internal development against locally running Tela services:
93
+
94
+ 1. Start Docker services in the tela monorepo (`make dev`)
95
+ 2. Run `bunx @meistrari/tela-skills` and select "Localhost"
96
+ 3. The CLI discovers Docker ports, reads `AUTH_API_SECRET` from `packages/api/.env`, loads auth settings from `.repositories/auth-api/.env`, fetches workspaces, and generates a JWT through the local auth-api JWKS signing flow
97
+
98
+ The localhost JWT has a 30-day expiry and is auto-regenerated when expired. Workspace selection is required during setup — use `bun list-workspaces.ts` to see available workspaces.
99
+
82
100
  ---
83
101
 
84
102
  ## Skills
@@ -100,14 +118,14 @@ bun --preload ~/.claude/skills/tela/preload.ts -e "
100
118
 
101
119
  **File:** `skill/canvas.ts`
102
120
 
103
- Canvas is Tela's core concept — a versioned AI prompt with messages, variables, configuration, and optional structured output.
121
+ Canvas is Tela's core concept — a versioned AI prompt with a message, variables, configuration, and optional structured output.
104
122
 
105
123
  #### Types
106
124
 
107
125
  | Type | Description |
108
126
  |------|-------------|
109
127
  | `Canvas` | Top-level prompt object with metadata |
110
- | `CanvasVersion` | Versioned snapshot: messages, variables, config |
128
+ | `CanvasVersion` | Versioned snapshot: message, variables, config |
111
129
  | `Variable` | Input/output definition (text, file, or mixed) |
112
130
  | `StructuredOutput` | JSON schema for enforcing response format |
113
131
  | `CanvasMessage` | Chat message (system, user, assistant, function) |
@@ -122,7 +140,7 @@ tela.createCanvas({
122
140
  title: "My Prompt",
123
141
  projectId: "proj-uuid",
124
142
  version: {
125
- messages: [tela.createMessage("system", "You are a helpful assistant.")],
143
+ message: tela.createMessage("system", "You are a helpful assistant."),
126
144
  variables: [tela.createVariable("input", "text", { variableType: "input" })],
127
145
  configuration: { model: "gpt-5", temperature: 0 }
128
146
  }
@@ -294,7 +312,7 @@ Upload, download, and manage files using immutable `vault://` references.
294
312
 
295
313
  | Function | Description |
296
314
  |----------|-------------|
297
- | `loadApiKey()` | Reads token from `~/.tela/session.production` or `~/.tela/session.staging` |
315
+ | `loadApiKey()` | Reads token from `~/.tela/session.production`, `~/.tela/session.staging`, or `~/.tela/session.local` |
298
316
  | `getApiBaseUrl()` | Returns API URL from globals or defaults |
299
317
  | `getAppBaseUrl()` | Returns app URL for constructing viewer links |
300
318
  | `apiRequest<T>(method, path, body?)` | Generic HTTP client with Bearer auth |
@@ -375,7 +393,8 @@ Runtime:
375
393
  ├─→ prompts.ts (list/get prompt metadata)
376
394
  ├─→ test-cases.ts (create/run/manage evaluations)
377
395
  ├─→ tasks.ts (human-in-the-loop validation)
378
- └─→ vault.ts (file upload/download/manage)
396
+ ├─→ vault.ts (file upload/download/manage)
397
+ └─→ localhost-utils.ts (Docker discovery, JWT signing, workspace fetching)
379
398
  ```
380
399
 
381
400
  ---
@@ -388,7 +407,7 @@ Every message and variable has both **markdown** and **HTML** representations. T
388
407
 
389
408
  ### Vault references
390
409
 
391
- All files are referenced by immutable `vault://UUID` identifiers — never local paths. Upload files to Vault first, then use the returned reference in canvas messages, test cases, or task inputs.
410
+ All files are referenced by immutable `vault://UUID` identifiers — never local paths. Upload files to Vault first, then use the returned reference in a canvas message, test cases, or task inputs.
392
411
 
393
412
  ### Smart merging
394
413
 
package/cli.ts CHANGED
@@ -7,10 +7,9 @@ import { fileURLToPath } from 'node:url'
7
7
  import open from 'open'
8
8
  import {
9
9
  discoverLocalhostServices,
10
- parseApiGatewayKeys,
11
10
  readAuthApiSecret,
12
- signLocalhostJwt,
13
11
  fetchWorkspaces,
12
+ generateLocalhostJwt,
14
13
  saveLocalhostSession,
15
14
  writeLocalhostEnv,
16
15
  readSavedReposPath,
@@ -23,7 +22,7 @@ const OPEN_ISSUE_SKILLS_DIR = join(homedir(), '.claude', 'skills', 'tela-open-is
23
22
  const LOCALHOST_SKILLS_DIR = join(homedir(), '.claude', 'skills', 'tela-localhost')
24
23
  const SESSION_DIR = join(homedir(), '.tela')
25
24
  const BACKOFFICE_URL = 'https://backoffice.tela.com'
26
- const STAGING_BACKOFFICE_URL = 'https://staging-backoffice.tela.com'
25
+ const STAGING_BACKOFFICE_URL = 'https://backoffice.telastaging.com'
27
26
 
28
27
  // --- CLI Flag Parsing ---
29
28
  function parseFlags(argv: string[]): Record<string, string | true> {
@@ -126,8 +125,13 @@ async function handleLocalhostSetup() {
126
125
  p.log.error('Could not find tela-api Docker container. Make sure local services are running.')
127
126
  process.exit(1)
128
127
  }
128
+ if (!services.authApiDbPort) {
129
+ discoverSpinner.stop('auth-postgres container not found')
130
+ p.log.error('Could not find auth-postgres Docker container. Make sure local auth services are running.')
131
+ process.exit(1)
132
+ }
129
133
 
130
- discoverSpinner.stop(`Found services — auth-api :${services.authApiPort}, tela-api :${services.telaApiPort}${services.telaAppPort ? `, tela-app :${services.telaAppPort}` : ''}`)
134
+ discoverSpinner.stop(`Found services — auth-api :${services.authApiPort}, auth-postgres :${services.authApiDbPort}, tela-api :${services.telaApiPort}${services.telaAppPort ? `, tela-app :${services.telaAppPort}` : ''}`)
131
135
 
132
136
  // 2. Ask for meistrari repos path
133
137
  const savedPath = readSavedReposPath()
@@ -161,18 +165,16 @@ async function handleLocalhostSetup() {
161
165
  const resolvedPath = reposPath.replace(/^~/, homedir())
162
166
  saveReposPath(resolvedPath)
163
167
 
164
- // 3. Read RSA keys and auth secret
168
+ // 3. Read auth-api access secret
165
169
  const keysSpinner = p.spinner()
166
- keysSpinner.start('Reading RSA keys from api-gateway...')
170
+ keysSpinner.start('Reading auth-api configuration...')
167
171
 
168
- let keys: ReturnType<typeof parseApiGatewayKeys>
169
172
  let authApiSecret: string
170
173
  try {
171
- keys = parseApiGatewayKeys(resolvedPath)
172
174
  authApiSecret = readAuthApiSecret(resolvedPath)
173
- keysSpinner.stop('RSA keys loaded')
175
+ keysSpinner.stop('Auth-api configuration loaded')
174
176
  } catch (err) {
175
- keysSpinner.stop('Failed to read keys')
177
+ keysSpinner.stop('Failed to read auth-api configuration')
176
178
  p.log.error(err instanceof Error ? err.message : String(err))
177
179
  process.exit(1)
178
180
  }
@@ -234,16 +236,22 @@ async function handleLocalhostSetup() {
234
236
  const tokenSpinner = p.spinner()
235
237
  tokenSpinner.start('Generating authentication token...')
236
238
 
237
- const token = signLocalhostJwt({
238
- workspace: { id: workspace.id, title: workspace.title },
239
- privateKey: keys.privateKey,
240
- kid: keys.kid,
241
- })
239
+ try {
240
+ const token = generateLocalhostJwt({
241
+ reposPath: resolvedPath,
242
+ workspace: { id: workspace.id, title: workspace.title },
243
+ authApiDbPort: services.authApiDbPort,
244
+ })
242
245
 
243
- saveLocalhostSession(token)
244
- writeLocalhostEnv(services.telaApiPort, services.telaAppPort)
246
+ saveLocalhostSession(token)
247
+ writeLocalhostEnv(services.telaApiPort, services.telaAppPort)
245
248
 
246
- tokenSpinner.stop('Token generated and saved')
249
+ tokenSpinner.stop('Token generated and saved')
250
+ } catch (err) {
251
+ tokenSpinner.stop('Failed to generate token')
252
+ p.log.error(err instanceof Error ? err.message : String(err))
253
+ process.exit(1)
254
+ }
247
255
 
248
256
  // 7. Install skills (reuse existing logic)
249
257
  if (YES) {
@@ -474,8 +482,8 @@ async function main() {
474
482
 
475
483
  const ENV_PATH = join(SESSION_DIR, '.env')
476
484
  const isStaging = env === 'staging'
477
- const apiUrl = isStaging ? 'https://staging.api.tela.com' : 'https://api.tela.com'
478
- const appUrl = isStaging ? 'https://staging.tela.com' : 'https://app.tela.com'
485
+ const apiUrl = isStaging ? 'https://api.telastaging.com' : 'https://api.tela.com'
486
+ const appUrl = isStaging ? 'https://app.telastaging.com' : 'https://app.tela.com'
479
487
 
480
488
  mkdirSync(SESSION_DIR, { recursive: true })
481
489
  writeFileSync(ENV_PATH, `TELA_API_URL=${apiUrl}\nTELA_APP_URL=${appUrl}\n`, { mode: 0o600 })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-skills",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Tela API skills for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,7 @@ Manage your local Tela development environment. Switch between workspaces, check
9
9
 
10
10
  ## Prerequisites
11
11
 
12
- - Docker running with tela services (auth-api, tela-api)
12
+ - Docker running with tela services (auth-api, auth-postgres, tela-api)
13
13
  - Localhost environment configured via `bunx @meistrari/tela-skills` (select "Localhost")
14
14
  - Tela monorepo path saved at `~/.tela/tela-repo-path`
15
15
 
@@ -31,7 +31,7 @@ bun list-workspaces.ts
31
31
 
32
32
  ### Switch Workspace
33
33
 
34
- Switch to a different workspace by generating a new JWT token.
34
+ Switch to a different workspace by generating a new JWT token through the local auth-api signing flow.
35
35
 
36
36
  Replace `WORKSPACE_ID` and `WORKSPACE_TITLE` with the target workspace values from "List Workspaces".
37
37
 
@@ -42,12 +42,13 @@ const { homedir } = await import('node:os')
42
42
  const lu = await import(join(homedir(), '.claude', 'skills', 'tela', 'localhost-utils.ts'))
43
43
  const reposPath = lu.readSavedReposPath()
44
44
  if (!reposPath) { console.error('No tela repo path saved. Run CLI setup first.'); process.exit(1) }
45
+ const services = lu.discoverLocalhostServices()
46
+ if (!services.authApiDbPort) { console.error('auth-postgres Docker container not found.'); process.exit(1) }
45
47
 
46
- const keys = lu.parseApiGatewayKeys(reposPath)
47
- const token = lu.signLocalhostJwt({
48
+ const token = lu.generateLocalhostJwt({
49
+ reposPath,
48
50
  workspace: { id: 'WORKSPACE_ID', title: 'WORKSPACE_TITLE' },
49
- privateKey: keys.privateKey,
50
- kid: keys.kid,
51
+ authApiDbPort: services.authApiDbPort,
51
52
  })
52
53
  lu.saveLocalhostSession(token)
53
54
  console.log('Switched to workspace: WORKSPACE_TITLE')
@@ -101,6 +102,7 @@ try {
101
102
  ## Troubleshooting
102
103
 
103
104
  - **"auth-api not running"**: Start Docker services in the tela monorepo (`make dev`)
105
+ - **"auth-postgres not running"**: Make sure the local auth database container is up and exposes its host port
104
106
  - **"No tela repo path saved"**: Run `bunx @meistrari/tela-skills` and select Localhost to configure
105
107
  - **"Connection: FAILED"**: Check that tela-api Docker container is running (`docker ps | grep tela-api`)
106
108
  - **Token expired**: Tokens are auto-regenerated when expired. If auto-regeneration fails, use "Switch Workspace" to regenerate manually
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: tela
3
- description: Skills for interacting with Tela - view, create, and update prompts/canvas. Upload and download files via Vault (vault:// references). Use this skill when user shares a Tela URL (app.tela.com or staging.tela.com) or asks about canvas/prompts. Do NOT use WebFetch for Tela URLs.
3
+ description: Skills for interacting with Tela - view, create, and update prompts/canvas. Upload and download files via Vault (vault:// references). Use this skill when user shares a Tela URL (app.tela.com or app.telastaging.com) or asks about canvas/prompts. Do NOT use WebFetch for Tela URLs.
4
4
  ---
5
5
 
6
6
  # Tela API Skills
@@ -9,7 +9,7 @@ Skills for interacting with the Tela API. Note: Prompts are called "Canvas" in T
9
9
 
10
10
  **IMPORTANT**: When the user shares a Tela URL like `https://app.tela.com/prompt/{id}/craft`, use these skills to fetch and modify the canvas - do NOT use WebFetch.
11
11
 
12
- **Workflows**: For workflow authoring and execution, use the `workflow.md` skill which provides version-safe orchestration (v2/v3), graph validation, and bundled reference docs.
12
+ **Workflows**: For workflow authoring and execution, use the `workflow.md` skill. Current graph authoring is the only supported path.
13
13
 
14
14
  ## Installation
15
15
 
@@ -19,7 +19,13 @@ bunx @meistrari/tela-skills
19
19
 
20
20
  ## Authentication
21
21
 
22
- The API uses Bearer token authentication. The token is stored at `~/.tela/session.production` or `~/.tela/session.staging`, depending on the configured environment.
22
+ The API uses token authentication via the `x-data-token` header. The token is stored at:
23
+
24
+ - `~/.tela/session.production` — Production environment
25
+ - `~/.tela/session.staging` — Staging environment
26
+ - `~/.tela/session.local` — Localhost environment (JWT, 30-day expiry, auto-regenerated when expired)
27
+
28
+ The active environment is determined by the `TELA_API_URL` in `~/.tela/.env`. When the URL contains `localhost`, the localhost session is used automatically. All session files are created with `0o600` permissions (user read/write only).
23
29
 
24
30
  ## Using These Skills
25
31
 
@@ -50,10 +56,8 @@ bun --preload ~/.claude/skills/tela/preload.ts -e "console.log(await tela.listPr
50
56
 
51
57
  ### Workflow Operations
52
58
 
53
- - `tela.createWorkflowVersionV2(payload)` - Create workflow prompt-version in v2 compatibility mode
54
- - `tela.createWorkflowVersionV3(payload)` - Create workflow prompt-version in v3 compatibility mode
55
- - `tela.updateWorkflowVersionV2(versionId, payload)` - Update workflow prompt-version in v2 compatibility mode
56
- - `tela.updateWorkflowVersionV3(versionId, payload)` - Update workflow prompt-version in v3 compatibility mode
59
+ - `tela.createWorkflowVersion(payload)` - Create workflow prompt-version with graph validation
60
+ - `tela.updateWorkflowVersion(versionId, payload)` - Update workflow prompt-version with graph validation
57
61
  - `tela.runWorkflow(payload)` - Run workflow via `/workflow/test`
58
62
  - `tela.getWorkflowRun(runId)` - Get workflow run by ID
59
63
  - `tela.waitForWorkflowRun(runId, options?)` - Poll until terminal status
@@ -61,10 +65,14 @@ bun --preload ~/.claude/skills/tela/preload.ts -e "console.log(await tela.listPr
61
65
  - `tela.listWorkflowRuns(options)` - List workflow runs by prompt
62
66
  - `tela.getWorkflowVariables(promptId, payload?)` - Get available variables for graph/input context
63
67
  - `tela.getWorkflowVariablesByStep(promptId, stepId, payload?)` - Get step-targeted compatibility variables
64
- - `tela.validateWorkflowGraph(graph, { version })` - Validate graph/ports/references by version
65
- - `tela.assertWorkflowGraphValid(graph, { version })` - Throw on validation errors
66
- - `tela.getWorkflowV2Url(promptId)` - Get legacy UI URL (`/prompt/:id/workflow`)
67
- - `tela.getWorkflowV3Url(promptId)` - Get DAG UI URL (`/workflows/:id`)
68
+ - `tela.validateWorkflowGraph(graph)` - Local pre-save graph shape checks (synchronous)
69
+ - `tela.assertWorkflowGraphValid(graph, { version })` - Throw on local validation errors
70
+ - `tela.validateWorkflowGraphServer(promptId, graph)` - Server-side validation with full inference (compatibility, condition branch visibility) — requires the prompt to exist
71
+ - `tela.getWorkflowActions()` - Catalog of all workflow actions with config/output schemas, reference field metadata, and public methods
72
+ - `tela.getWorkflowDependencies(promptId, versionId)` - All variables visible across a saved workflow version, with schemas
73
+ - `tela.getStepDependencies(promptId, versionId, stepId)` - Variables visible at a specific step, including per-reference-field format compatibility — use this instead of guessing what `?format=` values an action supports
74
+ - `tela.getStepTypes(promptId, versionId, stepId)` - TypeScript declarations (`.d.ts`) for the `TelaInput` shape inside a `codeExecutionNode` — use to author `input["..."]` accesses with confidence
75
+ - `tela.getWorkflowUrl(promptId)` - Get workflow UI URL (`/workflows/:id`)
68
76
 
69
77
  ### Export
70
78
 
@@ -153,9 +161,7 @@ const canvas = await tela.createCanvas({
153
161
  keyPoints: { type: 'array', items: { type: 'string' } }
154
162
  })
155
163
  },
156
- messages: [
157
- tela.createMessage('system', '## Task\n\nAnalyze the document.\n\n## Input\n\n{document}', 0)
158
- ],
164
+ message: tela.createMessage('system', '## Task\n\nAnalyze the document.\n\n## Input\n\n{document}', 0),
159
165
  variables: [tela.createVariable('document', 'file', { allowMultimodal: true })]
160
166
  }
161
167
  })
@@ -283,29 +289,51 @@ Referenced in content as `{variableName}` (single braces)
283
289
  }
284
290
  ```
285
291
 
286
- ### Messages
292
+ ### Message
287
293
  ```typescript
288
294
  { role: 'system' | 'user' | 'assistant', content: 'Message content', index: 0 }
289
295
  ```
290
296
 
291
297
  ## Environment Variables
292
298
 
293
- - `TELA_API_URL` - Override the API base URL (default: `https://api.tela.com` or `https://staging.api.tela.com` for staging)
294
- - `TELA_APP_URL` - Override the app base URL (default: `https://app.tela.com` or `https://staging.tela.com` for staging)
299
+ Configured automatically by the CLI installer in `~/.tela/.env`:
300
+
301
+ - `TELA_API_URL` - API base URL (production: `https://api.tela.com`, staging: `https://api.telastaging.com`, localhost: `http://localhost:<port>`)
302
+ - `TELA_APP_URL` - App base URL (production: `https://app.tela.com`, staging: `https://app.telastaging.com`, localhost: `http://localhost:<port>`)
303
+
304
+ For localhost, ports are auto-discovered from Docker containers (`auth-api`, `tela-api`, `tela-app`). If `tela-app` is not running, `TELA_APP_URL` defaults to `http://localhost:3000`.
305
+
306
+ ## Localhost Environment
307
+
308
+ For developing against locally running Tela services, use the localhost environment:
309
+
310
+ ```bash
311
+ bunx @meistrari/tela-skills # Select "Localhost" during setup
312
+ ```
313
+
314
+ Prerequisites: Docker running with tela services (`auth-api`, `auth-postgres`, `tela-api`), with auth settings available in `.repositories/auth-api/.env` and `AUTH_API_SECRET` in `packages/api/.env`. The CLI discovers ports, reads the local auth configuration, and generates a JWT token through the auth-api JWKS signing flow.
315
+
316
+ For workspace management after setup, see the `tela-localhost` skill (switch workspaces, check status, regenerate tokens).
317
+
318
+ Run commands against localhost with the helper script:
319
+ ```bash
320
+ bun localhost.ts "console.log(await tela.listProjects())"
321
+ ```
295
322
 
296
323
  ## File Structure
297
324
 
298
325
  ```
299
326
  ~/.claude/skills/tela/
300
- ├── SKILL.md # This file
301
- ├── preload.ts # Preload script - makes `tela` globally available
302
- ├── index.ts # Barrel export
303
- ├── common.ts # Shared utilities (loadApiKey, apiRequest)
304
- ├── projects.ts # Project functions
305
- ├── prompts.ts # Prompt/Canvas list functions
306
- ├── canvas.ts # Canvas create/update functions
307
- ├── workflows.ts # Workflow create/update/run/validation functions
308
- ├── test-cases.ts # Test case functions
309
- ├── tasks.ts # Task management functions
310
- └── vault.ts # Vault file management functions
327
+ ├── SKILL.md # This file
328
+ ├── preload.ts # Preload script - makes `tela` globally available
329
+ ├── index.ts # Barrel export
330
+ ├── common.ts # Shared utilities (loadApiKey, apiRequest, env detection)
331
+ ├── projects.ts # Project functions
332
+ ├── prompts.ts # Prompt/Canvas list functions
333
+ ├── canvas.ts # Canvas create/update functions
334
+ ├── workflows.ts # Workflow create/update/run/validation functions
335
+ ├── test-cases.ts # Test case functions
336
+ ├── tasks.ts # Task management functions
337
+ ├── vault.ts # Vault file management functions
338
+ └── localhost-utils.ts # Docker discovery, JWT signing, workspace fetching (internal, not on tela global)
311
339
  ```
@@ -0,0 +1,149 @@
1
+ import { beforeEach, describe, expect, mock, test } from 'bun:test'
2
+
3
+ const apiRequest = mock(async (_endpoint: string, _options?: RequestInit): Promise<unknown> => ({ id: 'canvas-id' }))
4
+
5
+ void mock.module('./common.ts', () => ({
6
+ apiRequest,
7
+ }))
8
+
9
+ describe('canvas', () => {
10
+ const legacyMessagesError = 'Use message instead of messages (pass a single CanvasMessage object, not an array)'
11
+
12
+ beforeEach(() => {
13
+ apiRequest.mockClear()
14
+ })
15
+
16
+ test('creates canvases with the canvas layout version', async () => {
17
+ const { createCanvas } = await import('./canvas.ts')
18
+
19
+ await createCanvas({
20
+ title: 'Canvas',
21
+ projectId: 'project-id',
22
+ workspaceId: 'workspace-id',
23
+ version: {
24
+ title: 'v1',
25
+ configuration: { model: 'gpt-5', type: 'chat' },
26
+ },
27
+ })
28
+
29
+ const [, options] = apiRequest.mock.calls[0]!
30
+ const body = JSON.parse(String(options?.body))
31
+
32
+ expect(body.layoutVersion).toBe('v1')
33
+ expect(body.version.isWorkflow).toBe(false)
34
+ })
35
+
36
+ test('rejects legacy message arrays when creating a canvas', async () => {
37
+ const { createCanvas } = await import('./canvas.ts')
38
+
39
+ const create = createCanvas({
40
+ title: 'Canvas',
41
+ projectId: 'project-id',
42
+ workspaceId: 'workspace-id',
43
+ version: {
44
+ title: 'v1',
45
+ configuration: { model: 'gpt-5', type: 'chat' },
46
+ // @ts-expect-error Legacy JavaScript callers can still send this field at runtime.
47
+ messages: [{ role: 'system', content: 'Legacy', index: 0 }],
48
+ },
49
+ })
50
+
51
+ let error: Error | undefined
52
+ try {
53
+ await create
54
+ }
55
+ catch (caught) {
56
+ error = caught as Error
57
+ }
58
+
59
+ expect(error?.message).toContain(legacyMessagesError)
60
+ expect(apiRequest).not.toHaveBeenCalled()
61
+ })
62
+
63
+ test('rejects legacy message arrays when updating a canvas version', async () => {
64
+ const { updateCanvasVersion } = await import('./canvas.ts')
65
+
66
+ const update = updateCanvasVersion('version-id', {
67
+ title: 'v2',
68
+ // @ts-expect-error Legacy JavaScript callers can still send this field at runtime.
69
+ messages: [{ role: 'system', content: 'Legacy', index: 0 }],
70
+ })
71
+
72
+ let error: Error | undefined
73
+ try {
74
+ await update
75
+ }
76
+ catch (caught) {
77
+ error = caught as Error
78
+ }
79
+
80
+ expect(error?.message).toContain(legacyMessagesError)
81
+ expect(apiRequest).not.toHaveBeenCalled()
82
+ })
83
+
84
+ test('rejects legacy message arrays when creating a canvas version', async () => {
85
+ const { createCanvasVersion } = await import('./canvas.ts')
86
+
87
+ const create = createCanvasVersion('canvas-id', {
88
+ title: 'v2',
89
+ configuration: { model: 'gpt-5', type: 'chat' },
90
+ // @ts-expect-error Legacy JavaScript callers can still send this field at runtime.
91
+ messages: [{ role: 'system', content: 'Legacy', index: 0 }],
92
+ })
93
+
94
+ let error: Error | undefined
95
+ try {
96
+ await create
97
+ }
98
+ catch (caught) {
99
+ error = caught as Error
100
+ }
101
+
102
+ expect(error?.message).toContain(legacyMessagesError)
103
+ expect(apiRequest).not.toHaveBeenCalled()
104
+ })
105
+
106
+ test('preserves the current message when updating a canvas', async () => {
107
+ const { updateCanvas } = await import('./canvas.ts')
108
+
109
+ apiRequest.mockImplementation(async (endpoint: string) => {
110
+ if (endpoint === '/prompt/canvas-id') {
111
+ return { id: 'canvas-id', title: 'Canvas' }
112
+ }
113
+
114
+ if (endpoint === '/prompt-version?promptId=canvas-id') {
115
+ return [
116
+ {
117
+ id: 'version-id',
118
+ promptId: 'canvas-id',
119
+ title: 'v1',
120
+ content: null,
121
+ markdownContent: null,
122
+ configuration: { model: 'gpt-5', type: 'chat' },
123
+ variables: [],
124
+ promoted: false,
125
+ draft: false,
126
+ isWorkflow: false,
127
+ workspaceId: 'workspace-id',
128
+ createdBy: 'user-id',
129
+ updatedBy: 'user-id',
130
+ createdAt: '2026-06-22T10:00:00.000Z',
131
+ updatedAt: '2026-06-22T10:00:00.000Z',
132
+ deletedAt: null,
133
+ message: { role: 'system', content: 'Current', markdownContent: 'Current', index: 0 },
134
+ },
135
+ ]
136
+ }
137
+
138
+ return { id: 'new-version-id' }
139
+ })
140
+
141
+ await updateCanvas('canvas-id', { title: 'v2' })
142
+
143
+ const [, options] = apiRequest.mock.calls[2]!
144
+ const body = JSON.parse(String(options?.body))
145
+
146
+ expect(body.message).toEqual({ role: 'system', content: 'Current', markdownContent: 'Current', index: 0 })
147
+ expect('messages' in body).toBe(false)
148
+ })
149
+ })