mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
feat: 添加服务层增强与零散改进
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
59
src/assistant/__tests__/index.test.ts
Normal file
59
src/assistant/__tests__/index.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import { readFile, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
resetStateForTests,
|
||||
setCwdState,
|
||||
setOriginalCwd,
|
||||
} from '../../bootstrap/state'
|
||||
import { getTaskListId } from '../../utils/tasks'
|
||||
import { getTeamFilePath } from '../../utils/swarm/teamHelpers'
|
||||
import { initializeAssistantTeam } from '../index'
|
||||
|
||||
let tempDir = ''
|
||||
let previousConfigDir: string | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
previousConfigDir = process.env.CLAUDE_CONFIG_DIR
|
||||
tempDir = join(
|
||||
tmpdir(),
|
||||
`assistant-team-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||
)
|
||||
process.env.CLAUDE_CONFIG_DIR = join(tempDir, 'config')
|
||||
resetStateForTests()
|
||||
setOriginalCwd(tempDir)
|
||||
setCwdState(tempDir)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
resetStateForTests()
|
||||
if (previousConfigDir === undefined) {
|
||||
delete process.env.CLAUDE_CONFIG_DIR
|
||||
} else {
|
||||
process.env.CLAUDE_CONFIG_DIR = previousConfigDir
|
||||
}
|
||||
await rm(tempDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('initializeAssistantTeam', () => {
|
||||
test('creates a session-scoped in-process team context and task list', async () => {
|
||||
const context = await initializeAssistantTeam()
|
||||
expect(context).toBeDefined()
|
||||
const teamContext = context!
|
||||
|
||||
expect(teamContext.teamName).toStartWith('assistant-')
|
||||
expect(teamContext.isLeader).toBe(true)
|
||||
expect(teamContext.selfAgentName).toBe('team-lead')
|
||||
expect(
|
||||
teamContext.teammates[teamContext.leadAgentId]?.tmuxSessionName,
|
||||
).toBe('in-process')
|
||||
expect(getTaskListId()).toBe(teamContext.teamName)
|
||||
|
||||
const raw = await readFile(getTeamFilePath(teamContext.teamName), 'utf-8')
|
||||
const teamFile = JSON.parse(raw)
|
||||
expect(teamFile.leadAgentId).toBe(teamContext.leadAgentId)
|
||||
expect(teamFile.members[0].backendType).toBe('in-process')
|
||||
expect(teamFile.members[0].agentType).toBe('assistant')
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,24 @@
|
||||
import { readFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { getKairosActive } from '../bootstrap/state.js'
|
||||
import { getKairosActive, getSessionId } from '../bootstrap/state.js'
|
||||
import type { AppState } from '../state/AppState.js'
|
||||
import { formatAgentId } from '../utils/agentId.js'
|
||||
import { getCwd } from '../utils/cwd.js'
|
||||
import { getClaudeConfigHomeDir } from '../utils/envUtils.js'
|
||||
import { TEAM_LEAD_NAME } from '../utils/swarm/constants.js'
|
||||
import {
|
||||
getTeamFilePath,
|
||||
registerTeamForSessionCleanup,
|
||||
sanitizeName,
|
||||
writeTeamFileAsync,
|
||||
type TeamFile,
|
||||
} from '../utils/swarm/teamHelpers.js'
|
||||
import { assignTeammateColor } from '../utils/swarm/teammateLayoutManager.js'
|
||||
import {
|
||||
ensureTasksDir,
|
||||
resetTaskList,
|
||||
setLeaderTeamName,
|
||||
} from '../utils/tasks.js'
|
||||
|
||||
let _assistantForced = false
|
||||
|
||||
@@ -29,13 +46,67 @@ export function isAssistantForced(): boolean {
|
||||
* Pre-create an in-process team so Agent(name) can spawn teammates
|
||||
* without TeamCreate.
|
||||
*
|
||||
* Phase 1: returns undefined so main.tsx's `assistantTeamContext ?? computeInitialTeamContext()`
|
||||
* correctly falls back. Returning {} would bypass the ?? operator since {} is truthy.
|
||||
*
|
||||
* Phase 2: should return a full team context object matching AppState.teamContext shape.
|
||||
* Creates a session-scoped assistant team file and returns a full team
|
||||
* context object matching AppState.teamContext.
|
||||
*/
|
||||
export async function initializeAssistantTeam(): Promise<undefined> {
|
||||
return undefined
|
||||
export async function initializeAssistantTeam(): Promise<
|
||||
AppState['teamContext']
|
||||
> {
|
||||
const sessionId = getSessionId()
|
||||
const teamName = sanitizeName(`assistant-${sessionId.slice(0, 8)}`)
|
||||
const leadAgentId = formatAgentId(TEAM_LEAD_NAME, teamName)
|
||||
const teamFilePath = getTeamFilePath(teamName)
|
||||
const now = Date.now()
|
||||
const cwd = getCwd()
|
||||
const color = assignTeammateColor(leadAgentId)
|
||||
|
||||
const teamFile: TeamFile = {
|
||||
name: teamName,
|
||||
description: 'Assistant mode in-process team',
|
||||
createdAt: now,
|
||||
leadAgentId,
|
||||
leadSessionId: sessionId,
|
||||
members: [
|
||||
{
|
||||
agentId: leadAgentId,
|
||||
name: TEAM_LEAD_NAME,
|
||||
agentType: 'assistant',
|
||||
color,
|
||||
joinedAt: now,
|
||||
tmuxPaneId: '',
|
||||
cwd,
|
||||
subscriptions: [],
|
||||
backendType: 'in-process',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
await writeTeamFileAsync(teamName, teamFile)
|
||||
registerTeamForSessionCleanup(teamName)
|
||||
await resetTaskList(teamName)
|
||||
await ensureTasksDir(teamName)
|
||||
setLeaderTeamName(teamName)
|
||||
|
||||
return {
|
||||
teamName,
|
||||
teamFilePath,
|
||||
leadAgentId,
|
||||
selfAgentId: leadAgentId,
|
||||
selfAgentName: TEAM_LEAD_NAME,
|
||||
isLeader: true,
|
||||
selfAgentColor: color,
|
||||
teammates: {
|
||||
[leadAgentId]: {
|
||||
name: TEAM_LEAD_NAME,
|
||||
agentType: 'assistant',
|
||||
color,
|
||||
tmuxSessionName: 'in-process',
|
||||
tmuxPaneId: 'leader',
|
||||
cwd,
|
||||
spawnedAt: now,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1963,7 +1963,6 @@ NOTES
|
||||
- You must be logged in with a Claude account that has a subscription
|
||||
- Run \`claude\` first in the directory to accept the workspace trust dialog
|
||||
${serverNote}`
|
||||
// biome-ignore lint/suspicious/noConsole: intentional help output
|
||||
console.log(help)
|
||||
}
|
||||
|
||||
@@ -2002,7 +2001,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
return
|
||||
}
|
||||
if (parsed.error) {
|
||||
// biome-ignore lint/suspicious/noConsole: intentional error output
|
||||
console.error(`Error: ${parsed.error}`)
|
||||
// eslint-disable-next-line custom-rules/no-process-exit
|
||||
process.exit(1)
|
||||
@@ -2041,7 +2039,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
const { PERMISSION_MODES } = await import('../types/permissions.js')
|
||||
const valid: readonly string[] = PERMISSION_MODES
|
||||
if (!valid.includes(permissionMode)) {
|
||||
// biome-ignore lint/suspicious/noConsole: intentional error output
|
||||
console.error(
|
||||
`Error: Invalid permission mode '${permissionMode}'. Valid modes: ${valid.join(', ')}`,
|
||||
)
|
||||
@@ -2084,7 +2081,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
Promise.all([shutdown1PEventLogging(), shutdownDatadog()]),
|
||||
sleep(500, undefined, { unref: true }),
|
||||
]).catch(() => {})
|
||||
// biome-ignore lint/suspicious/noConsole: intentional error output
|
||||
console.error(
|
||||
'Error: Multi-session Remote Control is not enabled for your account yet.',
|
||||
)
|
||||
@@ -2101,7 +2097,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
// The bridge bypasses main.tsx (which renders the interactive TrustDialog via showSetupScreens),
|
||||
// so we must verify trust was previously established by a normal `claude` session.
|
||||
if (!checkHasTrustDialogAccepted()) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(
|
||||
`Error: Workspace not trusted. Please run \`claude\` in ${dir} first to review and accept the workspace trust dialog.`,
|
||||
)
|
||||
@@ -2118,7 +2113,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
|
||||
const bridgeToken = getBridgeAccessToken()
|
||||
if (!bridgeToken) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(BRIDGE_LOGIN_ERROR)
|
||||
// eslint-disable-next-line custom-rules/no-process-exit
|
||||
process.exit(1)
|
||||
@@ -2137,7 +2131,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
})
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(
|
||||
'\nRemote Control lets you access this CLI session from the web (claude.ai/code)\nor the Claude app, so you can pick up where you left off on any device.\n\nYou can disconnect remote access anytime by running /remote-control again.\n',
|
||||
)
|
||||
@@ -2169,7 +2162,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
)
|
||||
const found = await readBridgePointerAcrossWorktrees(dir)
|
||||
if (!found) {
|
||||
// biome-ignore lint/suspicious/noConsole: intentional error output
|
||||
console.error(
|
||||
`Error: No recent session found in this directory or its worktrees. Run \`claude remote-control\` to start a new one.`,
|
||||
)
|
||||
@@ -2180,7 +2172,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
const ageMin = Math.round(pointer.ageMs / 60_000)
|
||||
const ageStr = ageMin < 60 ? `${ageMin}m` : `${Math.round(ageMin / 60)}h`
|
||||
const fromWt = pointerDir !== dir ? ` from worktree ${pointerDir}` : ''
|
||||
// biome-ignore lint/suspicious/noConsole: intentional info output
|
||||
console.error(
|
||||
`Resuming session ${pointer.sessionId} (${ageStr} ago)${fromWt}\u2026`,
|
||||
)
|
||||
@@ -2201,7 +2192,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
!baseUrl.includes('localhost') &&
|
||||
!baseUrl.includes('127.0.0.1')
|
||||
) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(
|
||||
'Error: Remote Control base URL uses HTTP. Only HTTPS or localhost HTTP is allowed.',
|
||||
)
|
||||
@@ -2237,7 +2227,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
? getCurrentProjectConfig().remoteControlSpawnMode
|
||||
: undefined
|
||||
if (savedSpawnMode === 'worktree' && !worktreeAvailable) {
|
||||
// biome-ignore lint/suspicious/noConsole: intentional warning output
|
||||
console.error(
|
||||
'Warning: Saved spawn mode is worktree but this directory is not a git repository. Falling back to same-dir.',
|
||||
)
|
||||
@@ -2264,7 +2253,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
})
|
||||
// biome-ignore lint/suspicious/noConsole: intentional dialog output
|
||||
console.log(
|
||||
`\nClaude Remote Control is launching in spawn mode which lets you create new sessions in this project from Claude Code on Web or your Mobile app. Learn more here: https://code.claude.com/docs/en/remote-control\n\n` +
|
||||
`Spawn mode for this project:\n` +
|
||||
@@ -2343,7 +2331,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
// Only reachable via explicit --spawn=worktree (default is same-dir);
|
||||
// saved worktree pref was already guarded above.
|
||||
if (spawnMode === 'worktree' && !worktreeAvailable) {
|
||||
// biome-ignore lint/suspicious/noConsole: intentional error output
|
||||
console.error(
|
||||
`Error: Worktree mode requires a git repository or WorktreeCreate hooks configured. Use --spawn=session for single-session mode.`,
|
||||
)
|
||||
@@ -2378,7 +2365,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
try {
|
||||
validateBridgeId(resumeSessionId, 'sessionId')
|
||||
} catch {
|
||||
// biome-ignore lint/suspicious/noConsole: intentional error output
|
||||
console.error(
|
||||
`Error: Invalid session ID "${resumeSessionId}". Session IDs must not contain unsafe characters.`,
|
||||
)
|
||||
@@ -2404,7 +2390,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
const { clearBridgePointer } = await import('./bridgePointer.js')
|
||||
await clearBridgePointer(resumePointerDir)
|
||||
}
|
||||
// biome-ignore lint/suspicious/noConsole: intentional error output
|
||||
console.error(
|
||||
`Error: Session ${resumeSessionId} not found. It may have been archived or expired, or your login may have lapsed (run \`claude /login\`).`,
|
||||
)
|
||||
@@ -2416,7 +2401,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
const { clearBridgePointer } = await import('./bridgePointer.js')
|
||||
await clearBridgePointer(resumePointerDir)
|
||||
}
|
||||
// biome-ignore lint/suspicious/noConsole: intentional error output
|
||||
console.error(
|
||||
`Error: Session ${resumeSessionId} has no environment_id. It may never have been attached to a bridge.`,
|
||||
)
|
||||
@@ -2470,7 +2454,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
status: err instanceof BridgeFatalError ? err.status : undefined,
|
||||
})
|
||||
// Registration failures are fatal — print a clean message instead of a stack trace.
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(
|
||||
err instanceof BridgeFatalError && err.status === 404
|
||||
? 'Remote Control environments are not available for your account.'
|
||||
@@ -2495,7 +2478,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
`Bridge resume env mismatch: requested ${reuseEnvironmentId}, backend returned ${environmentId}. Falling back to fresh session.`,
|
||||
),
|
||||
)
|
||||
// biome-ignore lint/suspicious/noConsole: intentional warning output
|
||||
console.warn(
|
||||
`Warning: Could not resume session ${resumeSessionId} — its environment has expired. Creating a fresh session instead.`,
|
||||
)
|
||||
@@ -2546,7 +2528,6 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
const { clearBridgePointer } = await import('./bridgePointer.js')
|
||||
await clearBridgePointer(resumePointerDir)
|
||||
}
|
||||
// biome-ignore lint/suspicious/noConsole: intentional error output
|
||||
console.error(
|
||||
isFatal
|
||||
? `Error: ${errorMessage(err)}`
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
/** Write an error message to stderr (if given) and exit with code 1. */
|
||||
export function cliError(msg?: string): never {
|
||||
// biome-ignore lint/suspicious/noConsole: centralized CLI error output
|
||||
if (msg) console.error(msg)
|
||||
process.exit(1)
|
||||
return undefined as never
|
||||
|
||||
@@ -59,12 +59,9 @@ export async function agentsHandler(): Promise<void> {
|
||||
}
|
||||
|
||||
if (lines.length === 0) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('No agents found.')
|
||||
} else {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${totalActive} active agents\n`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(lines.join('\n').trimEnd())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,27 +72,21 @@ export function handleMarketplaceError(error: unknown, action: string): never {
|
||||
|
||||
function printValidationResult(result: ValidationResult): void {
|
||||
if (result.errors.length > 0) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(
|
||||
`${figures.cross} Found ${result.errors.length} ${plural(result.errors.length, 'error')}:\n`,
|
||||
)
|
||||
result.errors.forEach(error => {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` ${figures.pointer} ${error.path}: ${error.message}`)
|
||||
})
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('')
|
||||
}
|
||||
if (result.warnings.length > 0) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(
|
||||
`${figures.warning} Found ${result.warnings.length} ${plural(result.warnings.length, 'warning')}:\n`,
|
||||
)
|
||||
result.warnings.forEach(warning => {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` ${figures.pointer} ${warning.path}: ${warning.message}`)
|
||||
})
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('')
|
||||
}
|
||||
}
|
||||
@@ -106,7 +100,6 @@ export async function pluginValidateHandler(
|
||||
try {
|
||||
const result = await validateManifest(manifestPath)
|
||||
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`Validating ${result.fileType} manifest: ${result.filePath}\n`)
|
||||
printValidationResult(result)
|
||||
|
||||
@@ -120,7 +113,6 @@ export async function pluginValidateHandler(
|
||||
if (basename(manifestDir) === '.claude-plugin') {
|
||||
contentResults = await validatePluginContents(dirname(manifestDir))
|
||||
for (const r of contentResults) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`Validating ${r.fileType}: ${r.filePath}\n`)
|
||||
printValidationResult(r)
|
||||
}
|
||||
@@ -139,13 +131,11 @@ export async function pluginValidateHandler(
|
||||
: `${figures.tick} Validation passed`,
|
||||
)
|
||||
} else {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${figures.cross} Validation failed`)
|
||||
process.exit(1)
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(
|
||||
`${figures.cross} Unexpected error during validation: ${errorMessage(error)}`,
|
||||
)
|
||||
@@ -358,7 +348,6 @@ export async function pluginListHandler(options: {
|
||||
}
|
||||
|
||||
if (pluginIds.length > 0) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('Installed plugins:\n')
|
||||
}
|
||||
|
||||
@@ -383,25 +372,18 @@ export async function pluginListHandler(options: {
|
||||
const version = installation.version || 'unknown'
|
||||
const scope = installation.scope
|
||||
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` ${figures.pointer} ${pluginId}`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Version: ${version}`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Scope: ${scope}`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Status: ${status}`)
|
||||
for (const error of pluginErrors) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Error: ${getPluginErrorMessage(error)}`)
|
||||
}
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('')
|
||||
}
|
||||
}
|
||||
|
||||
if (inlinePlugins.length > 0 || inlineLoadErrors.length > 0) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('Session-only plugins (--plugin-dir):\n')
|
||||
for (const p of inlinePlugins) {
|
||||
// Same dirName≠manifestName fallback as the JSON path above — error
|
||||
@@ -413,19 +395,13 @@ export async function pluginListHandler(options: {
|
||||
pErrors.length > 0
|
||||
? `${figures.cross} loaded with errors`
|
||||
: `${figures.tick} loaded`
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` ${figures.pointer} ${p.source}`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Version: ${p.manifest.version ?? 'unknown'}`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Path: ${p.path}`)
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Status: ${status}`)
|
||||
for (const e of pErrors) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Error: ${getPluginErrorMessage(e)}`)
|
||||
}
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('')
|
||||
}
|
||||
// Path-level failures: no LoadedPlugin object exists. Show them so
|
||||
@@ -433,7 +409,6 @@ export async function pluginListHandler(options: {
|
||||
for (const e of inlineLoadErrors.filter(e =>
|
||||
e.source.startsWith('inline['),
|
||||
)) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(
|
||||
` ${figures.pointer} ${e.source}: ${figures.cross} ${getPluginErrorMessage(e)}\n`,
|
||||
)
|
||||
@@ -489,12 +464,10 @@ export async function marketplaceAddHandler(
|
||||
}
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('Adding marketplace...')
|
||||
|
||||
const { name, alreadyMaterialized, resolvedSource } =
|
||||
await addMarketplaceSource(marketplaceSource, message => {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(message)
|
||||
})
|
||||
|
||||
@@ -555,33 +528,25 @@ export async function marketplaceListHandler(options: {
|
||||
cliOk('No marketplaces configured')
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('Configured marketplaces:\n')
|
||||
names.forEach(name => {
|
||||
const marketplace = config[name]
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` ${figures.pointer} ${name}`)
|
||||
|
||||
if (marketplace?.source) {
|
||||
const src = marketplace.source
|
||||
if (src.source === 'github') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Source: GitHub (${src.repo})`)
|
||||
} else if (src.source === 'git') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Source: Git (${src.url})`)
|
||||
} else if (src.source === 'url') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Source: URL (${src.url})`)
|
||||
} else if (src.source === 'directory') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Source: Directory (${src.path})`)
|
||||
} else if (src.source === 'file') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(` Source: File (${src.path})`)
|
||||
}
|
||||
}
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log('')
|
||||
})
|
||||
|
||||
@@ -620,11 +585,9 @@ export async function marketplaceUpdateHandler(
|
||||
if (options.cowork) setUseCoworkPlugins(true)
|
||||
try {
|
||||
if (name) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`Updating marketplace: ${name}...`)
|
||||
|
||||
await refreshMarketplace(name, message => {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(message)
|
||||
})
|
||||
|
||||
@@ -644,7 +607,6 @@ export async function marketplaceUpdateHandler(
|
||||
cliOk('No marketplaces configured')
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`Updating ${marketplaceNames.length} marketplace(s)...`)
|
||||
|
||||
await refreshAllMarketplaces()
|
||||
|
||||
@@ -462,7 +462,6 @@ export class StructuredIO {
|
||||
}
|
||||
return message
|
||||
} catch (error) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(`Error parsing streaming input line: ${line}: ${error}`)
|
||||
// eslint-disable-next-line custom-rules/no-process-exit
|
||||
process.exit(1)
|
||||
@@ -687,7 +686,6 @@ export class StructuredIO {
|
||||
)
|
||||
return result
|
||||
} catch (error) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(`Error in hook callback ${callbackId}:`, error)
|
||||
return {}
|
||||
}
|
||||
@@ -781,7 +779,6 @@ export class StructuredIO {
|
||||
}
|
||||
|
||||
function exitWithMessage(message: string): never {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(message)
|
||||
// eslint-disable-next-line custom-rules/no-process-exit
|
||||
process.exit(1)
|
||||
|
||||
@@ -67,7 +67,7 @@ export function parseReferences(
|
||||
const matches = [...input.matchAll(referencePattern)]
|
||||
return matches
|
||||
.map(match => ({
|
||||
id: parseInt(match[2] || '0'),
|
||||
id: parseInt(match[2] || '0', 10),
|
||||
match: match[0],
|
||||
index: match.index,
|
||||
}))
|
||||
|
||||
@@ -19,7 +19,7 @@ const MIGRATIONS: ((c: GlobalConfig) => Notification | undefined)[] = [
|
||||
}
|
||||
},
|
||||
// Opus Pro → default, or pinned 4.0/4.1 → opus alias. Both land on the
|
||||
// current Opus default (4.6 for 1P).
|
||||
// current Opus default (4.7 for 1P).
|
||||
c => {
|
||||
const isLegacyRemap = Boolean(c.legacyOpusMigrationTimestamp)
|
||||
const ts = c.legacyOpusMigrationTimestamp ?? c.opusProMigrationTimestamp
|
||||
@@ -27,8 +27,8 @@ const MIGRATIONS: ((c: GlobalConfig) => Notification | undefined)[] = [
|
||||
return {
|
||||
key: 'opus-pro-update',
|
||||
text: isLegacyRemap
|
||||
? 'Model updated to Opus 4.6 · Set CLAUDE_CODE_DISABLE_LEGACY_MODEL_REMAP=1 to opt out'
|
||||
: 'Model updated to Opus 4.6',
|
||||
? 'Model updated to Opus 4.7 · Set CLAUDE_CODE_DISABLE_LEGACY_MODEL_REMAP=1 to opt out'
|
||||
: 'Model updated to Opus 4.7',
|
||||
color: 'suggestion',
|
||||
priority: 'high',
|
||||
timeoutMs: isLegacyRemap ? 8000 : 3000,
|
||||
|
||||
@@ -97,16 +97,13 @@ export function useIssueFlagBanner(
|
||||
return false
|
||||
}
|
||||
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: process.env.USER_TYPE is a compile-time constant
|
||||
const lastTriggeredAtRef = useRef(0)
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: process.env.USER_TYPE is a compile-time constant
|
||||
const activeForSubmitRef = useRef(-1)
|
||||
|
||||
// Memoize the O(messages) scans. This hook runs on every REPL render
|
||||
// (including every keystroke), but messages is stable during typing.
|
||||
// isSessionContainerCompatible walks all messages + regex-tests each
|
||||
// bash command — by far the heaviest work here.
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: process.env.USER_TYPE is a compile-time constant
|
||||
const shouldTrigger = useMemo(
|
||||
() => isSessionContainerCompatible(messages) && hasFrictionSignal(messages),
|
||||
[messages],
|
||||
|
||||
@@ -24,6 +24,7 @@ import type { ImageDimensions } from '../utils/imageResizer.js'
|
||||
import { isModifierPressed, prewarmModifiers } from '../utils/modifiers.js'
|
||||
import { useDoublePress } from './useDoublePress.js'
|
||||
|
||||
// biome-ignore lint/suspicious/noConfusingVoidType: void is the correct return type for cursor handlers that return nothing
|
||||
type MaybeCursor = void | Cursor
|
||||
type InputHandler = (input: string) => MaybeCursor
|
||||
type InputMapper = (input: string) => MaybeCursor
|
||||
|
||||
@@ -584,7 +584,6 @@ export function useTypeahead({
|
||||
const debouncedFetchSlackChannels = useDebounceCallback(fetchSlackChannels, 150);
|
||||
|
||||
// Handle immediate suggestion logic (cheap operations)
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: store is a stable context ref, read imperatively at call-time
|
||||
const updateSuggestions = useCallback(
|
||||
async (value: string, inputCursorOffset?: number): Promise<void> => {
|
||||
// Use provided cursor offset or fall back to ref (avoids dependency on cursorOffset)
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
/**
|
||||
* Migrate first-party users off explicit Opus 4.0/4.1 model strings.
|
||||
*
|
||||
* The 'opus' alias already resolves to Opus 4.6 for 1P, so anyone still
|
||||
* The 'opus' alias already resolves to Opus 4.7 for 1P, so anyone still
|
||||
* on an explicit 4.0/4.1 string pinned it in settings before 4.5 launched.
|
||||
* parseUserSpecifiedModel now silently remaps these at runtime anyway —
|
||||
* this migration cleans up the settings file so /model shows the right
|
||||
|
||||
@@ -48,6 +48,7 @@ export class FileIndex {
|
||||
private topLevelCache: SearchResult[] | null = null
|
||||
// During async build, tracks how many paths have bitmap/lowerPath filled.
|
||||
// search() uses this to search the ready prefix while build continues.
|
||||
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: used via destructuring in search()
|
||||
private readyCount = 0
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,6 +111,7 @@ function isDefined(n: number): boolean {
|
||||
|
||||
// NaN-safe equality for layout-cache input comparison
|
||||
function sameFloat(a: number, b: number): boolean {
|
||||
// biome-ignore lint/suspicious/noSelfCompare: intentional NaN detection (a !== a is true only for NaN)
|
||||
return a === b || (a !== a && b !== b)
|
||||
}
|
||||
|
||||
@@ -2372,12 +2373,14 @@ function boundAxis(
|
||||
if (v > maxV.value) v = maxV.value
|
||||
} else if (maxU === 2) {
|
||||
const m = (maxV.value * owner) / 100
|
||||
// biome-ignore lint/suspicious/noSelfCompare: intentional NaN guard (m === m is false only for NaN)
|
||||
if (m === m && v > m) v = m
|
||||
}
|
||||
if (minU === 1) {
|
||||
if (v < minV.value) v = minV.value
|
||||
} else if (minU === 2) {
|
||||
const m = (minV.value * owner) / 100
|
||||
// biome-ignore lint/suspicious/noSelfCompare: intentional NaN guard (m === m is false only for NaN)
|
||||
if (m === m && v < m) v = m
|
||||
}
|
||||
return v
|
||||
|
||||
@@ -331,6 +331,7 @@ export function initialize1PEventLogging(): void {
|
||||
parseInt(
|
||||
process.env.OTEL_LOGS_EXPORT_INTERVAL ||
|
||||
DEFAULT_LOGS_EXPORT_INTERVAL_MS.toString(),
|
||||
10,
|
||||
)
|
||||
|
||||
const maxExportBatchSize =
|
||||
|
||||
@@ -470,6 +470,10 @@ const LOCAL_GATE_DEFAULTS: Record<string, unknown> = {
|
||||
tengu_kairos_cron_durable: true, // Persistent cron tasks
|
||||
tengu_attribution_header: true, // API request attribution header
|
||||
tengu_slate_prism: true, // Agent progress summaries
|
||||
|
||||
// ── Ultrareview (cloud code review via CCR) ─────────────────────
|
||||
tengu_review_bughunter_config: { enabled: true }, // /ultrareview command visibility
|
||||
tengu_ccr_bundle_seed_enabled: true, // Bundle seed: skip GitHub App check for branch mode
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
702
src/services/langfuse/__tests__/langfuse.isolated.ts
Normal file
702
src/services/langfuse/__tests__/langfuse.isolated.ts
Normal file
@@ -0,0 +1,702 @@
|
||||
import { mock, describe, test, expect, beforeEach } from 'bun:test'
|
||||
|
||||
// Mock @langfuse/otel before any imports
|
||||
const mockForceFlush = mock(() => Promise.resolve())
|
||||
const mockShutdown = mock(() => Promise.resolve())
|
||||
|
||||
mock.module('@langfuse/otel', () => ({
|
||||
LangfuseSpanProcessor: class MockLangfuseSpanProcessor {
|
||||
forceFlush = mockForceFlush
|
||||
shutdown = mockShutdown
|
||||
onStart = mock(() => {})
|
||||
onEnd = mock(() => {})
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock @opentelemetry/sdk-trace-base
|
||||
mock.module('@opentelemetry/sdk-trace-base', () => ({
|
||||
BasicTracerProvider: class MockBasicTracerProvider {
|
||||
constructor(_opts?: unknown) {}
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock @langfuse/tracing
|
||||
const mockChildUpdate = mock(() => {})
|
||||
const mockChildEnd = mock(() => {})
|
||||
const mockRootUpdate = mock(() => {})
|
||||
const mockRootEnd = mock(() => {})
|
||||
|
||||
// Mock LangfuseOtelSpanAttributes (re-exported from @langfuse/core)
|
||||
const mockLangfuseOtelSpanAttributes: Record<string, string> = {
|
||||
TRACE_SESSION_ID: 'session.id',
|
||||
TRACE_USER_ID: 'user.id',
|
||||
OBSERVATION_TYPE: 'observation.type',
|
||||
OBSERVATION_INPUT: 'observation.input',
|
||||
OBSERVATION_OUTPUT: 'observation.output',
|
||||
OBSERVATION_MODEL: 'observation.model',
|
||||
OBSERVATION_COMPLETION_START_TIME: 'observation.completionStartTime',
|
||||
OBSERVATION_USAGE_DETAILS: 'observation.usageDetails',
|
||||
}
|
||||
|
||||
const mockSpanContext = {
|
||||
traceId: 'test-trace-id',
|
||||
spanId: 'test-span-id',
|
||||
traceFlags: 1,
|
||||
}
|
||||
const mockSetAttribute = mock(() => {})
|
||||
|
||||
// Child observation mock (returned by startObservation for tools/generations)
|
||||
const mockStartObservation = mock(() => ({
|
||||
id: 'test-span-id',
|
||||
traceId: 'test-trace-id',
|
||||
type: 'span',
|
||||
otelSpan: {
|
||||
spanContext: () => mockSpanContext,
|
||||
setAttribute: mockSetAttribute,
|
||||
},
|
||||
update: mockRootUpdate,
|
||||
end: mockRootEnd,
|
||||
}))
|
||||
const mockSetLangfuseTracerProvider = mock(() => {})
|
||||
|
||||
mock.module('@langfuse/tracing', () => ({
|
||||
startObservation: mockStartObservation,
|
||||
LangfuseOtelSpanAttributes: mockLangfuseOtelSpanAttributes,
|
||||
propagateAttributes: mock((_params: unknown, fn?: () => void) => fn?.()),
|
||||
setLangfuseTracerProvider: mockSetLangfuseTracerProvider,
|
||||
}))
|
||||
|
||||
// Mock debug logger
|
||||
mock.module('src/utils/debug.js', () => ({
|
||||
logForDebugging: mock(() => {}),
|
||||
logAntError: mock(() => {}),
|
||||
isDebugToStdErr: () => false,
|
||||
isDebugMode: () => false,
|
||||
getDebugLogPath: () => '/tmp/debug.log',
|
||||
}))
|
||||
|
||||
// Mock user module to avoid heavy dependency chain (execa, config, cwd, env, etc.)
|
||||
mock.module('src/utils/user.js', () => ({
|
||||
getCoreUserData: () => ({
|
||||
email: 'test@example.com',
|
||||
deviceId: 'test-device',
|
||||
}),
|
||||
getUserDataForLogging: () => ({}),
|
||||
}))
|
||||
|
||||
describe('Langfuse integration', () => {
|
||||
beforeEach(() => {
|
||||
// Reset env
|
||||
process.env.HOME = '/Users/testuser'
|
||||
delete process.env.LANGFUSE_PUBLIC_KEY
|
||||
delete process.env.LANGFUSE_SECRET_KEY
|
||||
delete process.env.LANGFUSE_BASE_URL
|
||||
delete process.env.LANGFUSE_USER_ID
|
||||
mockStartObservation.mockClear()
|
||||
mockRootUpdate.mockClear()
|
||||
mockRootEnd.mockClear()
|
||||
mockForceFlush.mockClear()
|
||||
mockShutdown.mockClear()
|
||||
mockSetAttribute.mockClear()
|
||||
})
|
||||
|
||||
// ── sanitize tests ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('sanitizeToolInput', () => {
|
||||
test('replaces home dir in file_path', async () => {
|
||||
const { sanitizeToolInput } = await import('../sanitize.js')
|
||||
const home = process.env.HOME ?? '/Users/testuser'
|
||||
const result = sanitizeToolInput('FileReadTool', {
|
||||
file_path: `${home}/project/file.ts`,
|
||||
}) as Record<string, string>
|
||||
expect(result.file_path).toBe('~/project/file.ts')
|
||||
})
|
||||
|
||||
test('redacts sensitive keys', async () => {
|
||||
const { sanitizeToolInput } = await import('../sanitize.js')
|
||||
const result = sanitizeToolInput('MCPTool', {
|
||||
api_key: 'secret123',
|
||||
token: 'abc',
|
||||
}) as Record<string, string>
|
||||
expect(result.api_key).toBe('[REDACTED]')
|
||||
expect(result.token).toBe('[REDACTED]')
|
||||
})
|
||||
|
||||
test('returns non-object input unchanged', async () => {
|
||||
const { sanitizeToolInput } = await import('../sanitize.js')
|
||||
expect(sanitizeToolInput('BashTool', 'raw string')).toBe('raw string')
|
||||
expect(sanitizeToolInput('BashTool', null)).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sanitizeToolOutput', () => {
|
||||
test('redacts FileReadTool output', async () => {
|
||||
const { sanitizeToolOutput } = await import('../sanitize.js')
|
||||
const result = sanitizeToolOutput('FileReadTool', 'file content here')
|
||||
expect(result).toBe('[file content redacted, 17 chars]')
|
||||
})
|
||||
|
||||
test('redacts FileWriteTool output', async () => {
|
||||
const { sanitizeToolOutput } = await import('../sanitize.js')
|
||||
const result = sanitizeToolOutput('FileWriteTool', 'written content')
|
||||
expect(result).toBe('[file content redacted, 15 chars]')
|
||||
})
|
||||
|
||||
test('truncates BashTool output over 500 chars', async () => {
|
||||
const { sanitizeToolOutput } = await import('../sanitize.js')
|
||||
const longOutput = 'x'.repeat(600)
|
||||
const result = sanitizeToolOutput('BashTool', longOutput)
|
||||
expect(result).toContain('[truncated]')
|
||||
expect(result.length).toBeLessThan(600)
|
||||
})
|
||||
|
||||
test('does not truncate BashTool output under 500 chars', async () => {
|
||||
const { sanitizeToolOutput } = await import('../sanitize.js')
|
||||
const shortOutput = 'hello world'
|
||||
expect(sanitizeToolOutput('BashTool', shortOutput)).toBe('hello world')
|
||||
})
|
||||
|
||||
test('redacts ConfigTool output', async () => {
|
||||
const { sanitizeToolOutput } = await import('../sanitize.js')
|
||||
const result = sanitizeToolOutput('ConfigTool', 'config data')
|
||||
expect(result).toBe('[ConfigTool output redacted, 11 chars]')
|
||||
})
|
||||
|
||||
test('redacts MCPTool output', async () => {
|
||||
const { sanitizeToolOutput } = await import('../sanitize.js')
|
||||
const result = sanitizeToolOutput('MCPTool', 'mcp data')
|
||||
expect(result).toBe('[MCPTool output redacted, 8 chars]')
|
||||
})
|
||||
})
|
||||
|
||||
describe('sanitizeGlobal', () => {
|
||||
test('replaces home dir in strings', async () => {
|
||||
const { sanitizeGlobal } = await import('../sanitize.js')
|
||||
const home = process.env.HOME ?? '/Users/testuser'
|
||||
expect(sanitizeGlobal(`path: ${home}/file`)).toBe('path: ~/file')
|
||||
})
|
||||
|
||||
test('recursively sanitizes nested objects', async () => {
|
||||
const { sanitizeGlobal } = await import('../sanitize.js')
|
||||
const result = sanitizeGlobal({
|
||||
nested: { api_key: 'secret', name: 'test' },
|
||||
}) as Record<string, Record<string, string>>
|
||||
expect(result.nested.api_key).toBe('[REDACTED]')
|
||||
expect(result.nested.name).toBe('test')
|
||||
})
|
||||
|
||||
test('returns non-string/object values unchanged', async () => {
|
||||
const { sanitizeGlobal } = await import('../sanitize.js')
|
||||
expect(sanitizeGlobal(42)).toBe(42)
|
||||
expect(sanitizeGlobal(true)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// ── client tests ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('isLangfuseEnabled', () => {
|
||||
test('returns false when keys not configured', async () => {
|
||||
const { isLangfuseEnabled } = await import('../client.js')
|
||||
expect(isLangfuseEnabled()).toBe(false)
|
||||
})
|
||||
|
||||
test('returns true when both keys are set', async () => {
|
||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||
const { isLangfuseEnabled } = await import('../client.js')
|
||||
expect(isLangfuseEnabled()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('initLangfuse', () => {
|
||||
test('returns false when keys not configured', async () => {
|
||||
const { initLangfuse } = await import('../client.js')
|
||||
expect(initLangfuse()).toBe(false)
|
||||
})
|
||||
|
||||
test('returns true when keys are configured', async () => {
|
||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||
const { isLangfuseEnabled } = await import('../client.js')
|
||||
expect(isLangfuseEnabled()).toBe(true)
|
||||
})
|
||||
|
||||
test('is idempotent — multiple calls do not re-initialize', async () => {
|
||||
const { initLangfuse } = await import('../client.js')
|
||||
expect(() => {
|
||||
initLangfuse()
|
||||
initLangfuse()
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('shutdownLangfuse', () => {
|
||||
test('calls forceFlush and shutdown on processor', async () => {
|
||||
const { shutdownLangfuse } = await import('../client.js')
|
||||
await expect(shutdownLangfuse()).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
// ── tracing tests ───────────────────────────────────────────────────────────
|
||||
|
||||
describe('createTrace', () => {
|
||||
test('returns null when langfuse not enabled', async () => {
|
||||
const { createTrace } = await import('../tracing.js')
|
||||
const span = createTrace({
|
||||
sessionId: 's1',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
})
|
||||
expect(span).toBeNull()
|
||||
})
|
||||
|
||||
test('creates root span when enabled', async () => {
|
||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||
const { createTrace } = await import('../tracing.js')
|
||||
const span = createTrace({
|
||||
sessionId: 's1',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
input: [],
|
||||
})
|
||||
expect(span).not.toBeNull()
|
||||
expect(mockStartObservation).toHaveBeenCalledWith(
|
||||
'agent-run',
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({
|
||||
provider: 'firstParty',
|
||||
model: 'claude-3',
|
||||
agentType: 'main',
|
||||
}),
|
||||
}),
|
||||
{ asType: 'agent' },
|
||||
)
|
||||
// Should set session.id attribute
|
||||
expect(mockSetAttribute).toHaveBeenCalledWith('session.id', 's1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('recordLLMObservation', () => {
|
||||
test('no-ops when rootSpan is null', async () => {
|
||||
const { recordLLMObservation } = await import('../tracing.js')
|
||||
recordLLMObservation(null, {
|
||||
model: 'm',
|
||||
provider: 'firstParty',
|
||||
input: [],
|
||||
output: [],
|
||||
usage: { input_tokens: 10, output_tokens: 5 },
|
||||
})
|
||||
expect(mockStartObservation).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
test('records generation child observation via global startObservation', async () => {
|
||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||
const { createTrace, recordLLMObservation } = await import(
|
||||
'../tracing.js'
|
||||
)
|
||||
const span = createTrace({
|
||||
sessionId: 's1',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
})
|
||||
mockStartObservation.mockClear()
|
||||
mockRootUpdate.mockClear()
|
||||
mockRootEnd.mockClear()
|
||||
recordLLMObservation(span, {
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
input: [{ role: 'user', content: 'hello' }],
|
||||
output: [{ role: 'assistant', content: 'hi' }],
|
||||
usage: { input_tokens: 10, output_tokens: 5 },
|
||||
})
|
||||
// Should call the global startObservation with asType: 'generation' and parentSpanContext
|
||||
expect(mockStartObservation).toHaveBeenCalledWith(
|
||||
'ChatAnthropic',
|
||||
expect.objectContaining({
|
||||
model: 'claude-3',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
asType: 'generation',
|
||||
parentSpanContext: mockSpanContext,
|
||||
}),
|
||||
)
|
||||
expect(mockRootUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
usageDetails: { input: 10, output: 5 },
|
||||
}),
|
||||
)
|
||||
expect(mockRootEnd).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('recordToolObservation', () => {
|
||||
test('no-ops when rootSpan is null', async () => {
|
||||
const { recordToolObservation } = await import('../tracing.js')
|
||||
recordToolObservation(null, {
|
||||
toolName: 'BashTool',
|
||||
toolUseId: 'id1',
|
||||
input: {},
|
||||
output: 'out',
|
||||
})
|
||||
})
|
||||
|
||||
test('records tool child observation via global startObservation', async () => {
|
||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||
const { createTrace, recordToolObservation } = await import(
|
||||
'../tracing.js'
|
||||
)
|
||||
const span = createTrace({
|
||||
sessionId: 's1',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
})
|
||||
mockStartObservation.mockClear()
|
||||
mockRootUpdate.mockClear()
|
||||
mockRootEnd.mockClear()
|
||||
recordToolObservation(span, {
|
||||
toolName: 'BashTool',
|
||||
toolUseId: 'tu-1',
|
||||
input: { command: 'ls' },
|
||||
output: 'file.ts',
|
||||
})
|
||||
// Should call the global startObservation with asType: 'tool' and parentSpanContext
|
||||
expect(mockStartObservation).toHaveBeenCalledWith(
|
||||
'BashTool',
|
||||
expect.objectContaining({
|
||||
input: expect.any(Object),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
asType: 'tool',
|
||||
parentSpanContext: mockSpanContext,
|
||||
}),
|
||||
)
|
||||
expect(mockRootUpdate).toHaveBeenCalled()
|
||||
expect(mockRootEnd).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('passes startTime to global startObservation', async () => {
|
||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||
const { createTrace, recordToolObservation } = await import(
|
||||
'../tracing.js'
|
||||
)
|
||||
const span = createTrace({
|
||||
sessionId: 's1',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
})
|
||||
mockStartObservation.mockClear()
|
||||
const startTime = new Date('2026-01-01T00:00:00Z')
|
||||
recordToolObservation(span, {
|
||||
toolName: 'BashTool',
|
||||
toolUseId: 'tu-2',
|
||||
input: {},
|
||||
output: 'out',
|
||||
startTime,
|
||||
})
|
||||
expect(mockStartObservation).toHaveBeenCalledWith(
|
||||
'BashTool',
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
startTime,
|
||||
parentSpanContext: mockSpanContext,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test('sanitizes FileReadTool output', async () => {
|
||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||
const { createTrace, recordToolObservation } = await import(
|
||||
'../tracing.js'
|
||||
)
|
||||
const span = createTrace({
|
||||
sessionId: 's1',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
})
|
||||
mockRootUpdate.mockClear()
|
||||
recordToolObservation(span, {
|
||||
toolName: 'FileReadTool',
|
||||
toolUseId: 'tu-2',
|
||||
input: { file_path: '/tmp/file.ts' },
|
||||
output: 'file content here',
|
||||
})
|
||||
expect(mockRootUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
output: '[file content redacted, 17 chars]',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test('sets ERROR level for error observations', async () => {
|
||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||
const { createTrace, recordToolObservation } = await import(
|
||||
'../tracing.js'
|
||||
)
|
||||
const span = createTrace({
|
||||
sessionId: 's1',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
})
|
||||
mockRootUpdate.mockClear()
|
||||
recordToolObservation(span, {
|
||||
toolName: 'BashTool',
|
||||
toolUseId: 'tu-3',
|
||||
input: {},
|
||||
output: 'error occurred',
|
||||
isError: true,
|
||||
})
|
||||
expect(mockRootUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ level: 'ERROR' }),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('endTrace', () => {
|
||||
test('no-ops when rootSpan is null', async () => {
|
||||
const { endTrace } = await import('../tracing.js')
|
||||
endTrace(null)
|
||||
expect(mockRootEnd).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('calls span.end()', async () => {
|
||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||
const { createTrace, endTrace } = await import('../tracing.js')
|
||||
const span = createTrace({
|
||||
sessionId: 's1',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
})
|
||||
mockRootEnd.mockClear()
|
||||
endTrace(span)
|
||||
expect(mockRootEnd).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('calls span.update() with output when provided', async () => {
|
||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||
const { createTrace, endTrace } = await import('../tracing.js')
|
||||
const span = createTrace({
|
||||
sessionId: 's1',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
})
|
||||
mockRootUpdate.mockClear()
|
||||
mockRootEnd.mockClear()
|
||||
endTrace(span, 'final output')
|
||||
expect(mockRootUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ output: 'final output' }),
|
||||
)
|
||||
expect(mockRootEnd).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('createSubagentTrace', () => {
|
||||
test('returns null when langfuse not enabled', async () => {
|
||||
const { createSubagentTrace } = await import('../tracing.js')
|
||||
const span = createSubagentTrace({
|
||||
sessionId: 's1',
|
||||
agentType: 'Explore',
|
||||
agentId: 'agent-1',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
})
|
||||
expect(span).toBeNull()
|
||||
})
|
||||
|
||||
test('creates trace with agentType and agentId metadata', async () => {
|
||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||
const { createSubagentTrace } = await import('../tracing.js')
|
||||
const span = createSubagentTrace({
|
||||
sessionId: 's1',
|
||||
agentType: 'Explore',
|
||||
agentId: 'agent-1',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
input: [{ role: 'user', content: 'search for X' }],
|
||||
})
|
||||
expect(span).not.toBeNull()
|
||||
expect(mockStartObservation).toHaveBeenCalledWith(
|
||||
'agent:Explore',
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({
|
||||
agentType: 'Explore',
|
||||
agentId: 'agent-1',
|
||||
provider: 'firstParty',
|
||||
model: 'claude-3',
|
||||
}),
|
||||
}),
|
||||
{ asType: 'agent' },
|
||||
)
|
||||
// Verify session.id attribute is set
|
||||
expect(mockSetAttribute).toHaveBeenCalledWith('session.id', 's1')
|
||||
})
|
||||
|
||||
test('returns null on SDK error', async () => {
|
||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||
mockStartObservation.mockImplementationOnce(() => {
|
||||
throw new Error('SDK error')
|
||||
})
|
||||
const { createSubagentTrace } = await import('../tracing.js')
|
||||
const span = createSubagentTrace({
|
||||
sessionId: 's1',
|
||||
agentType: 'Plan',
|
||||
agentId: 'agent-2',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
})
|
||||
expect(span).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('createTrace with querySource', () => {
|
||||
test('includes querySource in metadata', async () => {
|
||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||
const { createTrace } = await import('../tracing.js')
|
||||
const span = createTrace({
|
||||
sessionId: 's1',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
querySource: 'user',
|
||||
})
|
||||
expect(span).not.toBeNull()
|
||||
expect(mockStartObservation).toHaveBeenCalledWith(
|
||||
'agent-run:user',
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({
|
||||
agentType: 'main',
|
||||
querySource: 'user',
|
||||
}),
|
||||
}),
|
||||
{ asType: 'agent' },
|
||||
)
|
||||
})
|
||||
|
||||
test('omits querySource when not provided', async () => {
|
||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||
mockStartObservation.mockClear()
|
||||
const { createTrace } = await import('../tracing.js')
|
||||
createTrace({
|
||||
sessionId: 's1',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
})
|
||||
const calls = mockStartObservation.mock.calls as unknown[][]
|
||||
const secondArg = calls[0]?.[1] as Record<string, unknown> | undefined
|
||||
const metadata = (secondArg?.metadata ?? {}) as Record<string, unknown>
|
||||
expect(metadata).not.toHaveProperty('querySource')
|
||||
})
|
||||
})
|
||||
|
||||
describe('nested agent scenario', () => {
|
||||
test('sub-agent trace shares sessionId with parent', async () => {
|
||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||
const { createTrace, createSubagentTrace } = await import('../tracing.js')
|
||||
mockSetAttribute.mockClear()
|
||||
|
||||
// Create parent trace
|
||||
const parentSpan = createTrace({
|
||||
sessionId: 'shared-session',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
})
|
||||
|
||||
// Create sub-agent trace with same sessionId
|
||||
const subSpan = createSubagentTrace({
|
||||
sessionId: 'shared-session',
|
||||
agentType: 'Explore',
|
||||
agentId: 'agent-explore-1',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
})
|
||||
|
||||
expect(parentSpan).not.toBeNull()
|
||||
expect(subSpan).not.toBeNull()
|
||||
|
||||
// Both should have set session.id attribute
|
||||
const sessionAttributeCalls = mockSetAttribute.mock.calls.filter(
|
||||
(call: unknown[]) =>
|
||||
Array.isArray(call) &&
|
||||
call[0] === 'session.id' &&
|
||||
call[1] === 'shared-session',
|
||||
)
|
||||
expect(sessionAttributeCalls.length).toBeGreaterThanOrEqual(2)
|
||||
})
|
||||
|
||||
test('query reuses passed langfuseTrace instead of creating new one', async () => {
|
||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||
const { createSubagentTrace } = await import('../tracing.js')
|
||||
|
||||
const subTrace = createSubagentTrace({
|
||||
sessionId: 's1',
|
||||
agentType: 'Explore',
|
||||
agentId: 'agent-1',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
})
|
||||
expect(subTrace).not.toBeNull()
|
||||
|
||||
// Simulate query.ts logic: if langfuseTrace already set, don't create new one
|
||||
const ownsTrace = false
|
||||
const langfuseTrace = subTrace
|
||||
|
||||
expect(ownsTrace).toBe(false)
|
||||
expect(langfuseTrace).toBe(subTrace)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SDK exceptions do not affect main flow', () => {
|
||||
test('createTrace returns null on SDK error', async () => {
|
||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||
mockStartObservation.mockImplementationOnce(() => {
|
||||
throw new Error('SDK error')
|
||||
})
|
||||
const { createTrace } = await import('../tracing.js')
|
||||
const span = createTrace({
|
||||
sessionId: 's1',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
})
|
||||
expect(span).toBeNull()
|
||||
})
|
||||
|
||||
test('recordLLMObservation silently fails on SDK error', async () => {
|
||||
process.env.LANGFUSE_PUBLIC_KEY = 'pk-test'
|
||||
process.env.LANGFUSE_SECRET_KEY = 'sk-test'
|
||||
const { createTrace, recordLLMObservation } = await import(
|
||||
'../tracing.js'
|
||||
)
|
||||
const span = createTrace({
|
||||
sessionId: 's1',
|
||||
model: 'claude-3',
|
||||
provider: 'firstParty',
|
||||
})
|
||||
// The next call to startObservation (for the generation) will throw
|
||||
mockStartObservation.mockImplementationOnce(() => {
|
||||
throw new Error('SDK error')
|
||||
})
|
||||
expect(() =>
|
||||
recordLLMObservation(span, {
|
||||
model: 'm',
|
||||
provider: 'firstParty',
|
||||
input: [],
|
||||
output: [],
|
||||
usage: { input_tokens: 1, output_tokens: 1 },
|
||||
}),
|
||||
).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1444,6 +1444,7 @@ export const connectToServer = memoize(
|
||||
}
|
||||
|
||||
// Wait for graceful shutdown with rapid escalation (total 500ms to keep CLI responsive)
|
||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: async needed for sequential await inside executor
|
||||
await new Promise<void>(async resolve => {
|
||||
let resolved = false
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ function handlePluginCommandError(
|
||||
: command === 'disable-all'
|
||||
? 'disable all plugins'
|
||||
: `${command} plugins`
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(
|
||||
`${figures.cross} Failed to ${operation}: ${errorMessage(error)}`,
|
||||
)
|
||||
@@ -105,7 +104,6 @@ export async function installPlugin(
|
||||
scope: InstallableScope = 'user',
|
||||
): Promise<void> {
|
||||
try {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`Installing plugin "${plugin}"...`)
|
||||
|
||||
const result = await installPluginOp(plugin, scope)
|
||||
@@ -114,7 +112,6 @@ export async function installPlugin(
|
||||
throw new Error(result.message)
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${figures.tick} ${result.message}`)
|
||||
|
||||
// _PROTO_* routes to PII-tagged plugin_name/marketplace_name BQ columns.
|
||||
@@ -162,7 +159,6 @@ export async function uninstallPlugin(
|
||||
throw new Error(result.message)
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${figures.tick} ${result.message}`)
|
||||
|
||||
const { name, marketplace } = parsePluginIdentifier(
|
||||
@@ -203,7 +199,6 @@ export async function enablePlugin(
|
||||
throw new Error(result.message)
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${figures.tick} ${result.message}`)
|
||||
|
||||
const { name, marketplace } = parsePluginIdentifier(
|
||||
@@ -244,7 +239,6 @@ export async function disablePlugin(
|
||||
throw new Error(result.message)
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${figures.tick} ${result.message}`)
|
||||
|
||||
const { name, marketplace } = parsePluginIdentifier(
|
||||
@@ -280,7 +274,6 @@ export async function disableAllPlugins(): Promise<void> {
|
||||
throw new Error(result.message)
|
||||
}
|
||||
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(`${figures.tick} ${result.message}`)
|
||||
|
||||
logEvent('tengu_plugin_disabled_all_cli', {})
|
||||
|
||||
13
src/setup.ts
13
src/setup.ts
@@ -20,6 +20,7 @@ import {
|
||||
} from './bootstrap/state.js'
|
||||
import { getCommands } from './commands.js'
|
||||
import { initSessionMemory } from './services/SessionMemory/sessionMemory.js'
|
||||
import { initSkillLearning } from './services/skillLearning/runtimeObserver.js'
|
||||
import { asSessionId } from './types/ids.js'
|
||||
import { isAgentSwarmsEnabled } from './utils/agentSwarmsEnabled.js'
|
||||
import { checkAndRestoreTerminalBackup } from './utils/appleTerminalBackup.js'
|
||||
@@ -68,8 +69,7 @@ export async function setup(
|
||||
|
||||
// Check for Node.js version < 18
|
||||
const nodeVersion = process.version.match(/^v(\d+)\./)?.[1]
|
||||
if (!nodeVersion || parseInt(nodeVersion) < 18) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
if (!nodeVersion || parseInt(nodeVersion, 10) < 18) {
|
||||
console.error(
|
||||
chalk.bold.red(
|
||||
'Error: Claude Code requires Node.js version 18 or higher.',
|
||||
@@ -117,14 +117,12 @@ export async function setup(
|
||||
if (isAgentSwarmsEnabled()) {
|
||||
const restoredIterm2Backup = await checkAndRestoreITerm2Backup()
|
||||
if (restoredIterm2Backup.status === 'restored') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Detected an interrupted iTerm2 setup. Your original settings have been restored. You may need to restart iTerm2 for the changes to take effect.',
|
||||
),
|
||||
)
|
||||
} else if (restoredIterm2Backup.status === 'failed') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Failed to restore iTerm2 settings. Please manually restore your original settings with: defaults import com.googlecode.iterm2 ${restoredIterm2Backup.backupPath}.`,
|
||||
@@ -137,14 +135,12 @@ export async function setup(
|
||||
try {
|
||||
const restoredTerminalBackup = await checkAndRestoreTerminalBackup()
|
||||
if (restoredTerminalBackup.status === 'restored') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Detected an interrupted Terminal.app setup. Your original settings have been restored. You may need to restart Terminal.app for the changes to take effect.',
|
||||
),
|
||||
)
|
||||
} else if (restoredTerminalBackup.status === 'failed') {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Failed to restore Terminal.app settings. Please manually restore your original settings with: defaults import com.apple.Terminal ${restoredTerminalBackup.backupPath}.`,
|
||||
@@ -252,14 +248,12 @@ export async function setup(
|
||||
worktreeSession.worktreePath,
|
||||
)
|
||||
if (tmuxResult.created) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Created tmux session: ${chalk.bold(tmuxSessionName)}\nTo attach: ${chalk.bold(`tmux attach -t ${tmuxSessionName}`)}`,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(
|
||||
chalk.yellow(
|
||||
`Warning: Failed to create tmux session: ${tmuxResult.error}`,
|
||||
@@ -292,6 +286,7 @@ export async function setup(
|
||||
// raced ahead and memoized an empty bundledSkills list.
|
||||
if (!isBareMode()) {
|
||||
initSessionMemory() // Synchronous - registers hook, gate check happens lazily
|
||||
initSkillLearning() // Synchronous - registers hook, gate check happens lazily
|
||||
if (feature('CONTEXT_COLLAPSE')) {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
;(
|
||||
@@ -406,7 +401,6 @@ export async function setup(
|
||||
process.env.IS_SANDBOX !== '1' &&
|
||||
!isEnvTruthy(process.env.CLAUDE_CODE_BUBBLEWRAP)
|
||||
) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(
|
||||
`--dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons`,
|
||||
)
|
||||
@@ -432,7 +426,6 @@ export async function setup(
|
||||
const isSandbox = process.env.IS_SANDBOX === '1'
|
||||
const isSandboxed = isDocker || isBubblewrap || isSandbox
|
||||
if (!isSandboxed || hasInternet) {
|
||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||
console.error(
|
||||
`--dangerously-skip-permissions can only be used in Docker/sandbox containers with no internet access but got Docker: ${isDocker}, Bubblewrap: ${isBubblewrap}, IS_SANDBOX: ${isSandbox}, hasInternet: ${hasInternet}`,
|
||||
)
|
||||
|
||||
@@ -34,8 +34,8 @@ import typescriptClaudeApiToolUse from './claude-api/typescript/claude-api/tool-
|
||||
// - claude-api/SKILL.md (Current Models pricing table)
|
||||
// - claude-api/shared/models.md (full model catalog with legacy versions and alias mappings)
|
||||
export const SKILL_MODEL_VARS = {
|
||||
OPUS_ID: 'claude-opus-4-6',
|
||||
OPUS_NAME: 'Claude Opus 4.6',
|
||||
OPUS_ID: 'claude-opus-4-7',
|
||||
OPUS_NAME: 'Claude Opus 4.7',
|
||||
SONNET_ID: 'claude-sonnet-4-6',
|
||||
SONNET_NAME: 'Claude Sonnet 4.6',
|
||||
HAIKU_ID: 'claude-haiku-4-5',
|
||||
|
||||
@@ -243,7 +243,7 @@ export function registerLoremIpsumSkill(): void {
|
||||
argumentHint: '[token_count]',
|
||||
userInvocable: true,
|
||||
async getPromptForCommand(args) {
|
||||
const parsed = parseInt(args)
|
||||
const parsed = parseInt(args, 10)
|
||||
|
||||
if (args && (isNaN(parsed) || parsed <= 0)) {
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user