qwen-code/packages/cli/src/utils/sessionPaths.ts
kkhomej33-netizen dc6a5ad50a
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
feat(cli): add session path status command (#4124)
* feat(cli): add session path status command

* fix(cli): add status paths translations

* fix(core): use secure subagent id suffix

* fix(cli): harden status paths log lookup

* fix(cli): use secure prompt id randomness

* test(cli): cover status paths formatting
2026-05-20 16:33:19 +08:00

189 lines
5 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs/promises';
import path from 'node:path';
import {
createDebugLogger,
resolveOpenAILogDir,
Storage,
} from '@qwen-code/qwen-code-core';
import type { CommandContext } from '../ui/commands/types.js';
const debugLogger = createDebugLogger('SESSION_PATHS');
const OPENAI_LOG_SCAN_LIMIT = 100;
export interface SessionPathEntry {
label: string;
value: string;
}
export interface SessionPathSection {
title: string;
entries: SessionPathEntry[];
}
export interface SessionPathInfo {
sections: SessionPathSection[];
}
export async function collectSessionPathInfo(
context: CommandContext,
): Promise<SessionPathInfo> {
const config = context.services.config;
const sessionId =
config?.getSessionId() || context.session.stats.sessionId || 'unknown';
const contentGeneratorConfig = config?.getContentGeneratorConfig();
const workingDir = config?.getWorkingDir() || process.cwd();
const openAILogDir = resolveOpenAILogDir(
contentGeneratorConfig?.openAILoggingDir,
workingDir,
);
const openAILoggingEnabled =
contentGeneratorConfig?.enableOpenAILogging === true;
const latestOpenAILog =
openAILoggingEnabled && sessionId !== 'unknown'
? await findLatestOpenAILogForSession(openAILogDir, sessionId)
: undefined;
const transcriptPath = config?.getTranscriptPath() || '';
const debugLogPath =
config?.getDebugMode() && sessionId !== 'unknown'
? Storage.getDebugLogPath(sessionId)
: '';
const planFilePath =
config?.getPlanFilePath() ||
(sessionId === 'unknown' ? '' : Storage.getPlanFilePath(sessionId));
const planFileExists = planFilePath ? await pathExists(planFilePath) : false;
const sections: SessionPathSection[] = [
{
title: 'Session files',
entries: [
{ label: 'Session ID', value: sessionId },
...(transcriptPath
? [{ label: 'Transcript', value: transcriptPath }]
: []),
...(debugLogPath ? [{ label: 'Debug log', value: debugLogPath }] : []),
...(planFileExists
? [{ label: 'Plan file', value: planFilePath }]
: []),
],
},
];
if (openAILoggingEnabled) {
sections.push({
title: 'OpenAI logs',
entries: [
{ label: 'Directory', value: openAILogDir },
{ label: 'Latest for session', value: latestOpenAILog ?? 'none yet' },
],
});
}
return {
sections,
};
}
export function formatSessionPathInfo(info: SessionPathInfo): string {
const lines: string[] = [];
for (const [index, section] of info.sections.entries()) {
if (index > 0) {
lines.push('');
}
lines.push(`${section.title}:`);
for (const entry of section.entries) {
lines.push(` ${entry.label}: ${entry.value}`);
}
}
return lines.join('\n');
}
async function findLatestOpenAILogForSession(
logDir: string,
sessionId: string,
): Promise<string | undefined> {
const files = await listLogFiles(logDir, (name) =>
/^openai-.*\.json$/.test(name),
);
for (const file of files) {
try {
const raw = await fs.readFile(file, 'utf-8');
const parsed: unknown = JSON.parse(raw);
if (hasContextSessionId(parsed, sessionId)) {
return file;
}
} catch (error) {
if (
error instanceof SyntaxError ||
(error as NodeJS.ErrnoException).code === 'ENOENT'
) {
continue;
}
debugLogger.warn('Error reading OpenAI log file', file, error);
}
}
return undefined;
}
async function listLogFiles(
dir: string,
predicate: (name: string) => boolean,
): Promise<string[]> {
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
if (!entry.isFile() || !predicate(entry.name)) {
continue;
}
insertRecentFile(files, path.join(dir, entry.name));
}
return files;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
debugLogger.warn('Unable to list OpenAI log directory', dir, error);
}
return [];
}
}
function insertRecentFile(files: string[], file: string): void {
const insertAt = files.findIndex((existing) => file > existing);
if (insertAt === -1) {
if (files.length < OPENAI_LOG_SCAN_LIMIT) {
files.push(file);
}
return;
}
files.splice(insertAt, 0, file);
if (files.length > OPENAI_LOG_SCAN_LIMIT) {
files.pop();
}
}
async function pathExists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
function hasContextSessionId(value: unknown, sessionId: string): boolean {
if (!value || typeof value !== 'object') {
return false;
}
const context = (value as { context?: unknown }).context;
if (!context || typeof context !== 'object') {
return false;
}
const ctx = context as { sessionId?: unknown; promptId?: unknown };
return ctx.sessionId === sessionId || ctx.promptId === sessionId;
}