feat: 添加工具类命令(teleport、recap、break-cache、env、tui 等)

- /teleport: 从 claude.ai 恢复会话
- /recap: 生成会话摘要
- /break-cache: 提示缓存管理(once/always/off/status)
- /env: 环境信息展示(含密钥脱敏)
- /tui: 无闪烁 TUI 模式管理
- /onboarding: 引导流程
- /perf-issue: 性能问题诊断
- /debug-tool-call: 工具调用调试
- /usage: 用量统计(合并 /cost 和 /stats 别名)

Co-Authored-By: glm-5-turbo <zai-org@claude-code-best.win>
This commit is contained in:
claude-code-best
2026-05-09 23:04:31 +08:00
parent 6766f08e47
commit fdddb6dbe8
38 changed files with 5494 additions and 43 deletions

View File

@@ -0,0 +1,58 @@
/**
* Tests for teleport/index.ts — command metadata + load() body.
* We do NOT mock launchTeleport to avoid polluting launchTeleport.test.ts
* via Bun's process-level mock.module cache.
* load() is tested by verifying it resolves to an object with a call function.
*/
import { beforeAll, describe, expect, mock, test } from 'bun:test'
mock.module('bun:bundle', () => ({
feature: (_name: string) => false,
}))
let cmd: {
load?: () => Promise<{ call: unknown }>
isEnabled?: () => boolean
name?: string
type?: string
aliases?: string[]
getBridgeInvocationError?: (args: string) => string | undefined
}
beforeAll(async () => {
const mod = await import('../index.js')
cmd = mod.default as typeof cmd
})
describe('teleport index', () => {
test('command name is teleport', () => {
expect(cmd.name).toBe('teleport')
})
test('command type is local-jsx', () => {
expect(cmd.type).toBe('local-jsx')
})
test('isEnabled returns true', () => {
expect(cmd.isEnabled?.()).toBe(true)
})
test('aliases includes tp', () => {
expect(cmd.aliases).toContain('tp')
})
test('getBridgeInvocationError returns error string (not bridge-safe)', () => {
const err = cmd.getBridgeInvocationError?.('anything')
expect(typeof err).toBe('string')
expect(err).toContain('not bridge-safe')
})
test('load() exists and is a function', () => {
expect(typeof cmd.load).toBe('function')
})
test('load() resolves to object with call function', async () => {
const loaded = await cmd.load!()
expect(typeof (loaded as { call?: unknown }).call).toBe('function')
})
})

View File

@@ -0,0 +1,388 @@
import { beforeAll, beforeEach, describe, expect, mock, test } from 'bun:test'
import type { LogOption } from '../../../types/logs.js'
import type { LocalJSXCommandCall } from '../../../types/command.js'
import { debugMock } from '../../../../tests/mocks/debug.js'
import { logMock } from '../../../../tests/mocks/log.js'
// ── Mock module-level side effects BEFORE any imports ──
mock.module('src/utils/log.ts', logMock)
mock.module('src/utils/debug.ts', debugMock)
mock.module('bun:bundle', () => ({
feature: (_name: string) => false,
}))
// ── Teleport utilities ──
const validateGitStateMock = mock(() => Promise.resolve())
const teleportResumeMock = mock(
(_id: string, _onProgress?: (stage: string) => void) =>
Promise.resolve({ log: [], branch: 'main' }),
)
mock.module('src/utils/teleport.js', () => ({
validateGitState: validateGitStateMock,
teleportResumeCodeSession: teleportResumeMock,
processMessagesForTeleportResume: mock(
(_msgs: unknown[], _err: unknown) => [],
),
checkOutTeleportedSessionBranch: mock(() =>
Promise.resolve({ branchName: 'main', branchError: null }),
),
validateSessionRepository: mock(() => Promise.resolve({ status: 'match' })),
teleportToRemoteWithErrorHandling: mock(() => Promise.resolve(null)),
teleportFromSessionsAPI: mock(() =>
Promise.resolve({ log: [], branch: 'main' }),
),
pollRemoteSessionEvents: mock(() => Promise.resolve([])),
teleportToRemote: mock(() => Promise.resolve(null)),
archiveRemoteSession: mock(() => Promise.resolve()),
}))
// ── Sessions API mock ──
const fetchSessionsMock = mock(() =>
Promise.resolve([
{
id: 'session_01ABC',
title: 'Test session',
status: 'idle',
created_at: '2026-04-29',
},
]),
)
mock.module('src/utils/teleport/api.js', () => ({
fetchCodeSessionsFromSessionsAPI: fetchSessionsMock,
}))
// ── Session storage ──
const mockLog: LogOption = {
date: '2026-04-29',
messages: [],
value: 0,
created: new Date(),
modified: new Date(),
firstPrompt: '',
messageCount: 0,
isSidechain: false,
}
const getLastSessionLogMock = mock(() => Promise.resolve(mockLog))
mock.module('src/utils/sessionStorage.js', () => ({
getLastSessionLog: getLastSessionLogMock,
}))
// ── Analytics ──
const logEventMock = mock(() => {})
mock.module('src/services/analytics/index.js', () => ({
logEvent: logEventMock,
logEventAsync: mock(() => Promise.resolve()),
_resetForTesting: mock(() => {}),
attachAnalyticsSink: mock(() => {}),
stripProtoFields: mock((v: unknown) => v),
}))
// ── Import SUT after mocks ──
let callTeleport: LocalJSXCommandCall
beforeAll(async () => {
const sut = await import('../launchTeleport.js')
callTeleport = sut.callTeleport
})
// ── Test helpers ──
const onDone = mock((_result?: string, _opts?: unknown) => {})
const resumeMockFn = mock(() => Promise.resolve())
function makeContext(withResume = true) {
return {
abortController: new AbortController(),
resume: withResume ? resumeMockFn : undefined,
} as unknown as Parameters<typeof callTeleport>[1]
}
function getLoggedEvents(): string[] {
return (logEventMock.mock.calls as unknown as [string, unknown][]).map(
c => c[0],
)
}
beforeEach(() => {
validateGitStateMock.mockClear()
teleportResumeMock.mockClear()
getLastSessionLogMock.mockClear()
fetchSessionsMock.mockClear()
logEventMock.mockClear()
onDone.mockClear()
resumeMockFn.mockClear()
// Restore default happy-path implementations
validateGitStateMock.mockImplementation(() => Promise.resolve())
teleportResumeMock.mockImplementation(
(_id: string, _onProgress?: (stage: string) => void) =>
Promise.resolve({ log: [], branch: 'main' }),
)
getLastSessionLogMock.mockImplementation(() => Promise.resolve(mockLog))
fetchSessionsMock.mockImplementation(() =>
Promise.resolve([
{
id: 'session_01ABC',
title: 'Test session',
status: 'idle',
created_at: '2026-04-29',
},
]),
)
})
describe('callTeleport', () => {
test('empty args: fetches sessions list and shows picker', async () => {
await callTeleport(onDone, makeContext(), ' ')
const firstArg = onDone.mock.calls[0]?.[0] as string | undefined
expect(firstArg).toMatch(/Available sessions/)
expect(validateGitStateMock).not.toHaveBeenCalled()
expect(teleportResumeMock).not.toHaveBeenCalled()
const events = getLoggedEvents()
expect(events).toContain('tengu_teleport_started')
expect(events).toContain('tengu_teleport_source_decision')
})
test('empty args + sessions fetch fails with generic error → fetch_fail event', async () => {
fetchSessionsMock.mockImplementationOnce(() =>
Promise.reject(new Error('network timeout')),
)
await callTeleport(onDone, makeContext(), '')
const firstArg = onDone.mock.calls[0]?.[0] as string | undefined
expect(firstArg).toMatch(/failed to fetch sessions/)
const events = getLoggedEvents()
expect(events).toContain('tengu_teleport_events_fetch_fail')
})
test('empty args + sessions fetch fails with 401/forbidden → fetch_forbidden event', async () => {
fetchSessionsMock.mockImplementationOnce(() =>
Promise.reject(new Error('403 Forbidden: access denied')),
)
await callTeleport(onDone, makeContext(), '')
const firstArg = onDone.mock.calls[0]?.[0] as string | undefined
expect(firstArg).toMatch(/permission denied/)
const events = getLoggedEvents()
expect(events).toContain('tengu_teleport_events_fetch_forbidden')
})
test('empty args + sessions fetch fails with 404/not-found → fetch_not_found event', async () => {
fetchSessionsMock.mockImplementationOnce(() =>
Promise.reject(new Error('404 Not Found')),
)
await callTeleport(onDone, makeContext(), '')
const firstArg = onDone.mock.calls[0]?.[0] as string | undefined
expect(firstArg).toMatch(/404/)
const events = getLoggedEvents()
expect(events).toContain('tengu_teleport_events_fetch_not_found')
})
test('empty args + sessions fetch fails with token/unauthorized → bad_token event', async () => {
fetchSessionsMock.mockImplementationOnce(() =>
Promise.reject(new Error('unauthorized: invalid token')),
)
await callTeleport(onDone, makeContext(), '')
const firstArg = onDone.mock.calls[0]?.[0] as string | undefined
expect(firstArg).toMatch(/authentication error/)
const events = getLoggedEvents()
expect(events).toContain('tengu_teleport_error_bad_token')
})
test('empty args + empty sessions list → teleport_null event', async () => {
fetchSessionsMock.mockImplementationOnce(() => Promise.resolve([]))
await callTeleport(onDone, makeContext(), '')
const firstArg = onDone.mock.calls[0]?.[0] as string | undefined
expect(firstArg).toMatch(/No active sessions/)
const events = getLoggedEvents()
expect(events).toContain('tengu_teleport_null')
})
test('empty args + exactly PICKER_PAGE_CAP sessions → page_cap event', async () => {
// 20 sessions triggers the page cap log
const sessions = Array.from({ length: 20 }, (_, i) => ({
id: `session_${i}`,
title: `Session ${i}`,
status: 'idle',
created_at: '2026-04-29',
}))
fetchSessionsMock.mockImplementationOnce(() => Promise.resolve(sessions))
await callTeleport(onDone, makeContext(), '')
const events = getLoggedEvents()
expect(events).toContain('tengu_teleport_page_cap')
})
test('--print flag with no session id → shows picker in print mode', async () => {
await callTeleport(onDone, makeContext(), '--print')
const firstArg = onDone.mock.calls[0]?.[0] as string | undefined
expect(firstArg).toMatch(/Available sessions/)
})
test('short non-UUID session id is rejected without calling teleport', async () => {
await callTeleport(onDone, makeContext(), 'abc')
const firstArg = onDone.mock.calls[0]?.[0] as string | undefined
expect(firstArg).toMatch(/Invalid session id/)
expect(validateGitStateMock).not.toHaveBeenCalled()
expect(teleportResumeMock).not.toHaveBeenCalled()
})
test('valid session id + git unclean → reports error, skips resume', async () => {
validateGitStateMock.mockImplementation(() =>
Promise.reject(
new Error(
'Git working directory is not clean. Please commit or stash your changes.',
),
),
)
await callTeleport(
onDone,
makeContext(),
'12345678-abcd-ef01-2345-6789abcdef01',
)
const firstArg = onDone.mock.calls[0]?.[0] as string | undefined
expect(firstArg).toMatch(/Cannot teleport/)
expect(firstArg).toMatch(/not clean/)
expect(teleportResumeMock).not.toHaveBeenCalled()
})
test('valid session id + clean git → calls teleportResumeCodeSession + context.resume', async () => {
const ctx = makeContext(true)
await callTeleport(onDone, ctx, '12345678-abcd-ef01-2345-6789abcdef01')
expect(teleportResumeMock).toHaveBeenCalledWith(
'12345678-abcd-ef01-2345-6789abcdef01',
expect.any(Function),
)
expect(resumeMockFn).toHaveBeenCalledWith(
'12345678-abcd-ef01-2345-6789abcdef01',
mockLog,
'slash_command_session_id',
)
const events = getLoggedEvents()
expect(events).toContain('tengu_teleport_resume_session')
expect(events).toContain('tengu_teleport_first_message_success')
})
test('progress callback is invoked during teleportResumeCodeSession (line 225)', async () => {
teleportResumeMock.mockImplementationOnce(
(_id: string, onProgress?: (stage: string) => void) => {
onProgress?.('fetching_session')
return Promise.resolve({ log: [], branch: 'main' })
},
)
const ctx = makeContext(true)
await callTeleport(onDone, ctx, '12345678-abcd-ef01-2345-6789abcdef01')
expect(resumeMockFn).toHaveBeenCalled()
const events = getLoggedEvents()
expect(events).toContain('tengu_teleport_resume_session')
})
test('teleportResumeCodeSession throws not-found error → fires session_not_found_ event', async () => {
teleportResumeMock.mockImplementation(() =>
Promise.reject(new Error('Session not found')),
)
await callTeleport(
onDone,
makeContext(),
'12345678-abcd-ef01-2345-6789abcdef01',
)
const firstArg = onDone.mock.calls[0]?.[0] as string | undefined
expect(firstArg).toMatch(/Teleport failed/)
const events = getLoggedEvents()
expect(events).toContain('tengu_teleport_error_session_not_found_')
})
test('teleportResumeCodeSession throws repo mismatch → fires repo_mismatch event', async () => {
teleportResumeMock.mockImplementation(() =>
Promise.reject(new Error('repo mismatch: expected acme/foo')),
)
await callTeleport(
onDone,
makeContext(),
'12345678-abcd-ef01-2345-6789abcdef01',
)
const events = getLoggedEvents()
expect(events).toContain('tengu_teleport_error_repo_mismatch_sessions_api')
})
test('git dir error → fires tengu_teleport_error_repo_not_in_git_dir_ event', async () => {
teleportResumeMock.mockImplementationOnce(() =>
Promise.reject(new Error('not in git directory: /tmp/test')),
)
await callTeleport(
onDone,
makeContext(),
'12345678-abcd-ef01-2345-6789abcdef01',
)
const events = getLoggedEvents()
expect(events).toContain(
'tengu_teleport_error_repo_not_in_git_dir_sessions_api',
)
})
test('cancelled error → fires tengu_teleport_cancelled event', async () => {
teleportResumeMock.mockImplementationOnce(() =>
Promise.reject(new Error('operation was cancelled')),
)
await callTeleport(
onDone,
makeContext(),
'12345678-abcd-ef01-2345-6789abcdef01',
)
const events = getLoggedEvents()
expect(events).toContain('tengu_teleport_cancelled')
})
test('token/unauthorized error → fires bad_token event', async () => {
teleportResumeMock.mockImplementationOnce(() =>
Promise.reject(new Error('401 unauthorized: bad token')),
)
await callTeleport(
onDone,
makeContext(),
'12345678-abcd-ef01-2345-6789abcdef01',
)
const events = getLoggedEvents()
expect(events).toContain('tengu_teleport_error_bad_token')
})
test('status/4xx error → fires bad_status event', async () => {
teleportResumeMock.mockImplementationOnce(() =>
Promise.reject(new Error('500 internal server error bad status')),
)
await callTeleport(
onDone,
makeContext(),
'12345678-abcd-ef01-2345-6789abcdef01',
)
const events = getLoggedEvents()
expect(events).toContain('tengu_teleport_error_bad_status')
})
test('valid session id without context.resume → fallback message', async () => {
const ctx = makeContext(false) // no resume callback
await callTeleport(onDone, ctx, '12345678-abcd-ef01-2345-6789abcdef01')
const firstArg = onDone.mock.calls[0]?.[0] as string | undefined
expect(firstArg).toMatch(/did not provide a resume callback/)
})
test('valid session id without context.resume + print mode → success message', async () => {
const ctx = makeContext(false)
await callTeleport(
onDone,
ctx,
'--print 12345678-abcd-ef01-2345-6789abcdef01',
)
const firstArg = onDone.mock.calls[0]?.[0] as string | undefined
expect(typeof firstArg).toBe('string')
})
test('log not found after resume → fallback message', async () => {
getLastSessionLogMock.mockImplementation(() =>
Promise.resolve(null as unknown as LogOption),
)
await callTeleport(
onDone,
makeContext(),
'12345678-abcd-ef01-2345-6789abcdef01',
)
const firstArg = onDone.mock.calls[0]?.[0] as string | undefined
expect(firstArg).toMatch(/local log was not found/)
})
})

View File

@@ -1 +0,0 @@
export default { isEnabled: () => false, isHidden: true, name: 'stub' }

View File

@@ -0,0 +1,23 @@
import type { Command } from '../../types/command.js'
const teleport: Command = {
type: 'local-jsx',
name: 'teleport',
// Official v2.1.123 advertises alias `tp` (reverse-engineered from
// claude.exe: `name:"teleport",aliases:["tp"]`). Keeping it for parity.
aliases: ['tp'],
description: 'Resume a Claude Code session from claude.ai',
// REPL markdown renderer strips `<...>` as HTML tags — use uppercase.
argumentHint: 'SESSION_ID',
isHidden: false,
isEnabled: () => true,
bridgeSafe: false,
getBridgeInvocationError: (_args: string) =>
'teleport resumes the REPL and is not bridge-safe',
load: async () => {
const m = await import('./launchTeleport.js')
return { call: m.callTeleport }
},
}
export default teleport

View File

@@ -0,0 +1,314 @@
import type { UUID } from 'node:crypto'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../../services/analytics/index.js'
import type { LocalJSXCommandCall } from '../../types/command.js'
import type { LogOption } from '../../types/logs.js'
import { getLastSessionLog } from '../../utils/sessionStorage.js'
import {
teleportResumeCodeSession,
validateGitState,
} from '../../utils/teleport.js'
import { fetchCodeSessionsFromSessionsAPI } from '../../utils/teleport/api.js'
// Minimum length for a UUID-like session ID (8 hex chars with dashes allowed)
const SESSION_ID_MIN_LENGTH = 8
// Maximum sessions to display in the interactive picker
const PICKER_PAGE_CAP = 20
function meta(
s: string,
): AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS {
return s as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
}
export type TeleportProgressStep =
| 'fetch'
| 'validate'
| 'resume'
| 'ready'
| 'error'
/**
* Formats a sessions list as a text picker (no interactive UI in headless mode).
* Returns a prompt the user can copy a session ID from.
*/
function formatSessionsPicker(
sessions: Array<{
id: string
title: string
status: string
created_at: string
}>,
): string {
const rows = sessions.slice(0, PICKER_PAGE_CAP).map((s, i) => {
const idx = String(i + 1).padStart(2)
const title = s.title.slice(0, 50).padEnd(50)
const status = s.status.padEnd(14)
const created = s.created_at.slice(0, 10)
return ` ${idx}. ${title} ${status} ${created} id=${s.id}`
})
return [
'## Available sessions (most recent first)',
'',
...rows,
'',
'Run `/teleport <session-id>` to resume a session.',
].join('\n')
}
/**
* /teleport [session-id]
*
* Without session-id: fetches the user's session list from the Sessions API
* and renders an interactive picker (or text list in headless mode).
*
* With session-id:
* 1. Validates local git state (must be clean)
* 2. Fetches session logs + branch via teleportResumeCodeSession()
* 3. Looks up the session LogOption by ID
* 4. Hands off to the REPL via context.resume()
*
* Telemetry coverage:
* - tengu_teleport_started
* - tengu_teleport_events_fetch_fail
* - tengu_teleport_page_cap
* - tengu_teleport_source_decision
* - tengu_teleport_resume_session
* - tengu_teleport_first_message_success
* - tengu_teleport_first_message_error
* - tengu_teleport_failed
* - tengu_teleport_cancelled
* - tengu_teleport_null
* - tengu_teleport_errors_detected
* - tengu_teleport_errors_resolved
* - tengu_teleport_error_session_not_found_
* - tengu_teleport_error_repo_mismatch_sessions_api
* - tengu_teleport_error_repo_not_in_git_dir_sessions_api
* - tengu_teleport_error_bad_token
* - tengu_teleport_error_bad_status
*/
export const callTeleport: LocalJSXCommandCall = async (
onDone,
context,
args,
) => {
const rawArgs = args.trim()
// --print flag: headless / non-interactive output
const isPrintMode = rawArgs === '--print' || rawArgs.startsWith('--print ')
const sessionId = isPrintMode
? rawArgs.replace(/^--print\s*/, '').trim()
: rawArgs
logEvent('tengu_teleport_started', {
has_session_id: meta(sessionId ? 'true' : 'false'),
})
// ── No session ID: interactive picker ──
if (!sessionId) {
logEvent('tengu_teleport_source_decision', {
source: meta('sessions_api'),
})
let sessions: Array<{
id: string
title: string
status: string
created_at: string
}>
try {
const raw = await fetchCodeSessionsFromSessionsAPI()
sessions = raw.map(s => ({
id: s.id,
title: s.title ?? 'Untitled',
status: (s.status ?? 'unknown') as string,
created_at: s.created_at ?? '',
}))
} catch (fetchErr: unknown) {
const msg =
fetchErr instanceof Error ? fetchErr.message : String(fetchErr)
if (/forbidden|401|403/i.test(msg)) {
logEvent('tengu_teleport_events_fetch_forbidden', {
error: meta(msg.slice(0, 200)),
})
onDone(
'Teleport: permission denied fetching sessions. Check your OAuth token (`claude auth status`).',
{ display: 'system' },
)
return null
}
if (/not found|404/i.test(msg)) {
logEvent('tengu_teleport_events_fetch_not_found', {
error: meta(msg.slice(0, 200)),
})
onDone(
'Teleport: sessions endpoint returned 404. The Sessions API may not be available for your account.',
{ display: 'system' },
)
return null
}
if (/token|unauthorized/i.test(msg)) {
logEvent('tengu_teleport_error_bad_token', {
error: meta(msg.slice(0, 200)),
})
onDone(
`Teleport: authentication error — ${msg}. Try \`claude auth login\`.`,
{ display: 'system' },
)
return null
}
logEvent('tengu_teleport_events_fetch_fail', {
error: meta(msg.slice(0, 200)),
})
onDone(
`Teleport: failed to fetch sessions — ${msg}.\nUsage: /teleport SESSION_ID`,
{ display: 'system' },
)
return null
}
if (sessions.length === 0) {
logEvent('tengu_teleport_null', {})
onDone(
'No active sessions found on claude.ai/code.\nStart a new session at https://claude.ai/code',
{ display: 'system' },
)
return null
}
if (sessions.length >= PICKER_PAGE_CAP) {
logEvent('tengu_teleport_page_cap', {
count: meta(String(sessions.length)),
})
}
const pickerText = formatSessionsPicker(sessions)
if (isPrintMode) {
onDone(pickerText, { display: 'system' })
return null
}
// Interactive context: display the list and prompt user to run with an ID.
// A full Ink <SelectInput> picker requires an event loop that isn't safely
// available from all command contexts; text list is the portable fallback.
onDone(pickerText, { display: 'system' })
return null
}
// ── Basic format guard ──
if (
sessionId.length < SESSION_ID_MIN_LENGTH ||
!/^[0-9a-f-]{8,}$/i.test(sessionId)
) {
logEvent('tengu_teleport_error_bad_status', {
error: meta(`invalid_session_id: ${sessionId.slice(0, 40)}`),
})
onDone(
`Invalid session id "${sessionId}". Expected a UUID-like string (e.g. 12345678-abcd-...).`,
{ display: 'system' },
)
return null
}
logEvent('tengu_teleport_source_decision', { source: meta('explicit_id') })
// ── Progress tracker (internal, no Ink rendering needed) ──
const steps: TeleportProgressStep[] = []
const recordStep = (step: TeleportProgressStep) => {
steps.push(step)
}
// ── Git state validation ──
recordStep('validate')
try {
await validateGitState()
} catch (gErr: unknown) {
const msg = gErr instanceof Error ? gErr.message : String(gErr)
logEvent('tengu_teleport_errors_detected', {
error: meta(msg.slice(0, 200)),
})
onDone(`Cannot teleport: ${msg}`, { display: 'system' })
return null
}
// ── Resume session ──
recordStep('resume')
try {
let lastProgress = ''
await teleportResumeCodeSession(sessionId, stage => {
lastProgress = String(stage)
})
logEvent('tengu_teleport_resume_session', {
stage: meta(lastProgress),
})
recordStep('ready')
if (!context.resume) {
logEvent('tengu_teleport_null', {})
// resume callback unavailable (e.g. non-interactive context)
if (isPrintMode) {
onDone(`Session ${sessionId} fetched successfully.`, {
display: 'system',
})
return null
}
onDone(
`Teleport resume succeeded for ${sessionId}, but the REPL did not provide a resume callback.`,
{ display: 'system' },
)
return null
}
// Look up the session log so we can pass it to context.resume().
recordStep('fetch')
const log: LogOption | null = await getLastSessionLog(sessionId as UUID)
if (!log) {
logEvent('tengu_teleport_errors_detected', {
error: meta('log_not_found_after_resume'),
})
onDone(
`Teleport fetched session ${sessionId} but the local log was not found. Try /resume ${sessionId} manually.`,
{ display: 'system' },
)
return null
}
logEvent('tengu_teleport_errors_resolved', {})
await context.resume(sessionId as UUID, log, 'slash_command_session_id')
logEvent('tengu_teleport_first_message_success', {})
return null
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err)
// Map error message content to specific telemetry event names
let evt = 'tengu_teleport_failed'
if (/not found/i.test(msg)) {
evt = 'tengu_teleport_error_session_not_found_'
} else if (/repo.*mismatch/i.test(msg)) {
evt = 'tengu_teleport_error_repo_mismatch_sessions_api'
} else if (/not in.*git|git.*dir/i.test(msg)) {
evt = 'tengu_teleport_error_repo_not_in_git_dir_sessions_api'
} else if (/cancelled|aborted/i.test(msg)) {
evt = 'tengu_teleport_cancelled'
} else if (/token|unauthorized|401/i.test(msg)) {
evt = 'tengu_teleport_error_bad_token'
} else if (/status|4\d\d|5\d\d/i.test(msg)) {
evt = 'tengu_teleport_error_bad_status'
}
logEvent(evt, { error: meta(msg.slice(0, 200)) })
logEvent('tengu_teleport_first_message_error', {
error: meta(msg.slice(0, 200)),
})
onDone(`Teleport failed: ${msg}`, { display: 'system' })
return null
}
}