yzcode-cli 1.0.1 → 1.0.3
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/assistant/sessionHistory.ts +87 -0
- package/bootstrap/state.ts +1769 -0
- package/bridge/bridgeApi.ts +539 -0
- package/bridge/bridgeConfig.ts +48 -0
- package/bridge/bridgeDebug.ts +135 -0
- package/bridge/bridgeEnabled.ts +202 -0
- package/bridge/bridgeMain.ts +2999 -0
- package/bridge/bridgeMessaging.ts +461 -0
- package/bridge/bridgePermissionCallbacks.ts +43 -0
- package/bridge/bridgePointer.ts +210 -0
- package/bridge/bridgeStatusUtil.ts +163 -0
- package/bridge/bridgeUI.ts +530 -0
- package/bridge/capacityWake.ts +56 -0
- package/bridge/codeSessionApi.ts +168 -0
- package/bridge/createSession.ts +384 -0
- package/bridge/debugUtils.ts +141 -0
- package/bridge/envLessBridgeConfig.ts +165 -0
- package/bridge/flushGate.ts +71 -0
- package/bridge/inboundAttachments.ts +175 -0
- package/bridge/inboundMessages.ts +80 -0
- package/bridge/initReplBridge.ts +569 -0
- package/bridge/jwtUtils.ts +256 -0
- package/bridge/pollConfig.ts +110 -0
- package/bridge/pollConfigDefaults.ts +82 -0
- package/bridge/remoteBridgeCore.ts +1008 -0
- package/bridge/replBridge.ts +2406 -0
- package/bridge/replBridgeHandle.ts +36 -0
- package/bridge/replBridgeTransport.ts +370 -0
- package/bridge/sessionIdCompat.ts +57 -0
- package/bridge/sessionRunner.ts +550 -0
- package/bridge/trustedDevice.ts +210 -0
- package/bridge/types.ts +262 -0
- package/bridge/workSecret.ts +127 -0
- package/buddy/CompanionSprite.tsx +371 -0
- package/buddy/companion.ts +133 -0
- package/buddy/prompt.ts +36 -0
- package/buddy/sprites.ts +514 -0
- package/buddy/types.ts +148 -0
- package/buddy/useBuddyNotification.tsx +98 -0
- package/coordinator/coordinatorMode.ts +369 -0
- package/memdir/findRelevantMemories.ts +141 -0
- package/memdir/memdir.ts +507 -0
- package/memdir/memoryAge.ts +53 -0
- package/memdir/memoryScan.ts +94 -0
- package/memdir/memoryTypes.ts +271 -0
- package/memdir/paths.ts +278 -0
- package/memdir/teamMemPaths.ts +292 -0
- package/memdir/teamMemPrompts.ts +100 -0
- package/migrations/migrateAutoUpdatesToSettings.ts +61 -0
- package/migrations/migrateBypassPermissionsAcceptedToSettings.ts +40 -0
- package/migrations/migrateEnableAllProjectMcpServersToSettings.ts +118 -0
- package/migrations/migrateFennecToOpus.ts +45 -0
- package/migrations/migrateLegacyOpusToCurrent.ts +57 -0
- package/migrations/migrateOpusToOpus1m.ts +43 -0
- package/migrations/migrateReplBridgeEnabledToRemoteControlAtStartup.ts +22 -0
- package/migrations/migrateSonnet1mToSonnet45.ts +48 -0
- package/migrations/migrateSonnet45ToSonnet46.ts +67 -0
- package/migrations/resetAutoModeOptInForDefaultOffer.ts +51 -0
- package/migrations/resetProToOpusDefault.ts +51 -0
- package/native-ts/color-diff/index.ts +999 -0
- package/native-ts/file-index/index.ts +370 -0
- package/native-ts/yoga-layout/enums.ts +134 -0
- package/native-ts/yoga-layout/index.ts +2578 -0
- package/outputStyles/loadOutputStylesDir.ts +98 -0
- package/package.json +22 -5
- package/plugins/builtinPlugins.ts +159 -0
- package/plugins/bundled/index.ts +23 -0
- package/schemas/hooks.ts +222 -0
- package/screens/Doctor.tsx +575 -0
- package/screens/REPL.tsx +5006 -0
- package/screens/ResumeConversation.tsx +399 -0
- package/server/createDirectConnectSession.ts +88 -0
- package/server/directConnectManager.ts +213 -0
- package/server/types.ts +57 -0
- package/skills/bundled/batch.ts +124 -0
- package/skills/bundled/claudeApi.ts +196 -0
- package/skills/bundled/claudeApiContent.ts +75 -0
- package/skills/bundled/claudeInChrome.ts +34 -0
- package/skills/bundled/debug.ts +103 -0
- package/skills/bundled/index.ts +79 -0
- package/skills/bundled/keybindings.ts +339 -0
- package/skills/bundled/loop.ts +92 -0
- package/skills/bundled/loremIpsum.ts +282 -0
- package/skills/bundled/remember.ts +82 -0
- package/skills/bundled/scheduleRemoteAgents.ts +447 -0
- package/skills/bundled/simplify.ts +69 -0
- package/skills/bundled/skillify.ts +197 -0
- package/skills/bundled/stuck.ts +79 -0
- package/skills/bundled/updateConfig.ts +475 -0
- package/skills/bundled/verify/SKILL.md +3 -0
- package/skills/bundled/verify/examples/cli.md +3 -0
- package/skills/bundled/verify/examples/server.md +3 -0
- package/skills/bundled/verify.ts +30 -0
- package/skills/bundled/verifyContent.ts +13 -0
- package/skills/bundledSkills.ts +220 -0
- package/skills/loadSkillsDir.ts +1086 -0
- package/skills/mcpSkillBuilders.ts +44 -0
- package/tasks/DreamTask/DreamTask.ts +157 -0
- package/tasks/InProcessTeammateTask/InProcessTeammateTask.tsx +126 -0
- package/tasks/InProcessTeammateTask/types.ts +121 -0
- package/tasks/LocalAgentTask/LocalAgentTask.tsx +683 -0
- package/tasks/LocalMainSessionTask.ts +479 -0
- package/tasks/LocalShellTask/LocalShellTask.tsx +523 -0
- package/tasks/LocalShellTask/guards.ts +41 -0
- package/tasks/LocalShellTask/killShellTasks.ts +76 -0
- package/tasks/RemoteAgentTask/RemoteAgentTask.tsx +856 -0
- package/tasks/pillLabel.ts +82 -0
- package/tasks/stopTask.ts +100 -0
- package/tasks/types.ts +46 -0
- package/upstreamproxy/relay.ts +455 -0
- package/upstreamproxy/upstreamproxy.ts +285 -0
- package/vim/motions.ts +82 -0
- package/vim/operators.ts +556 -0
- package/vim/textObjects.ts +186 -0
- package/vim/transitions.ts +490 -0
- package/vim/types.ts +199 -0
- package/voice/voiceModeEnabled.ts +54 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin HTTP wrappers for the CCR v2 code-session API.
|
|
3
|
+
*
|
|
4
|
+
* Separate file from remoteBridgeCore.ts so the SDK /bridge subpath can
|
|
5
|
+
* export createCodeSession + fetchRemoteCredentials without bundling the
|
|
6
|
+
* heavy CLI tree (analytics, transport, etc.). Callers supply explicit
|
|
7
|
+
* accessToken + baseUrl — no implicit auth or config reads.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import axios from 'axios'
|
|
11
|
+
import { logForDebugging } from '../utils/debug.js'
|
|
12
|
+
import { errorMessage } from '../utils/errors.js'
|
|
13
|
+
import { jsonStringify } from '../utils/slowOperations.js'
|
|
14
|
+
import { extractErrorDetail } from './debugUtils.js'
|
|
15
|
+
|
|
16
|
+
const ANTHROPIC_VERSION = '2023-06-01'
|
|
17
|
+
|
|
18
|
+
function oauthHeaders(accessToken: string): Record<string, string> {
|
|
19
|
+
return {
|
|
20
|
+
Authorization: `Bearer ${accessToken}`,
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
'anthropic-version': ANTHROPIC_VERSION,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function createCodeSession(
|
|
27
|
+
baseUrl: string,
|
|
28
|
+
accessToken: string,
|
|
29
|
+
title: string,
|
|
30
|
+
timeoutMs: number,
|
|
31
|
+
tags?: string[],
|
|
32
|
+
): Promise<string | null> {
|
|
33
|
+
const url = `${baseUrl}/v1/code/sessions`
|
|
34
|
+
let response
|
|
35
|
+
try {
|
|
36
|
+
response = await axios.post(
|
|
37
|
+
url,
|
|
38
|
+
// bridge: {} is the positive signal for the oneof runner — omitting it
|
|
39
|
+
// (or sending environment_id: "") now 400s. BridgeRunner is an empty
|
|
40
|
+
// message today; it's a placeholder for future bridge-specific options.
|
|
41
|
+
{ title, bridge: {}, ...(tags?.length ? { tags } : {}) },
|
|
42
|
+
{
|
|
43
|
+
headers: oauthHeaders(accessToken),
|
|
44
|
+
timeout: timeoutMs,
|
|
45
|
+
validateStatus: s => s < 500,
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
} catch (err: unknown) {
|
|
49
|
+
logForDebugging(
|
|
50
|
+
`[code-session] Session create request failed: ${errorMessage(err)}`,
|
|
51
|
+
)
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (response.status !== 200 && response.status !== 201) {
|
|
56
|
+
const detail = extractErrorDetail(response.data)
|
|
57
|
+
logForDebugging(
|
|
58
|
+
`[code-session] Session create failed ${response.status}${detail ? `: ${detail}` : ''}`,
|
|
59
|
+
)
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const data: unknown = response.data
|
|
64
|
+
if (
|
|
65
|
+
!data ||
|
|
66
|
+
typeof data !== 'object' ||
|
|
67
|
+
!('session' in data) ||
|
|
68
|
+
!data.session ||
|
|
69
|
+
typeof data.session !== 'object' ||
|
|
70
|
+
!('id' in data.session) ||
|
|
71
|
+
typeof data.session.id !== 'string' ||
|
|
72
|
+
!data.session.id.startsWith('cse_')
|
|
73
|
+
) {
|
|
74
|
+
logForDebugging(
|
|
75
|
+
`[code-session] No session.id (cse_*) in response: ${jsonStringify(data).slice(0, 200)}`,
|
|
76
|
+
)
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
return data.session.id
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Credentials from POST /bridge. JWT is opaque — do not decode.
|
|
84
|
+
* Each /bridge call bumps worker_epoch server-side (it IS the register).
|
|
85
|
+
*/
|
|
86
|
+
export type RemoteCredentials = {
|
|
87
|
+
worker_jwt: string
|
|
88
|
+
api_base_url: string
|
|
89
|
+
expires_in: number
|
|
90
|
+
worker_epoch: number
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function fetchRemoteCredentials(
|
|
94
|
+
sessionId: string,
|
|
95
|
+
baseUrl: string,
|
|
96
|
+
accessToken: string,
|
|
97
|
+
timeoutMs: number,
|
|
98
|
+
trustedDeviceToken?: string,
|
|
99
|
+
): Promise<RemoteCredentials | null> {
|
|
100
|
+
const url = `${baseUrl}/v1/code/sessions/${sessionId}/bridge`
|
|
101
|
+
const headers = oauthHeaders(accessToken)
|
|
102
|
+
if (trustedDeviceToken) {
|
|
103
|
+
headers['X-Trusted-Device-Token'] = trustedDeviceToken
|
|
104
|
+
}
|
|
105
|
+
let response
|
|
106
|
+
try {
|
|
107
|
+
response = await axios.post(
|
|
108
|
+
url,
|
|
109
|
+
{},
|
|
110
|
+
{
|
|
111
|
+
headers,
|
|
112
|
+
timeout: timeoutMs,
|
|
113
|
+
validateStatus: s => s < 500,
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
} catch (err: unknown) {
|
|
117
|
+
logForDebugging(
|
|
118
|
+
`[code-session] /bridge request failed: ${errorMessage(err)}`,
|
|
119
|
+
)
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (response.status !== 200) {
|
|
124
|
+
const detail = extractErrorDetail(response.data)
|
|
125
|
+
logForDebugging(
|
|
126
|
+
`[code-session] /bridge failed ${response.status}${detail ? `: ${detail}` : ''}`,
|
|
127
|
+
)
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const data: unknown = response.data
|
|
132
|
+
if (
|
|
133
|
+
data === null ||
|
|
134
|
+
typeof data !== 'object' ||
|
|
135
|
+
!('worker_jwt' in data) ||
|
|
136
|
+
typeof data.worker_jwt !== 'string' ||
|
|
137
|
+
!('expires_in' in data) ||
|
|
138
|
+
typeof data.expires_in !== 'number' ||
|
|
139
|
+
!('api_base_url' in data) ||
|
|
140
|
+
typeof data.api_base_url !== 'string' ||
|
|
141
|
+
!('worker_epoch' in data)
|
|
142
|
+
) {
|
|
143
|
+
logForDebugging(
|
|
144
|
+
`[code-session] /bridge response malformed (need worker_jwt, expires_in, api_base_url, worker_epoch): ${jsonStringify(data).slice(0, 200)}`,
|
|
145
|
+
)
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
// protojson serializes int64 as a string to avoid JS precision loss;
|
|
149
|
+
// Go may also return a number depending on encoder settings.
|
|
150
|
+
const rawEpoch = data.worker_epoch
|
|
151
|
+
const epoch = typeof rawEpoch === 'string' ? Number(rawEpoch) : rawEpoch
|
|
152
|
+
if (
|
|
153
|
+
typeof epoch !== 'number' ||
|
|
154
|
+
!Number.isFinite(epoch) ||
|
|
155
|
+
!Number.isSafeInteger(epoch)
|
|
156
|
+
) {
|
|
157
|
+
logForDebugging(
|
|
158
|
+
`[code-session] /bridge worker_epoch invalid: ${jsonStringify(rawEpoch)}`,
|
|
159
|
+
)
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
worker_jwt: data.worker_jwt,
|
|
164
|
+
api_base_url: data.api_base_url,
|
|
165
|
+
expires_in: data.expires_in,
|
|
166
|
+
worker_epoch: epoch,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import type { SDKMessage } from '../entrypoints/agentSdkTypes.js'
|
|
2
|
+
import { logForDebugging } from '../utils/debug.js'
|
|
3
|
+
import { errorMessage } from '../utils/errors.js'
|
|
4
|
+
import { extractErrorDetail } from './debugUtils.js'
|
|
5
|
+
import { toCompatSessionId } from './sessionIdCompat.js'
|
|
6
|
+
|
|
7
|
+
type GitSource = {
|
|
8
|
+
type: 'git_repository'
|
|
9
|
+
url: string
|
|
10
|
+
revision?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type GitOutcome = {
|
|
14
|
+
type: 'git_repository'
|
|
15
|
+
git_info: { type: 'github'; repo: string; branches: string[] }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Events must be wrapped in { type: 'event', data: <sdk_message> } for the
|
|
19
|
+
// POST /v1/sessions endpoint (discriminated union format).
|
|
20
|
+
type SessionEvent = {
|
|
21
|
+
type: 'event'
|
|
22
|
+
data: SDKMessage
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a session on a bridge environment via POST /v1/sessions.
|
|
27
|
+
*
|
|
28
|
+
* Used by both `claude remote-control` (empty session so the user has somewhere to
|
|
29
|
+
* type immediately) and `/remote-control` (session pre-populated with conversation
|
|
30
|
+
* history).
|
|
31
|
+
*
|
|
32
|
+
* Returns the session ID on success, or null if creation fails (non-fatal).
|
|
33
|
+
*/
|
|
34
|
+
export async function createBridgeSession({
|
|
35
|
+
environmentId,
|
|
36
|
+
title,
|
|
37
|
+
events,
|
|
38
|
+
gitRepoUrl,
|
|
39
|
+
branch,
|
|
40
|
+
signal,
|
|
41
|
+
baseUrl: baseUrlOverride,
|
|
42
|
+
getAccessToken,
|
|
43
|
+
permissionMode,
|
|
44
|
+
}: {
|
|
45
|
+
environmentId: string
|
|
46
|
+
title?: string
|
|
47
|
+
events: SessionEvent[]
|
|
48
|
+
gitRepoUrl: string | null
|
|
49
|
+
branch: string
|
|
50
|
+
signal: AbortSignal
|
|
51
|
+
baseUrl?: string
|
|
52
|
+
getAccessToken?: () => string | undefined
|
|
53
|
+
permissionMode?: string
|
|
54
|
+
}): Promise<string | null> {
|
|
55
|
+
const { getClaudeAIOAuthTokens } = await import('../utils/auth.js')
|
|
56
|
+
const { getOrganizationUUID } = await import('../services/oauth/client.js')
|
|
57
|
+
const { getOauthConfig } = await import('../constants/oauth.js')
|
|
58
|
+
const { getOAuthHeaders } = await import('../utils/teleport/api.js')
|
|
59
|
+
const { parseGitHubRepository } = await import('../utils/detectRepository.js')
|
|
60
|
+
const { getDefaultBranch } = await import('../utils/git.js')
|
|
61
|
+
const { getMainLoopModel } = await import('../utils/model/model.js')
|
|
62
|
+
const { default: axios } = await import('axios')
|
|
63
|
+
|
|
64
|
+
const accessToken =
|
|
65
|
+
getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken
|
|
66
|
+
if (!accessToken) {
|
|
67
|
+
logForDebugging('[bridge] No access token for session creation')
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const orgUUID = await getOrganizationUUID()
|
|
72
|
+
if (!orgUUID) {
|
|
73
|
+
logForDebugging('[bridge] No org UUID for session creation')
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Build git source and outcome context
|
|
78
|
+
let gitSource: GitSource | null = null
|
|
79
|
+
let gitOutcome: GitOutcome | null = null
|
|
80
|
+
|
|
81
|
+
if (gitRepoUrl) {
|
|
82
|
+
const { parseGitRemote } = await import('../utils/detectRepository.js')
|
|
83
|
+
const parsed = parseGitRemote(gitRepoUrl)
|
|
84
|
+
if (parsed) {
|
|
85
|
+
const { host, owner, name } = parsed
|
|
86
|
+
const revision = branch || (await getDefaultBranch()) || undefined
|
|
87
|
+
gitSource = {
|
|
88
|
+
type: 'git_repository',
|
|
89
|
+
url: `https://${host}/${owner}/${name}`,
|
|
90
|
+
revision,
|
|
91
|
+
}
|
|
92
|
+
gitOutcome = {
|
|
93
|
+
type: 'git_repository',
|
|
94
|
+
git_info: {
|
|
95
|
+
type: 'github',
|
|
96
|
+
repo: `${owner}/${name}`,
|
|
97
|
+
branches: [`claude/${branch || 'task'}`],
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
// Fallback: try parseGitHubRepository for owner/repo format
|
|
102
|
+
const ownerRepo = parseGitHubRepository(gitRepoUrl)
|
|
103
|
+
if (ownerRepo) {
|
|
104
|
+
const [owner, name] = ownerRepo.split('/')
|
|
105
|
+
if (owner && name) {
|
|
106
|
+
const revision = branch || (await getDefaultBranch()) || undefined
|
|
107
|
+
gitSource = {
|
|
108
|
+
type: 'git_repository',
|
|
109
|
+
url: `https://github.com/${owner}/${name}`,
|
|
110
|
+
revision,
|
|
111
|
+
}
|
|
112
|
+
gitOutcome = {
|
|
113
|
+
type: 'git_repository',
|
|
114
|
+
git_info: {
|
|
115
|
+
type: 'github',
|
|
116
|
+
repo: `${owner}/${name}`,
|
|
117
|
+
branches: [`claude/${branch || 'task'}`],
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const requestBody = {
|
|
126
|
+
...(title !== undefined && { title }),
|
|
127
|
+
events,
|
|
128
|
+
session_context: {
|
|
129
|
+
sources: gitSource ? [gitSource] : [],
|
|
130
|
+
outcomes: gitOutcome ? [gitOutcome] : [],
|
|
131
|
+
model: getMainLoopModel(),
|
|
132
|
+
},
|
|
133
|
+
environment_id: environmentId,
|
|
134
|
+
source: 'remote-control',
|
|
135
|
+
...(permissionMode && { permission_mode: permissionMode }),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const headers = {
|
|
139
|
+
...getOAuthHeaders(accessToken),
|
|
140
|
+
'anthropic-beta': 'ccr-byoc-2025-07-29',
|
|
141
|
+
'x-organization-uuid': orgUUID,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const url = `${baseUrlOverride ?? getOauthConfig().BASE_API_URL}/v1/sessions`
|
|
145
|
+
let response
|
|
146
|
+
try {
|
|
147
|
+
response = await axios.post(url, requestBody, {
|
|
148
|
+
headers,
|
|
149
|
+
signal,
|
|
150
|
+
validateStatus: s => s < 500,
|
|
151
|
+
})
|
|
152
|
+
} catch (err: unknown) {
|
|
153
|
+
logForDebugging(
|
|
154
|
+
`[bridge] Session creation request failed: ${errorMessage(err)}`,
|
|
155
|
+
)
|
|
156
|
+
return null
|
|
157
|
+
}
|
|
158
|
+
const isSuccess = response.status === 200 || response.status === 201
|
|
159
|
+
|
|
160
|
+
if (!isSuccess) {
|
|
161
|
+
const detail = extractErrorDetail(response.data)
|
|
162
|
+
logForDebugging(
|
|
163
|
+
`[bridge] Session creation failed with status ${response.status}${detail ? `: ${detail}` : ''}`,
|
|
164
|
+
)
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const sessionData: unknown = response.data
|
|
169
|
+
if (
|
|
170
|
+
!sessionData ||
|
|
171
|
+
typeof sessionData !== 'object' ||
|
|
172
|
+
!('id' in sessionData) ||
|
|
173
|
+
typeof sessionData.id !== 'string'
|
|
174
|
+
) {
|
|
175
|
+
logForDebugging('[bridge] No session ID in response')
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return sessionData.id
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Fetch a bridge session via GET /v1/sessions/{id}.
|
|
184
|
+
*
|
|
185
|
+
* Returns the session's environment_id (for `--session-id` resume) and title.
|
|
186
|
+
* Uses the same org-scoped headers as create/archive — the environments-level
|
|
187
|
+
* client in bridgeApi.ts uses a different beta header and no org UUID, which
|
|
188
|
+
* makes the Sessions API return 404.
|
|
189
|
+
*/
|
|
190
|
+
export async function getBridgeSession(
|
|
191
|
+
sessionId: string,
|
|
192
|
+
opts?: { baseUrl?: string; getAccessToken?: () => string | undefined },
|
|
193
|
+
): Promise<{ environment_id?: string; title?: string } | null> {
|
|
194
|
+
const { getClaudeAIOAuthTokens } = await import('../utils/auth.js')
|
|
195
|
+
const { getOrganizationUUID } = await import('../services/oauth/client.js')
|
|
196
|
+
const { getOauthConfig } = await import('../constants/oauth.js')
|
|
197
|
+
const { getOAuthHeaders } = await import('../utils/teleport/api.js')
|
|
198
|
+
const { default: axios } = await import('axios')
|
|
199
|
+
|
|
200
|
+
const accessToken =
|
|
201
|
+
opts?.getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken
|
|
202
|
+
if (!accessToken) {
|
|
203
|
+
logForDebugging('[bridge] No access token for session fetch')
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const orgUUID = await getOrganizationUUID()
|
|
208
|
+
if (!orgUUID) {
|
|
209
|
+
logForDebugging('[bridge] No org UUID for session fetch')
|
|
210
|
+
return null
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const headers = {
|
|
214
|
+
...getOAuthHeaders(accessToken),
|
|
215
|
+
'anthropic-beta': 'ccr-byoc-2025-07-29',
|
|
216
|
+
'x-organization-uuid': orgUUID,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const url = `${opts?.baseUrl ?? getOauthConfig().BASE_API_URL}/v1/sessions/${sessionId}`
|
|
220
|
+
logForDebugging(`[bridge] Fetching session ${sessionId}`)
|
|
221
|
+
|
|
222
|
+
let response
|
|
223
|
+
try {
|
|
224
|
+
response = await axios.get<{ environment_id?: string; title?: string }>(
|
|
225
|
+
url,
|
|
226
|
+
{ headers, timeout: 10_000, validateStatus: s => s < 500 },
|
|
227
|
+
)
|
|
228
|
+
} catch (err: unknown) {
|
|
229
|
+
logForDebugging(
|
|
230
|
+
`[bridge] Session fetch request failed: ${errorMessage(err)}`,
|
|
231
|
+
)
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (response.status !== 200) {
|
|
236
|
+
const detail = extractErrorDetail(response.data)
|
|
237
|
+
logForDebugging(
|
|
238
|
+
`[bridge] Session fetch failed with status ${response.status}${detail ? `: ${detail}` : ''}`,
|
|
239
|
+
)
|
|
240
|
+
return null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return response.data
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Archive a bridge session via POST /v1/sessions/{id}/archive.
|
|
248
|
+
*
|
|
249
|
+
* The CCR server never auto-archives sessions — archival is always an
|
|
250
|
+
* explicit client action. Both `claude remote-control` (standalone bridge) and the
|
|
251
|
+
* always-on `/remote-control` REPL bridge call this during shutdown to archive any
|
|
252
|
+
* sessions that are still alive.
|
|
253
|
+
*
|
|
254
|
+
* The archive endpoint accepts sessions in any status (running, idle,
|
|
255
|
+
* requires_action, pending) and returns 409 if already archived, making
|
|
256
|
+
* it safe to call even if the server-side runner already archived the
|
|
257
|
+
* session.
|
|
258
|
+
*
|
|
259
|
+
* Callers must handle errors — this function has no try/catch; 5xx,
|
|
260
|
+
* timeouts, and network errors throw. Archival is best-effort during
|
|
261
|
+
* cleanup; call sites wrap with .catch().
|
|
262
|
+
*/
|
|
263
|
+
export async function archiveBridgeSession(
|
|
264
|
+
sessionId: string,
|
|
265
|
+
opts?: {
|
|
266
|
+
baseUrl?: string
|
|
267
|
+
getAccessToken?: () => string | undefined
|
|
268
|
+
timeoutMs?: number
|
|
269
|
+
},
|
|
270
|
+
): Promise<void> {
|
|
271
|
+
const { getClaudeAIOAuthTokens } = await import('../utils/auth.js')
|
|
272
|
+
const { getOrganizationUUID } = await import('../services/oauth/client.js')
|
|
273
|
+
const { getOauthConfig } = await import('../constants/oauth.js')
|
|
274
|
+
const { getOAuthHeaders } = await import('../utils/teleport/api.js')
|
|
275
|
+
const { default: axios } = await import('axios')
|
|
276
|
+
|
|
277
|
+
const accessToken =
|
|
278
|
+
opts?.getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken
|
|
279
|
+
if (!accessToken) {
|
|
280
|
+
logForDebugging('[bridge] No access token for session archive')
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const orgUUID = await getOrganizationUUID()
|
|
285
|
+
if (!orgUUID) {
|
|
286
|
+
logForDebugging('[bridge] No org UUID for session archive')
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const headers = {
|
|
291
|
+
...getOAuthHeaders(accessToken),
|
|
292
|
+
'anthropic-beta': 'ccr-byoc-2025-07-29',
|
|
293
|
+
'x-organization-uuid': orgUUID,
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const url = `${opts?.baseUrl ?? getOauthConfig().BASE_API_URL}/v1/sessions/${sessionId}/archive`
|
|
297
|
+
logForDebugging(`[bridge] Archiving session ${sessionId}`)
|
|
298
|
+
|
|
299
|
+
const response = await axios.post(
|
|
300
|
+
url,
|
|
301
|
+
{},
|
|
302
|
+
{
|
|
303
|
+
headers,
|
|
304
|
+
timeout: opts?.timeoutMs ?? 10_000,
|
|
305
|
+
validateStatus: s => s < 500,
|
|
306
|
+
},
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
if (response.status === 200) {
|
|
310
|
+
logForDebugging(`[bridge] Session ${sessionId} archived successfully`)
|
|
311
|
+
} else {
|
|
312
|
+
const detail = extractErrorDetail(response.data)
|
|
313
|
+
logForDebugging(
|
|
314
|
+
`[bridge] Session archive failed with status ${response.status}${detail ? `: ${detail}` : ''}`,
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Update the title of a bridge session via PATCH /v1/sessions/{id}.
|
|
321
|
+
*
|
|
322
|
+
* Called when the user renames a session via /rename while a bridge
|
|
323
|
+
* connection is active, so the title stays in sync on claude.ai/code.
|
|
324
|
+
*
|
|
325
|
+
* Errors are swallowed — title sync is best-effort.
|
|
326
|
+
*/
|
|
327
|
+
export async function updateBridgeSessionTitle(
|
|
328
|
+
sessionId: string,
|
|
329
|
+
title: string,
|
|
330
|
+
opts?: { baseUrl?: string; getAccessToken?: () => string | undefined },
|
|
331
|
+
): Promise<void> {
|
|
332
|
+
const { getClaudeAIOAuthTokens } = await import('../utils/auth.js')
|
|
333
|
+
const { getOrganizationUUID } = await import('../services/oauth/client.js')
|
|
334
|
+
const { getOauthConfig } = await import('../constants/oauth.js')
|
|
335
|
+
const { getOAuthHeaders } = await import('../utils/teleport/api.js')
|
|
336
|
+
const { default: axios } = await import('axios')
|
|
337
|
+
|
|
338
|
+
const accessToken =
|
|
339
|
+
opts?.getAccessToken?.() ?? getClaudeAIOAuthTokens()?.accessToken
|
|
340
|
+
if (!accessToken) {
|
|
341
|
+
logForDebugging('[bridge] No access token for session title update')
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const orgUUID = await getOrganizationUUID()
|
|
346
|
+
if (!orgUUID) {
|
|
347
|
+
logForDebugging('[bridge] No org UUID for session title update')
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const headers = {
|
|
352
|
+
...getOAuthHeaders(accessToken),
|
|
353
|
+
'anthropic-beta': 'ccr-byoc-2025-07-29',
|
|
354
|
+
'x-organization-uuid': orgUUID,
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Compat gateway only accepts session_* (compat/convert.go:27). v2 callers
|
|
358
|
+
// pass raw cse_*; retag here so all callers can pass whatever they hold.
|
|
359
|
+
// Idempotent for v1's session_* and bridgeMain's pre-converted compatSessionId.
|
|
360
|
+
const compatId = toCompatSessionId(sessionId)
|
|
361
|
+
const url = `${opts?.baseUrl ?? getOauthConfig().BASE_API_URL}/v1/sessions/${compatId}`
|
|
362
|
+
logForDebugging(`[bridge] Updating session title: ${compatId} → ${title}`)
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const response = await axios.patch(
|
|
366
|
+
url,
|
|
367
|
+
{ title },
|
|
368
|
+
{ headers, timeout: 10_000, validateStatus: s => s < 500 },
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
if (response.status === 200) {
|
|
372
|
+
logForDebugging(`[bridge] Session title updated successfully`)
|
|
373
|
+
} else {
|
|
374
|
+
const detail = extractErrorDetail(response.data)
|
|
375
|
+
logForDebugging(
|
|
376
|
+
`[bridge] Session title update failed with status ${response.status}${detail ? `: ${detail}` : ''}`,
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
} catch (err: unknown) {
|
|
380
|
+
logForDebugging(
|
|
381
|
+
`[bridge] Session title update request failed: ${errorMessage(err)}`,
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
3
|
+
logEvent,
|
|
4
|
+
} from '../services/analytics/index.js'
|
|
5
|
+
import { logForDebugging } from '../utils/debug.js'
|
|
6
|
+
import { errorMessage } from '../utils/errors.js'
|
|
7
|
+
import { jsonStringify } from '../utils/slowOperations.js'
|
|
8
|
+
|
|
9
|
+
const DEBUG_MSG_LIMIT = 2000
|
|
10
|
+
|
|
11
|
+
const SECRET_FIELD_NAMES = [
|
|
12
|
+
'session_ingress_token',
|
|
13
|
+
'environment_secret',
|
|
14
|
+
'access_token',
|
|
15
|
+
'secret',
|
|
16
|
+
'token',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
const SECRET_PATTERN = new RegExp(
|
|
20
|
+
`"(${SECRET_FIELD_NAMES.join('|')})"\\s*:\\s*"([^"]*)"`,
|
|
21
|
+
'g',
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
const REDACT_MIN_LENGTH = 16
|
|
25
|
+
|
|
26
|
+
export function redactSecrets(s: string): string {
|
|
27
|
+
return s.replace(SECRET_PATTERN, (_match, field: string, value: string) => {
|
|
28
|
+
if (value.length < REDACT_MIN_LENGTH) {
|
|
29
|
+
return `"${field}":"[REDACTED]"`
|
|
30
|
+
}
|
|
31
|
+
const redacted = `${value.slice(0, 8)}...${value.slice(-4)}`
|
|
32
|
+
return `"${field}":"${redacted}"`
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Truncate a string for debug logging, collapsing newlines. */
|
|
37
|
+
export function debugTruncate(s: string): string {
|
|
38
|
+
const flat = s.replace(/\n/g, '\\n')
|
|
39
|
+
if (flat.length <= DEBUG_MSG_LIMIT) {
|
|
40
|
+
return flat
|
|
41
|
+
}
|
|
42
|
+
return flat.slice(0, DEBUG_MSG_LIMIT) + `... (${flat.length} chars)`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Truncate a JSON-serializable value for debug logging. */
|
|
46
|
+
export function debugBody(data: unknown): string {
|
|
47
|
+
const raw = typeof data === 'string' ? data : jsonStringify(data)
|
|
48
|
+
const s = redactSecrets(raw)
|
|
49
|
+
if (s.length <= DEBUG_MSG_LIMIT) {
|
|
50
|
+
return s
|
|
51
|
+
}
|
|
52
|
+
return s.slice(0, DEBUG_MSG_LIMIT) + `... (${s.length} chars)`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extract a descriptive error message from an axios error (or any error).
|
|
57
|
+
* For HTTP errors, appends the server's response body message if available,
|
|
58
|
+
* since axios's default message only includes the status code.
|
|
59
|
+
*/
|
|
60
|
+
export function describeAxiosError(err: unknown): string {
|
|
61
|
+
const msg = errorMessage(err)
|
|
62
|
+
if (err && typeof err === 'object' && 'response' in err) {
|
|
63
|
+
const response = (err as { response?: { data?: unknown } }).response
|
|
64
|
+
if (response?.data && typeof response.data === 'object') {
|
|
65
|
+
const data = response.data as Record<string, unknown>
|
|
66
|
+
const detail =
|
|
67
|
+
typeof data.message === 'string'
|
|
68
|
+
? data.message
|
|
69
|
+
: typeof data.error === 'object' &&
|
|
70
|
+
data.error &&
|
|
71
|
+
'message' in data.error &&
|
|
72
|
+
typeof (data.error as Record<string, unknown>).message ===
|
|
73
|
+
'string'
|
|
74
|
+
? (data.error as Record<string, unknown>).message
|
|
75
|
+
: undefined
|
|
76
|
+
if (detail) {
|
|
77
|
+
return `${msg}: ${detail}`
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return msg
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Extract the HTTP status code from an axios error, if present.
|
|
86
|
+
* Returns undefined for non-HTTP errors (e.g. network failures).
|
|
87
|
+
*/
|
|
88
|
+
export function extractHttpStatus(err: unknown): number | undefined {
|
|
89
|
+
if (
|
|
90
|
+
err &&
|
|
91
|
+
typeof err === 'object' &&
|
|
92
|
+
'response' in err &&
|
|
93
|
+
(err as { response?: { status?: unknown } }).response &&
|
|
94
|
+
typeof (err as { response: { status?: unknown } }).response.status ===
|
|
95
|
+
'number'
|
|
96
|
+
) {
|
|
97
|
+
return (err as { response: { status: number } }).response.status
|
|
98
|
+
}
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Pull a human-readable message out of an API error response body.
|
|
104
|
+
* Checks `data.message` first, then `data.error.message`.
|
|
105
|
+
*/
|
|
106
|
+
export function extractErrorDetail(data: unknown): string | undefined {
|
|
107
|
+
if (!data || typeof data !== 'object') return undefined
|
|
108
|
+
if ('message' in data && typeof data.message === 'string') {
|
|
109
|
+
return data.message
|
|
110
|
+
}
|
|
111
|
+
if (
|
|
112
|
+
'error' in data &&
|
|
113
|
+
data.error !== null &&
|
|
114
|
+
typeof data.error === 'object' &&
|
|
115
|
+
'message' in data.error &&
|
|
116
|
+
typeof data.error.message === 'string'
|
|
117
|
+
) {
|
|
118
|
+
return data.error.message
|
|
119
|
+
}
|
|
120
|
+
return undefined
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Log a bridge init skip — debug message + `tengu_bridge_repl_skipped`
|
|
125
|
+
* analytics event. Centralizes the event name and the AnalyticsMetadata
|
|
126
|
+
* cast so call sites don't each repeat the 5-line boilerplate.
|
|
127
|
+
*/
|
|
128
|
+
export function logBridgeSkip(
|
|
129
|
+
reason: string,
|
|
130
|
+
debugMsg?: string,
|
|
131
|
+
v2?: boolean,
|
|
132
|
+
): void {
|
|
133
|
+
if (debugMsg) {
|
|
134
|
+
logForDebugging(debugMsg)
|
|
135
|
+
}
|
|
136
|
+
logEvent('tengu_bridge_repl_skipped', {
|
|
137
|
+
reason:
|
|
138
|
+
reason as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
139
|
+
...(v2 !== undefined && { v2 }),
|
|
140
|
+
})
|
|
141
|
+
}
|