diff --git a/.changeset/export-install-source-shell-env.md b/.changeset/export-install-source-shell-env.md new file mode 100644 index 000000000..4187bed4f --- /dev/null +++ b/.changeset/export-install-source-shell-env.md @@ -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. diff --git a/apps/kimi-code/src/cli/sub/export.ts b/apps/kimi-code/src/cli/sub/export.ts index 9ec9aafdc..6c5caf10c 100644 --- a/apps/kimi-code/src/cli/sub/export.ts +++ b/apps/kimi-code/src/cli/sub/export.ts @@ -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 exportSession: (input: ExportSessionInput) => Promise; readonly confirmPreviousSession: (summary: PreviousSessionSummary) => Promise; + readonly getInstallSource: () => Promise; + 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 = {}): 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); } diff --git a/apps/kimi-code/test/cli/export.test.ts b/apps/kimi-code/test/cli/export.test.ts index 5413da10e..934c41ccd 100644 --- a/apps/kimi-code/test/cli/export.test.ts +++ b/apps/kimi-code/test/cli/export.test.ts @@ -164,6 +164,8 @@ function makeDeps(overrides: Partial = {}): { 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( diff --git a/packages/agent-core/src/rpc/core-api.ts b/packages/agent-core/src/rpc/core-api.ts index 8b80597d7..ebc1b2e36 100644 --- a/packages/agent-core/src/rpc/core-api.ts +++ b/packages/agent-core/src/rpc/core-api.ts @@ -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 { diff --git a/packages/agent-core/src/session/export/manifest.ts b/packages/agent-core/src/session/export/manifest.ts index d4fe1999c..2e1c1809c 100644 --- a/packages/agent-core/src/session/export/manifest.ts +++ b/packages/agent-core/src/session/export/manifest.ts @@ -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, }; } diff --git a/packages/agent-core/src/session/export/session-export.ts b/packages/agent-core/src/session/export/session-export.ts index 6f4d5e956..e4e625722 100644 --- a/packages/agent-core/src/session/export/session-export.ts +++ b/packages/agent-core/src/session/export/session-export.ts @@ -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 = diff --git a/packages/node-sdk/src/rpc.ts b/packages/node-sdk/src/rpc.ts index 0446dafd6..05bc7dfd0 100644 --- a/packages/node-sdk/src/rpc.ts +++ b/packages/node-sdk/src/rpc.ts @@ -183,6 +183,8 @@ export class SDKRpcClient { outputPath: input.outputPath, includeGlobalLog: input.includeGlobalLog, version: input.version, + installSource: input.installSource, + shellEnv: input.shellEnv, }); } diff --git a/packages/node-sdk/src/types.ts b/packages/node-sdk/src/types.ts index 7e4944f6f..f5a2a90b8 100644 --- a/packages/node-sdk/src/types.ts +++ b/packages/node-sdk/src/types.ts @@ -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 {