@meistrari/tela-skills 1.3.4 → 1.4.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.
package/README.md CHANGED
@@ -60,6 +60,9 @@ bun run cli.ts
60
60
 
61
61
  # Verify skill loads correctly
62
62
  bun --preload skill/preload.ts -e "console.log(Object.keys(tela))"
63
+
64
+ # Localhost (API :4000, App :3000)
65
+ bun localhost.ts "console.log(await tela.listProjects())"
63
66
  ```
64
67
 
65
68
  ### File layout after installation
package/cli.ts CHANGED
@@ -5,60 +5,49 @@ import { homedir } from 'node:os'
5
5
  import { join, dirname } from 'node:path'
6
6
  import { fileURLToPath } from 'node:url'
7
7
  import open from 'open'
8
+ import {
9
+ discoverLocalhostServices,
10
+ parseApiGatewayKeys,
11
+ readAuthApiSecret,
12
+ signLocalhostJwt,
13
+ fetchWorkspaces,
14
+ saveLocalhostSession,
15
+ writeLocalhostEnv,
16
+ readSavedReposPath,
17
+ saveReposPath,
18
+ } from './skill/localhost-utils.ts'
8
19
 
9
20
  const __dirname = dirname(fileURLToPath(import.meta.url))
10
21
  const SKILLS_DIR = join(homedir(), '.claude', 'skills', 'tela')
11
22
  const OPEN_ISSUE_SKILLS_DIR = join(homedir(), '.claude', 'skills', 'tela-open-issue')
23
+ const LOCALHOST_SKILLS_DIR = join(homedir(), '.claude', 'skills', 'tela-localhost')
12
24
  const SESSION_DIR = join(homedir(), '.tela')
13
25
  const BACKOFFICE_URL = 'https://backoffice.tela.com'
14
26
  const STAGING_BACKOFFICE_URL = 'https://staging-backoffice.tela.com'
15
27
 
16
- async function main() {
17
- p.intro('Tela Skills for Claude Code')
18
-
19
- const environment = await p.select({
20
- message: 'Which environment do you want to connect to?',
21
- options: [
22
- { value: 'production', label: 'Production' },
23
- { value: 'staging', label: 'Staging' },
24
- ],
25
- })
26
-
27
- if (p.isCancel(environment)) {
28
- p.cancel('Setup cancelled.')
29
- process.exit(0)
30
- }
31
-
32
- const env = environment as 'production' | 'staging'
33
- const sessionPath = join(SESSION_DIR, `session.${env}`)
34
- const sessionLabel = `~/.tela/session.${env}`
35
- const backofficeUrl = env === 'staging' ? STAGING_BACKOFFICE_URL : BACKOFFICE_URL
36
-
37
- const skillsExist = existsSync(SKILLS_DIR)
38
-
39
- if (skillsExist) {
40
- const update = await p.confirm({
41
- message: 'Tela skills already installed. Update to latest version?',
42
- initialValue: true,
43
- })
44
-
45
- if (p.isCancel(update) || !update) {
46
- p.cancel('Installation cancelled.')
47
- process.exit(0)
28
+ // --- CLI Flag Parsing ---
29
+ function parseFlags(argv: string[]): Record<string, string | true> {
30
+ const flags: Record<string, string | true> = {}
31
+ for (let i = 0; i < argv.length; i++) {
32
+ const arg = argv[i]!
33
+ if (arg.startsWith('--')) {
34
+ const key = arg.slice(2)
35
+ const next = argv[i + 1]
36
+ if (next && !next.startsWith('--')) {
37
+ flags[key] = next
38
+ i++
39
+ } else {
40
+ flags[key] = true
41
+ }
48
42
  }
49
43
  }
50
- else {
51
- const install = await p.confirm({
52
- message: 'Install Tela skills to ~/.claude/skills/tela/?',
53
- initialValue: true,
54
- })
44
+ return flags
45
+ }
55
46
 
56
- if (p.isCancel(install) || !install) {
57
- p.cancel('Installation cancelled.')
58
- process.exit(0)
59
- }
60
- }
47
+ const flags = parseFlags(process.argv.slice(2))
48
+ const YES = 'yes' in flags || 'y' in flags
61
49
 
50
+ async function installSkills() {
62
51
  const spinner = p.spinner()
63
52
  spinner.start('Installing skill files...')
64
53
 
@@ -68,17 +57,23 @@ async function main() {
68
57
  const sourceDir = join(__dirname, 'skill')
69
58
  const openIssueSkillFile = 'TELA_OPEN_ISSUE_SKILL.md'
70
59
  const telaSkillFile = 'TELA_SKILL.md'
71
- const files = readdirSync(sourceDir).filter(f => f !== openIssueSkillFile && f !== telaSkillFile)
60
+ const localhostSkillFile = 'TELA_LOCALHOST_SKILL.md'
61
+ const files = readdirSync(sourceDir).filter(f =>
62
+ f !== openIssueSkillFile && f !== telaSkillFile && f !== localhostSkillFile
63
+ )
72
64
 
73
65
  for (const file of files) {
74
66
  cpSync(join(sourceDir, file), join(SKILLS_DIR, file))
75
67
  }
76
68
  cpSync(join(sourceDir, telaSkillFile), join(SKILLS_DIR, 'SKILL.md'))
77
69
 
78
- // Install open-issue skill to its own directory
79
70
  mkdirSync(OPEN_ISSUE_SKILLS_DIR, { recursive: true })
80
71
  cpSync(join(sourceDir, openIssueSkillFile), join(OPEN_ISSUE_SKILLS_DIR, 'SKILL.md'))
81
72
 
73
+ // Install localhost skill to its own directory
74
+ mkdirSync(LOCALHOST_SKILLS_DIR, { recursive: true })
75
+ cpSync(join(sourceDir, localhostSkillFile), join(LOCALHOST_SKILLS_DIR, 'SKILL.md'))
76
+
82
77
  const agentsSourceDir = join(__dirname, 'agents')
83
78
  const agentsDestDir = join(homedir(), '.claude', 'agents')
84
79
  if (existsSync(agentsSourceDir)) {
@@ -107,38 +102,296 @@ async function main() {
107
102
  }
108
103
 
109
104
  depSpinner.stop(`Installed ${dependencies.length} dependencies`)
110
- }
111
- catch (err: unknown) {
105
+ } catch (err: unknown) {
112
106
  spinner.stop('Installation failed')
113
107
  p.log.error(err instanceof Error ? err.message : String(err))
114
108
  process.exit(1)
115
109
  }
110
+ }
116
111
 
117
- if (existsSync(sessionPath)) {
118
- const reconnect = await p.confirm({
119
- message: 'Session already configured. Clear and reconnect?',
120
- initialValue: false,
112
+ async function handleLocalhostSetup() {
113
+ // 1. Discover Docker services
114
+ const discoverSpinner = p.spinner()
115
+ discoverSpinner.start('Discovering local services via Docker...')
116
+
117
+ const services = discoverLocalhostServices()
118
+
119
+ if (!services.authApiPort) {
120
+ discoverSpinner.stop('auth-api container not found')
121
+ p.log.error('Could not find auth-api Docker container. Make sure local services are running.')
122
+ process.exit(1)
123
+ }
124
+ if (!services.telaApiPort) {
125
+ discoverSpinner.stop('tela-api container not found')
126
+ p.log.error('Could not find tela-api Docker container. Make sure local services are running.')
127
+ process.exit(1)
128
+ }
129
+
130
+ discoverSpinner.stop(`Found services — auth-api :${services.authApiPort}, tela-api :${services.telaApiPort}${services.telaAppPort ? `, tela-app :${services.telaAppPort}` : ''}`)
131
+
132
+ // 2. Ask for meistrari repos path
133
+ const savedPath = readSavedReposPath()
134
+ const reposPathFlag = typeof flags['repos-path'] === 'string' ? flags['repos-path'] : null
135
+
136
+ let reposPath: string
137
+ if (reposPathFlag) {
138
+ reposPath = reposPathFlag
139
+ p.log.info(`Using repos path: ${reposPath}`)
140
+ } else if (YES && savedPath) {
141
+ reposPath = savedPath
142
+ p.log.info(`Using saved repos path: ${reposPath}`)
143
+ } else {
144
+ const result = await p.text({
145
+ message: 'Where is the tela monorepo cloned?',
146
+ placeholder: '/path/to/tela',
147
+ ...(savedPath ? { defaultValue: savedPath } : {}),
148
+ validate(value): string | undefined {
149
+ if (!value.trim()) return 'Path is required'
150
+ return undefined
151
+ },
152
+ })
153
+
154
+ if (p.isCancel(result)) {
155
+ p.cancel('Setup cancelled.')
156
+ process.exit(0)
157
+ }
158
+ reposPath = result
159
+ }
160
+
161
+ const resolvedPath = reposPath.replace(/^~/, homedir())
162
+ saveReposPath(resolvedPath)
163
+
164
+ // 3. Read RSA keys and auth secret
165
+ const keysSpinner = p.spinner()
166
+ keysSpinner.start('Reading RSA keys from api-gateway...')
167
+
168
+ let keys: ReturnType<typeof parseApiGatewayKeys>
169
+ let authApiSecret: string
170
+ try {
171
+ keys = parseApiGatewayKeys(resolvedPath)
172
+ authApiSecret = readAuthApiSecret(resolvedPath)
173
+ keysSpinner.stop('RSA keys loaded')
174
+ } catch (err) {
175
+ keysSpinner.stop('Failed to read keys')
176
+ p.log.error(err instanceof Error ? err.message : String(err))
177
+ process.exit(1)
178
+ }
179
+
180
+ // 4. Fetch workspaces
181
+ const authApiUrl = `http://localhost:${services.authApiPort}`
182
+ const wsSpinner = p.spinner()
183
+ wsSpinner.start('Fetching workspaces from auth-api...')
184
+
185
+ let workspaces: Awaited<ReturnType<typeof fetchWorkspaces>>
186
+ try {
187
+ workspaces = await fetchWorkspaces(authApiUrl, authApiSecret)
188
+ wsSpinner.stop(`Found ${workspaces.length} workspace(s)`)
189
+ } catch (err) {
190
+ wsSpinner.stop('Failed to fetch workspaces')
191
+ p.log.error(err instanceof Error ? err.message : String(err))
192
+ process.exit(1)
193
+ }
194
+
195
+ if (workspaces.length === 0) {
196
+ p.log.warning('No workspaces found. Create one using auth-api CLI scripts first.')
197
+ process.exit(1)
198
+ }
199
+
200
+ // 5. Select workspace
201
+ const workspaceFlag = typeof flags['workspace'] === 'string' ? flags['workspace'] : null
202
+
203
+ let workspace: { id: string; title: string; slug: string }
204
+ if (workspaceFlag) {
205
+ const match = workspaces.find(ws => ws.id === workspaceFlag || ws.slug === workspaceFlag)
206
+ if (!match) {
207
+ p.log.error(`Workspace "${workspaceFlag}" not found. Available: ${workspaces.map(ws => `${ws.slug} (${ws.id})`).join(', ')}`)
208
+ process.exit(1)
209
+ }
210
+ workspace = match
211
+ p.log.info(`Using workspace: ${workspace.title}`)
212
+ } else if (YES && workspaces.length === 1) {
213
+ workspace = workspaces[0]!
214
+ p.log.info(`Auto-selecting only workspace: ${workspace.title}`)
215
+ } else {
216
+ const selectedWorkspace = await p.select({
217
+ message: 'Which workspace do you want to connect to?',
218
+ options: workspaces.map(ws => ({
219
+ value: ws,
220
+ label: ws.title,
221
+ hint: ws.slug,
222
+ })),
223
+ })
224
+
225
+ if (p.isCancel(selectedWorkspace)) {
226
+ p.cancel('Setup cancelled.')
227
+ process.exit(0)
228
+ }
229
+
230
+ workspace = selectedWorkspace as { id: string; title: string; slug: string }
231
+ }
232
+
233
+ // 6. Generate JWT and save
234
+ const tokenSpinner = p.spinner()
235
+ tokenSpinner.start('Generating authentication token...')
236
+
237
+ const token = signLocalhostJwt({
238
+ workspace: { id: workspace.id, title: workspace.title },
239
+ privateKey: keys.privateKey,
240
+ kid: keys.kid,
241
+ })
242
+
243
+ saveLocalhostSession(token)
244
+ writeLocalhostEnv(services.telaApiPort, services.telaAppPort)
245
+
246
+ tokenSpinner.stop('Token generated and saved')
247
+
248
+ // 7. Install skills (reuse existing logic)
249
+ if (YES) {
250
+ await installSkills()
251
+ } else {
252
+ const skillsExist = existsSync(SKILLS_DIR)
253
+
254
+ if (skillsExist) {
255
+ const update = await p.confirm({
256
+ message: 'Tela skills already installed. Update to latest version?',
257
+ initialValue: true,
258
+ })
259
+
260
+ if (!p.isCancel(update) && update) {
261
+ await installSkills()
262
+ }
263
+ } else {
264
+ const install = await p.confirm({
265
+ message: 'Install Tela skills to ~/.claude/skills/tela/?',
266
+ initialValue: true,
267
+ })
268
+
269
+ if (!p.isCancel(install) && install) {
270
+ await installSkills()
271
+ }
272
+ }
273
+ }
274
+
275
+ writeFileSync(join(SESSION_DIR, 'environment'), 'localhost', { mode: 0o600 })
276
+
277
+ p.log.success(`Connected to workspace "${workspace.title}" on localhost`)
278
+
279
+ p.note(
280
+ `Use in Claude Code with bun --preload:
281
+
282
+ bun --preload ~/.claude/skills/tela/preload.ts -e "
283
+ console.log(await tela.listProjects())
284
+ "`,
285
+ 'Usage',
286
+ )
287
+
288
+ p.outro('Localhost setup complete!')
289
+ }
290
+
291
+ async function main() {
292
+ p.intro('Tela Skills for Claude Code')
293
+
294
+ const envFlag = typeof flags['env'] === 'string' ? flags['env'] : null
295
+ const validEnvs = ['production', 'staging', 'localhost'] as const
296
+
297
+ let environment: string
298
+ if (envFlag) {
299
+ if (!validEnvs.includes(envFlag as any)) {
300
+ p.log.error(`Invalid environment: "${envFlag}". Valid: ${validEnvs.join(', ')}`)
301
+ process.exit(1)
302
+ }
303
+ environment = envFlag
304
+ p.log.info(`Environment: ${environment}`)
305
+ } else {
306
+ const selected = await p.select({
307
+ message: 'Which environment do you want to connect to?',
308
+ options: [
309
+ { value: 'production', label: 'Production' },
310
+ { value: 'staging', label: 'Staging' },
311
+ { value: 'localhost', label: 'Localhost (internal)' },
312
+ ],
121
313
  })
122
314
 
123
- if (!p.isCancel(reconnect) && reconnect) {
124
- const { unlinkSync } = await import('node:fs')
125
- unlinkSync(sessionPath)
126
- const envPath = join(SESSION_DIR, 'environment')
127
- if (existsSync(envPath)) unlinkSync(envPath)
128
- p.log.info('Session cleared. Proceeding to authentication...')
315
+ if (p.isCancel(selected)) {
316
+ p.cancel('Setup cancelled.')
317
+ process.exit(0)
318
+ }
319
+ environment = selected as string
320
+ }
321
+
322
+ if (environment === 'localhost') {
323
+ await handleLocalhostSetup()
324
+ return
325
+ }
326
+
327
+ const env = environment as 'production' | 'staging'
328
+ const sessionPath = join(SESSION_DIR, `session.${env}`)
329
+ const sessionLabel = `~/.tela/session.${env}`
330
+ const backofficeUrl = env === 'staging' ? STAGING_BACKOFFICE_URL : BACKOFFICE_URL
331
+
332
+ if (YES) {
333
+ await installSkills()
334
+ } else {
335
+ const skillsExist = existsSync(SKILLS_DIR)
336
+
337
+ if (skillsExist) {
338
+ const update = await p.confirm({
339
+ message: 'Tela skills already installed. Update to latest version?',
340
+ initialValue: true,
341
+ })
342
+
343
+ if (p.isCancel(update) || !update) {
344
+ p.cancel('Installation cancelled.')
345
+ process.exit(0)
346
+ }
129
347
  }
130
348
  else {
349
+ const install = await p.confirm({
350
+ message: 'Install Tela skills to ~/.claude/skills/tela/?',
351
+ initialValue: true,
352
+ })
353
+
354
+ if (p.isCancel(install) || !install) {
355
+ p.cancel('Installation cancelled.')
356
+ process.exit(0)
357
+ }
358
+ }
359
+
360
+ await installSkills()
361
+ }
362
+
363
+ if (existsSync(sessionPath)) {
364
+ if (YES) {
131
365
  p.log.info(`Keeping existing session at ${sessionLabel}`)
366
+ } else {
367
+ const reconnect = await p.confirm({
368
+ message: 'Session already configured. Clear and reconnect?',
369
+ initialValue: false,
370
+ })
371
+
372
+ if (!p.isCancel(reconnect) && reconnect) {
373
+ const { unlinkSync } = await import('node:fs')
374
+ unlinkSync(sessionPath)
375
+ const envPath = join(SESSION_DIR, 'environment')
376
+ if (existsSync(envPath)) unlinkSync(envPath)
377
+ p.log.info('Session cleared. Proceeding to authentication...')
378
+ }
379
+ else {
380
+ p.log.info(`Keeping existing session at ${sessionLabel}`)
381
+ }
132
382
  }
133
383
  }
134
384
 
135
385
  if (!existsSync(sessionPath)) {
136
- const setupKey = await p.confirm({
137
- message: 'Set up Tela authentication now?',
138
- initialValue: true,
139
- })
386
+ const shouldAuth = YES || await (async () => {
387
+ const setupKey = await p.confirm({
388
+ message: 'Set up Tela authentication now?',
389
+ initialValue: true,
390
+ })
391
+ return !p.isCancel(setupKey) && setupKey
392
+ })()
140
393
 
141
- if (!p.isCancel(setupKey) && setupKey) {
394
+ if (shouldAuth) {
142
395
  let resolveToken: (token: string) => void
143
396
  let rejectToken: (error: Error) => void
144
397
  const tokenPromise = new Promise<string>((resolve, reject) => {
@@ -147,6 +400,7 @@ async function main() {
147
400
  })
148
401
 
149
402
  const server = Bun.serve({
403
+ hostname: '127.0.0.1',
150
404
  port: 0,
151
405
  fetch(req) {
152
406
  const url = new URL(req.url)
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * List available Tela workspaces from the local auth-api.
4
+ *
5
+ * Usage:
6
+ * bun list-workspaces.ts
7
+ * bunx @meistrari/tela-skills/list-workspaces
8
+ *
9
+ * Requires:
10
+ * - Docker running with auth-api container
11
+ * - Tela monorepo path saved at ~/.tela/tela-repo-path (via CLI setup)
12
+ */
13
+ import {
14
+ discoverLocalhostServices,
15
+ readSavedReposPath,
16
+ readAuthApiSecret,
17
+ fetchWorkspaces,
18
+ } from './skill/localhost-utils.ts'
19
+
20
+ const reposPath = readSavedReposPath()
21
+ if (!reposPath) {
22
+ console.error('No tela repo path saved. Run CLI setup first: bunx @meistrari/tela-skills → select Localhost')
23
+ process.exit(1)
24
+ }
25
+
26
+ const services = discoverLocalhostServices()
27
+ if (!services.authApiPort) {
28
+ console.error('auth-api Docker container not found. Are local services running?')
29
+ process.exit(1)
30
+ }
31
+
32
+ const secret = readAuthApiSecret(reposPath)
33
+ const workspaces = await fetchWorkspaces(`http://localhost:${services.authApiPort}`, secret)
34
+
35
+ if (workspaces.length === 0) {
36
+ console.log('No workspaces found.')
37
+ process.exit(0)
38
+ }
39
+
40
+ console.log('Available workspaces:')
41
+ for (const ws of workspaces) {
42
+ console.log(` - ${ws.title} (${ws.id}) [${ws.slug}]`)
43
+ }
package/localhost.ts ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Run tela-skills against localhost (discovers ports via Docker).
4
+ * Usage: bun run localhost -- "console.log(await tela.listProjects())"
5
+ * or: bun localhost.ts "console.log(await tela.listProjects())"
6
+ */
7
+ import { existsSync } from 'node:fs'
8
+ import { dirname, join } from 'node:path'
9
+ import { homedir } from 'node:os'
10
+ import { fileURLToPath } from 'node:url'
11
+ import { discoverLocalhostServices, writeLocalhostEnv } from './skill/localhost-utils.ts'
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url))
14
+ const TELA_PRELOAD = join(__dirname, 'skill', 'preload.ts')
15
+ const SESSION_PATH = join(homedir(), '.tela', 'session.local')
16
+
17
+ const args = process.argv.slice(2)
18
+ const code = args.join(' ').trim()
19
+ if (!code) {
20
+ console.error('Usage: bun localhost.ts "console.log(await tela.listProjects())"')
21
+ process.exit(1)
22
+ }
23
+
24
+ // Check session.local exists
25
+ if (!existsSync(SESSION_PATH)) {
26
+ console.error('Error: No localhost session found at ~/.tela/session.local')
27
+ console.error('Run CLI setup first: bunx @meistrari/tela-skills → select Localhost')
28
+ process.exit(1)
29
+ }
30
+
31
+ // Discover ports from Docker
32
+ const services = discoverLocalhostServices()
33
+ if (!services.telaApiPort) {
34
+ console.error('Error: tela-api Docker container not found. Are local services running?')
35
+ process.exit(1)
36
+ }
37
+
38
+ // Write .env with discovered ports
39
+ writeLocalhostEnv(services.telaApiPort, services.telaAppPort)
40
+
41
+ const proc = Bun.spawn(['bun', '--preload', TELA_PRELOAD, '-e', code], {
42
+ stdio: ['inherit', 'inherit', 'inherit'],
43
+ })
44
+ process.exit(await proc.exited)
package/package.json CHANGED
@@ -1,17 +1,21 @@
1
1
  {
2
2
  "name": "@meistrari/tela-skills",
3
- "version": "1.3.4",
3
+ "version": "1.4.0",
4
4
  "description": "Tela API skills for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
7
- "tela-skills": "./cli.ts"
7
+ "tela-skills": "./cli.ts",
8
+ "tela-list-workspaces": "./list-workspaces.ts"
8
9
  },
9
10
  "scripts": {
11
+ "prepare": "ln -sf ../../scripts/pre-commit .git/hooks/pre-commit",
10
12
  "postinstall": "mise-en-place postinstall",
11
13
  "build": "echo \"No build step required, script placeholder for npm publish action compatibility\""
12
14
  },
13
15
  "files": [
14
16
  "cli.ts",
17
+ "localhost.ts",
18
+ "list-workspaces.ts",
15
19
  "skill/**/*"
16
20
  ],
17
21
  "keywords": [
@@ -29,7 +33,7 @@
29
33
  "open": "^11.0.0"
30
34
  },
31
35
  "devDependencies": {
32
- "@meistrari/mise-en-place": "^2.10.3",
36
+ "@meistrari/mise-en-place": "^2.10.6",
33
37
  "bun-types": "^1.3.4"
34
38
  }
35
39
  }
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: tela-localhost
3
+ description: Manage localhost workspaces for Tela. Switch workspaces, check status, and regenerate tokens for local development.
4
+ ---
5
+
6
+ # Tela Localhost Workspace Manager
7
+
8
+ Manage your local Tela development environment. Switch between workspaces, check connection status, and regenerate authentication tokens.
9
+
10
+ ## Prerequisites
11
+
12
+ - Docker running with tela services (auth-api, tela-api)
13
+ - Localhost environment configured via `bunx @meistrari/tela-skills` (select "Localhost")
14
+ - Tela monorepo path saved at `~/.tela/tela-repo-path`
15
+
16
+ ## Commands
17
+
18
+ ### List Workspaces
19
+
20
+ Show all available workspaces in the local environment.
21
+
22
+ ```bash
23
+ bunx @meistrari/tela-skills/list-workspaces
24
+ ```
25
+
26
+ Or if running from the repo directly:
27
+
28
+ ```bash
29
+ bun list-workspaces.ts
30
+ ```
31
+
32
+ ### Switch Workspace
33
+
34
+ Switch to a different workspace by generating a new JWT token.
35
+
36
+ Replace `WORKSPACE_ID` and `WORKSPACE_TITLE` with the target workspace values from "List Workspaces".
37
+
38
+ ```bash
39
+ bun --preload ~/.claude/skills/tela/preload.ts -e "
40
+ const { join } = await import('node:path')
41
+ const { homedir } = await import('node:os')
42
+ const lu = await import(join(homedir(), '.claude', 'skills', 'tela', 'localhost-utils.ts'))
43
+ const reposPath = lu.readSavedReposPath()
44
+ if (!reposPath) { console.error('No tela repo path saved. Run CLI setup first.'); process.exit(1) }
45
+
46
+ const keys = lu.parseApiGatewayKeys(reposPath)
47
+ const token = lu.signLocalhostJwt({
48
+ workspace: { id: 'WORKSPACE_ID', title: 'WORKSPACE_TITLE' },
49
+ privateKey: keys.privateKey,
50
+ kid: keys.kid,
51
+ })
52
+ lu.saveLocalhostSession(token)
53
+ console.log('Switched to workspace: WORKSPACE_TITLE')
54
+ console.log('New token saved to ~/.tela/session.local')
55
+ "
56
+ ```
57
+
58
+ ### Check Status
59
+
60
+ Verify localhost connection is working and show current workspace.
61
+
62
+ ```bash
63
+ bun --preload ~/.claude/skills/tela/preload.ts -e "
64
+ const { readFileSync, existsSync } = await import('node:fs')
65
+ const { join } = await import('node:path')
66
+ const { homedir } = await import('node:os')
67
+
68
+ const envPath = join(homedir(), '.tela', '.env')
69
+ const sessionPath = join(homedir(), '.tela', 'session.local')
70
+
71
+ if (!existsSync(sessionPath)) {
72
+ console.log('Status: NOT CONFIGURED')
73
+ console.log('Run CLI setup: bunx @meistrari/tela-skills → select Localhost')
74
+ process.exit(0)
75
+ }
76
+
77
+ // Decode JWT payload
78
+ const token = readFileSync(sessionPath, 'utf-8').trim()
79
+ const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString())
80
+ console.log('Status: CONFIGURED')
81
+ console.log('Workspace:', payload.workspace?.title, '(' + payload.workspace?.id + ')')
82
+ console.log('Token expires:', new Date(payload.exp * 1000).toISOString())
83
+
84
+ // Check env
85
+ if (existsSync(envPath)) {
86
+ const env = readFileSync(envPath, 'utf-8')
87
+ console.log('API URL:', env.match(/TELA_API_URL=(.*)/)?.[1] || 'not set')
88
+ console.log('App URL:', env.match(/TELA_APP_URL=(.*)/)?.[1] || 'not set')
89
+ }
90
+
91
+ // Try a test call
92
+ try {
93
+ const projects = await tela.listProjects()
94
+ console.log('Connection: OK (' + projects.length + ' projects)')
95
+ } catch (e) {
96
+ console.log('Connection: FAILED -', e.message)
97
+ }
98
+ "
99
+ ```
100
+
101
+ ## Troubleshooting
102
+
103
+ - **"auth-api not running"**: Start Docker services in the tela monorepo (`make dev`)
104
+ - **"No tela repo path saved"**: Run `bunx @meistrari/tela-skills` and select Localhost to configure
105
+ - **"Connection: FAILED"**: Check that tela-api Docker container is running (`docker ps | grep tela-api`)
106
+ - **Token expired**: Tokens are auto-regenerated when expired. If auto-regeneration fails, use "Switch Workspace" to regenerate manually