feat(export): record install source and shell environment in manifest (#105)

* feat(export): record install source and shell environment in manifest

Capture how the CLI was installed (npm-global, native, etc.) and the
user's terminal environment (TERM, TERM_PROGRAM, multiplexer, SHELL)
so exported session archives carry richer diagnostic context.

* chore: add changeset for export manifest enhancements

* chore: simplify changeset description
This commit is contained in:
liruifengv 2026-05-27 14:39:52 +08:00 committed by GitHub
parent 55870616ca
commit d599183c8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 72 additions and 7 deletions

View file

@ -0,0 +1,7 @@
---
"@moonshot-ai/agent-core": patch
"@moonshot-ai/kimi-code-sdk": patch
"@moonshot-ai/kimi-code": patch
---
Enhance `kimi export` to include more diagnostic information in the manifest.

View file

@ -18,12 +18,14 @@ import {
type ExportSessionInput,
type ExportSessionResult,
type SessionSummary,
type ShellEnvironment,
type TelemetryClient,
} from '@moonshot-ai/kimi-code-sdk';
import type { Command } from 'commander';
import { CLI_SHUTDOWN_TIMEOUT_MS, CLI_UI_MODE } from '#/constant/app';
import { createCliTelemetryBootstrap, initializeCliTelemetry } from '#/cli/telemetry';
import { detectInstallSource } from '#/cli/update/source';
import { createKimiCodeHostIdentity } from '#/cli/version';
interface WritableLike {
@ -41,6 +43,8 @@ export interface ExportDeps {
readonly listSessions: (workDir: string) => Promise<readonly SessionSummary[]>;
readonly exportSession: (input: ExportSessionInput) => Promise<ExportSessionResult>;
readonly confirmPreviousSession: (summary: PreviousSessionSummary) => Promise<boolean>;
readonly getInstallSource: () => Promise<string>;
readonly getShellEnv: () => ShellEnvironment;
readonly version: string;
readonly cwd: () => string;
readonly stdout: WritableLike;
@ -81,9 +85,13 @@ export async function handleExport(
}
try {
const installSource = await deps.getInstallSource();
const shellEnv = deps.getShellEnv();
const result = await deps.exportSession({
id: resolvedId,
version: deps.version,
installSource,
shellEnv,
...(output === undefined ? {} : { outputPath: output }),
...(opts.includeGlobalLog ? { includeGlobalLog: true } : {}),
});
@ -180,6 +188,8 @@ function createDefaultExportDeps(overrides: Partial<ExportDeps> = {}): ExportDep
}
}),
version: overrides.version ?? identity.version,
getInstallSource: overrides.getInstallSource ?? (() => detectInstallSource()),
getShellEnv: overrides.getShellEnv ?? detectShellEnvironment,
confirmPreviousSession: overrides.confirmPreviousSession ?? confirmPreviousSession,
cwd: overrides.cwd ?? (() => process.cwd()),
stdout: overrides.stdout ?? process.stdout,
@ -221,6 +231,23 @@ async function confirmPreviousSession(summary: PreviousSessionSummary): Promise<
}
}
function detectMultiplexer(): string | undefined {
if (process.env['TMUX']) return 'tmux';
if (process.env['STY']) return 'screen';
if (process.env['ZELLIJ']) return 'zellij';
return undefined;
}
function detectShellEnvironment(): ShellEnvironment {
return {
term: process.env['TERM'] || undefined,
termProgram: process.env['TERM_PROGRAM'] || undefined,
termProgramVersion: process.env['TERM_PROGRAM_VERSION'] || undefined,
multiplexer: detectMultiplexer(),
shell: process.env['SHELL'] || undefined,
};
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}

View file

@ -164,6 +164,8 @@ function makeDeps(overrides: Partial<ExportDeps> = {}): {
return makeResult(input.id, input.outputPath ?? join(tmp, `${input.id}.zip`));
},
confirmPreviousSession: async () => true,
getInstallSource: async () => 'npm-global',
getShellEnv: () => ({ term: 'xterm-256color', shell: '/bin/zsh' }),
version: '1.0.0-test',
cwd: () => tmp,
stdout: {
@ -223,7 +225,7 @@ describe('kimi export', () => {
expect(exitCodes).toEqual([]);
expect(stderr).toEqual([]);
expect(listedWorkDirs).toEqual([]);
expect(exportInputs).toEqual([{ id: 'ses_test123456', outputPath: output, includeGlobalLog: true, version: '1.0.0-test' }]);
expect(exportInputs).toEqual([{ id: 'ses_test123456', outputPath: output, includeGlobalLog: true, version: '1.0.0-test', installSource: 'npm-global', shellEnv: { term: 'xterm-256color', shell: '/bin/zsh' } }]);
expect(stdout.join('').trim()).toBe(output);
});
@ -232,7 +234,7 @@ describe('kimi export', () => {
await runExport(deps, { sessionId: 'session_default_output' });
expect(exportInputs).toEqual([{ id: 'session_default_output', includeGlobalLog: true, version: '1.0.0-test' }]);
expect(exportInputs).toEqual([{ id: 'session_default_output', includeGlobalLog: true, version: '1.0.0-test', installSource: 'npm-global', shellEnv: { term: 'xterm-256color', shell: '/bin/zsh' } }]);
expect(stdout.join('').trim()).toBe(join(tmp, 'session_default_output.zip'));
});
@ -270,7 +272,7 @@ describe('kimi export', () => {
await runExport(deps, { output });
expect(exitCodes).toEqual([]);
expect(exportInputs).toEqual([{ id: 'ses_fallback', outputPath: output, includeGlobalLog: true, version: '1.0.0-test' }]);
expect(exportInputs).toEqual([{ id: 'ses_fallback', outputPath: output, includeGlobalLog: true, version: '1.0.0-test', installSource: 'npm-global', shellEnv: { term: 'xterm-256color', shell: '/bin/zsh' } }]);
expect(stdout.join('').trim()).toBe(output);
});
@ -312,7 +314,7 @@ describe('kimi export', () => {
await runExport(deps, { output: join(tmp, 'yes.zip'), yes: true });
expect(exitCodes).toEqual([]);
expect(exportInputs).toEqual([{ id: 'ses_yes', outputPath: join(tmp, 'yes.zip'), includeGlobalLog: true, version: '1.0.0-test' }]);
expect(exportInputs).toEqual([{ id: 'ses_yes', outputPath: join(tmp, 'yes.zip'), includeGlobalLog: true, version: '1.0.0-test', installSource: 'npm-global', shellEnv: { term: 'xterm-256color', shell: '/bin/zsh' } }]);
});
it('describes the user-facing command without implementation details', () => {
@ -338,7 +340,7 @@ describe('kimi export', () => {
await program.parseAsync(['node', 'kimi', 'export', '--no-include-global-log', '-y']);
expect(exitCodes).toEqual([]);
expect(exportInputs).toEqual([{ id: 'ses_global_log', version: '1.0.0-test' }]);
expect(exportInputs).toEqual([{ id: 'ses_global_log', version: '1.0.0-test', installSource: 'npm-global', shellEnv: { term: 'xterm-256color', shell: '/bin/zsh' } }]);
expect(stdout.join('').trim()).toBe(join(tmp, 'ses_global_log.zip'));
});
@ -361,7 +363,7 @@ describe('kimi export', () => {
expect(exitCodes).toEqual([]);
expect(exportInputs).toEqual([
{ id: 'ses_after_id', outputPath: output, version: '1.0.0-test' },
{ id: 'ses_after_id', outputPath: output, version: '1.0.0-test', installSource: 'npm-global', shellEnv: { term: 'xterm-256color', shell: '/bin/zsh' } },
]);
});
@ -420,6 +422,8 @@ describe('kimi export', () => {
outputPath: output,
version: expect.any(String),
includeGlobalLog: true,
installSource: expect.any(String),
shellEnv: expect.objectContaining({ shell: expect.any(String) }),
});
expect(mocks.shutdownTelemetry).toHaveBeenCalledWith({ timeoutMs: 3000 });
expect(mocks.harnessExportSession.mock.invocationCallOrder[0]).toBeLessThan(

View file

@ -52,6 +52,14 @@ export interface ForkSessionPayload {
readonly metadata?: JsonObject;
}
export interface ShellEnvironment {
readonly term?: string | undefined;
readonly termProgram?: string | undefined;
readonly termProgramVersion?: string | undefined;
readonly multiplexer?: string | undefined;
readonly shell?: string | undefined;
}
export interface ExportSessionPayload {
readonly sessionId: string;
readonly outputPath?: string | undefined;
@ -63,6 +71,9 @@ export interface ExportSessionPayload {
readonly includeGlobalLog?: boolean | undefined;
/** Host version to record in the export manifest. */
readonly version: string;
/** How the CLI was installed (e.g. 'npm-global', 'native'). */
readonly installSource?: string | undefined;
readonly shellEnv?: ShellEnvironment | undefined;
}
export interface ExportSessionManifest {
@ -80,6 +91,9 @@ export interface ExportSessionManifest {
readonly sessionLogPath?: string | undefined;
/** zip-relative path to the bundled global diagnostic log (only when --include-global-log). */
readonly globalLogPath?: string | undefined;
/** How the CLI was installed (e.g. 'npm-global', 'native'). */
readonly installSource?: string | undefined;
readonly shellEnv?: ShellEnvironment | undefined;
}
export interface ExportSessionResult {

View file

@ -1,6 +1,6 @@
import { AGENT_WIRE_PROTOCOL_VERSION } from '../../agent/records';
import type { SessionWireScan } from '#/session/export/wire-scan';
import type { ExportSessionManifest, SessionSummary } from '#/rpc/core-api';
import type { ExportSessionManifest, ShellEnvironment, SessionSummary } from '#/rpc/core-api';
export const WIRE_PROTOCOL_VERSION = AGENT_WIRE_PROTOCOL_VERSION;
@ -12,6 +12,8 @@ export function buildExportManifest(args: {
readonly sessionScan: SessionWireScan;
readonly sessionLogPath?: string | undefined;
readonly globalLogPath?: string | undefined;
readonly installSource?: string | undefined;
readonly shellEnv?: ShellEnvironment | undefined;
}): ExportSessionManifest {
return {
sessionId: args.summary.id,
@ -32,5 +34,7 @@ export function buildExportManifest(args: {
workspaceDir: args.summary.workDir,
sessionLogPath: args.sessionLogPath,
globalLogPath: args.globalLogPath,
installSource: args.installSource,
shellEnv: args.shellEnv,
};
}

View file

@ -54,6 +54,8 @@ export async function exportSessionDirectory(input: {
sessionScan,
sessionLogPath: hasSessionLog ? SESSION_LOG_REL : undefined,
globalLogPath: bundledGlobal ? GLOBAL_LOG_REL : undefined,
installSource: input.request.installSource,
shellEnv: input.request.shellEnv,
});
const outputPath =

View file

@ -183,6 +183,8 @@ export class SDKRpcClient {
outputPath: input.outputPath,
includeGlobalLog: input.includeGlobalLog,
version: input.version,
installSource: input.installSource,
shellEnv: input.shellEnv,
});
}

View file

@ -1,6 +1,7 @@
import type {
ExportSessionManifest,
ResumeSessionResult,
ShellEnvironment,
TelemetryClient,
TelemetryContextPatch,
TelemetryProperties,
@ -35,6 +36,7 @@ export type {
ProviderType,
ResumedAgentState,
ServicesConfig,
ShellEnvironment,
SkillSummary,
ThinkingConfig,
ToolInfo,
@ -94,6 +96,9 @@ export interface ExportSessionInput {
readonly includeGlobalLog?: boolean | undefined;
/** Host version to record in the export manifest. */
readonly version: string;
/** How the CLI was installed (e.g. 'npm-global', 'native'). */
readonly installSource?: string | undefined;
readonly shellEnv?: ShellEnvironment | undefined;
}
export interface ExportSessionResult {