feat(cli): make clb web the primary web command

This commit is contained in:
digua committed 2026-08-12 12:47:00 +08:00
1 parent c6db9f0c4c
commit 47299a94cb
64 files changed
+264 -270

No files matched your search

+2 -2
View File
@@ -113,9 +113,9 @@ function resolveSingleSessionId(dbManager: DatabaseManager): string {
const sessionIds = dbManager.listSessionIds()
if (sessionIds.length === 1) return sessionIds[0]!
if (sessionIds.length === 0) {
throw new Error('No chat sessions found. Import data first or run `chatlab sessions` to verify available sessions.')
throw new Error('No chat sessions found. Import data first or run `clb sessions` to verify available sessions.')
}
throw new Error('Missing --session-id. Run `chatlab sessions` to choose a session ID.')
throw new Error('Missing --session-id. Run `clb sessions` to choose a session ID.')
}
function assertSessionExists(dbManager: DatabaseManager, sessionId: string): void {
+8 -7
View File
@@ -21,7 +21,7 @@ import { registerRuntimeCommand } from './semantic-index/runtime-command'
const program = new Command()
program.name('chatlab').description('ChatLab - Chat history analysis tool').version(getVersion(), '-v, --version')
program.name('clb').description('ChatLab - Chat history analysis tool').version(getVersion(), '-v, --version')
program.hook('preAction', async (_thisCommand, actionCommand) => {
if (actionCommand.name() === 'update' || actionCommand.parent?.name() === 'runtime') return
@@ -39,7 +39,7 @@ program
})
if (result.success) {
console.error(' Updated successfully. Please restart chatlab to use the new version.\n')
console.error(' Updated successfully. Please restart clb to use the new version.\n')
return
}
@@ -114,7 +114,8 @@ program
})
program
.command('start')
.command('web')
.alias('start')
.description('Start ChatLab (HTTP API + Web UI)')
.option('--port <port>', 'Server port', String(DEFAULT_API_PORT))
.option('--host <host>', 'Listen address', '127.0.0.1')
@@ -227,7 +228,7 @@ configCmd
configCmd
.command('set <key> <value>')
.description('Set a config field, e.g. `chatlab config set cli.allow_raw true`')
.description('Set a config field, e.g. `clb config set cli.allow_raw true`')
.action((key: string, value: string) => {
try {
const result = setConfigField(key, value)
@@ -244,7 +245,7 @@ configCmd
program
.command('stop')
.description('Stop the resident service and remove auto-start (reverse of start --daemon)')
.description('Stop the resident service and remove auto-start (reverse of web --daemon)')
.action(async () => {
const { serviceUninstall } = await import('./daemon/service')
serviceUninstall()
@@ -266,11 +267,11 @@ program
console.log(` Service: ${svc.running ? 'running' : 'installed (not running)'}`)
if (portStr) console.log(` Address: ${portStr}`)
console.log(` Auto-start: enabled`)
console.log(`\n Use \`chatlab stop\` to remove the service.\n`)
console.log(`\n Use \`clb stop\` to remove the service.\n`)
} else {
console.log(` Service: not installed`)
console.log(` Auto-start: disabled`)
console.log(`\n Use \`chatlab start --daemon\` to install as a system service.\n`)
console.log(`\n Use \`clb web --daemon\` to install as a system service.\n`)
}
})
+5 -5
View File
@@ -72,7 +72,7 @@ function buildPlist(options: ServiceInstallOptions): string {
const port = options.port ?? 3110
const host = options.host ?? '127.0.0.1'
const cliEntry = resolveCliPath('bin/chatlab.mjs')
const args = ['start', '--no-open', '--port', String(port), '--host', host]
const args = ['web', '--no-open', '--port', String(port), '--host', host]
if (options.token) args.push('--token', options.token)
if (options.headless) args.push('--headless')
if (options.requireAuth) args.push('--require-auth')
@@ -150,7 +150,7 @@ function installMacos(options: ServiceInstallOptions): void {
console.log(` API: http://${host}:${port}`)
console.log(` Logs: ${LOG_FILE}`)
console.log(` Service: ${SERVICE_LABEL} (launchd)`)
console.log(`\nAuto-starts on login. Use \`chatlab stop\` to remove.\n`)
console.log(`\nAuto-starts on login. Use \`clb stop\` to remove.\n`)
}
function uninstallMacos(): void {
@@ -192,7 +192,7 @@ function buildUnit(options: ServiceInstallOptions): string {
const port = options.port ?? 3110
const host = options.host ?? '127.0.0.1'
const cliEntry = resolveCliPath('bin/chatlab.mjs')
const args = ['start', '--no-open', '--port', String(port), '--host', host]
const args = ['web', '--no-open', '--port', String(port), '--host', host]
if (options.token) args.push('--token', options.token)
if (options.headless) args.push('--headless')
if (options.requireAuth) args.push('--require-auth')
@@ -235,7 +235,7 @@ function installLinux(options: ServiceInstallOptions): void {
console.log(` API: http://${host}:${port}`)
console.log(` Logs: ${LOG_FILE}`)
console.log(` Unit: ${SYSTEMD_SERVICE} (systemd)`)
console.log(`\nAuto-starts on login. Use \`chatlab stop\` to remove.\n`)
console.log(`\nAuto-starts on login. Use \`clb stop\` to remove.\n`)
}
function uninstallLinux(): void {
@@ -282,7 +282,7 @@ export function serviceInstall(options: ServiceInstallOptions): void {
return installLinux(options)
default:
console.error('Windows is not yet supported for daemon mode.')
console.error('Please use `chatlab start` to run in the foreground instead.')
console.error('Please use `clb web` to run in the foreground instead.')
process.exit(1)
}
}
+1 -1
View File
@@ -32,7 +32,7 @@ export function formatPortInUseError(port: number): string {
` ✖ Error: port ${port} is already in use.`,
``,
` Another process (possibly a ChatLab instance) is using this port. You can:`,
` • Use another port: chatlab start --port <port>`,
` • Use another port: clb web --port <port>`,
` • Find the process: lsof -iTCP:${port} -sTCP:LISTEN`,
``,
].join('\n')
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* 反向代理路由 — /_proxy/chatlab.fun/*
*
* 在 Web 生产模式(chatlab start)下,将 /_proxy/chatlab.fun/* 代理到
* 在 Web 生产模式(clb web)下,将 /_proxy/chatlab.fun/* 代理到
* https://chatlab.fun,行为与 vite.cli-web.config.mts 中的 dev proxy 保持一致,
* 以解决浏览器 CORS 限制。
*
+1 -1
View File
@@ -53,7 +53,7 @@ function normalizeImportError(message: string): NonNullable<ImportCommandEnvelop
return {
code: 'UNRECOGNIZED_FORMAT',
message: 'The chat export format could not be recognized.',
hint: 'Run "chatlab formats" and retry with --format <id>.',
hint: 'Run "clb formats" and retry with --format <id>.',
}
}
if (message.includes('sessionId contains invalid characters')) {
@@ -1,8 +1,8 @@
/**
* sessions / members query command groups.
*
* Parent actions keep the legacy top-level behavior (`chatlab sessions`,
* `chatlab members <session-id>`) as deprecated aliases for one major version.
* Parent actions keep the legacy top-level behavior (`clb sessions`,
* `clb members <session-id>`) as deprecated aliases for one major version.
*/
import type { Command } from 'commander'
@@ -29,8 +29,8 @@ export function registerSessionCommands(program: Command): void {
.description('Chat sessions (list, show)')
.option('--format <format>', 'Output format (table|json for legacy list; agent|json|text for subcommands)')
.action((options: { format?: string }) => {
// legacy alias: `chatlab sessions [--format table|json]`
console.error('[Deprecated] `chatlab sessions` — use `chatlab sessions list`; alias kept for one major version')
// legacy alias: `clb sessions [--format table|json]`
console.error('[Deprecated] `clb sessions` — use `clb sessions list`; alias kept for one major version')
legacySessionsList(options)
})
@@ -133,7 +133,7 @@ export function registerMemberCommands(program: Command): void {
return
}
console.error(
'[Deprecated] `chatlab members <session-id>` — use `chatlab members list --session <id>`; alias kept for one major version'
'[Deprecated] `clb members <session-id>` — use `clb members list --session <id>`; alias kept for one major version'
)
legacyMembersList(sessionId, options)
})
+1 -1
View File
@@ -72,7 +72,7 @@ export function registerStatsCommands(program: Command): void {
return
}
console.error(
'[Deprecated] `chatlab stats <session-id>` — use `chatlab stats overview --session <id>`; alias kept for one major version'
'[Deprecated] `clb stats <session-id>` — use `clb stats overview --session <id>`; alias kept for one major version'
)
legacyStats(sessionId, options)
})
+5 -5
View File
@@ -208,7 +208,7 @@ export function registerTopicCommands(program: Command): void {
if (items.length === 0) {
const any = getSegmentSummaries(ctx.db, { limit: 1 })
if (any.length === 0) {
const hint = 'Run summary generation in the ChatLab app, or use `chatlab stats keywords` as a fallback.'
const hint = 'Run summary generation in the ChatLab app, or use `clb stats keywords` as a fallback.'
return {
data:
format === 'agent'
@@ -273,7 +273,7 @@ export function registerTopicCommands(program: Command): void {
throw new QueryError({
code: 'INVALID_ARGUMENT',
message: `Invalid --id value: ${options.id}`,
hint: 'Segment ids come from `chatlab topics list`',
hint: 'Segment ids come from `clb topics list`',
})
}
const ctx = createQueryContext(options)
@@ -285,7 +285,7 @@ export function registerTopicCommands(program: Command): void {
throw new QueryError({
code: 'SEGMENT_NOT_FOUND',
message: `Segment ${segmentId} not found`,
hint: 'Run `chatlab topics list` to see available segments',
hint: 'Run `clb topics list` to see available segments',
})
}
const meta: Record<string, unknown> = {
@@ -327,7 +327,7 @@ export function registerSqlCommands(program: Command): void {
throw new QueryError({
code: 'SQL_DISABLED',
message: 'The sql command is disabled by configuration',
hint: 'The user can re-enable it with `chatlab config set cli.allow_sql true`',
hint: 'The user can re-enable it with `clb config set cli.allow_sql true`',
})
}
assertRawAllowed(ctx, options)
@@ -341,7 +341,7 @@ export function registerSqlCommands(program: Command): void {
throw new QueryError({
code: 'SQL_ERROR',
message: err instanceof Error ? err.message : String(err),
hint: 'Run `chatlab schema` to inspect available tables and columns',
hint: 'Run `clb schema` to inspect available tables and columns',
})
}
assertSqlResultPrivacyAllowed(result.columns, !!options.raw)
+1 -1
View File
@@ -60,7 +60,7 @@ export function assertRawAllowed(ctx: Pick<QueryContext, 'allowRaw'>, options: R
throw new QueryError({
code: 'RAW_DISABLED',
message: '--raw output is disabled by default',
hint: 'The user must opt in: set CHATLAB_CLI_ALLOW_RAW=1 or `chatlab config set cli.allow_raw true`',
hint: 'The user must opt in: set CHATLAB_CLI_ALLOW_RAW=1 or `clb config set cli.allow_raw true`',
})
}
}
+6 -1
View File
@@ -11,7 +11,7 @@ import { buildManifest } from './manifest'
function buildProgram(): Command {
const program = new Command()
program.name('chatlab').description('test program')
program.name('clb').description('test program')
const messages = program.command('messages').description('Query messages')
messages
@@ -27,6 +27,7 @@ function buildProgram(): Command {
.option('--limit <n>', 'Max results', '20')
program.command('import <file>').description('Import a chat history file')
program.command('web').alias('start').description('Start ChatLab')
return program
}
@@ -51,10 +52,13 @@ describe('buildManifest', () => {
it('lists non-query commands by name only', () => {
assert.ok(manifest.appCommands.some((c) => c.name === 'import'))
assert.ok(manifest.appCommands.some((c) => c.name === 'web'))
assert.ok(!manifest.appCommands.some((c) => c.name === 'start'))
assert.ok(!manifest.commands.some((c) => c.name === 'import'))
})
it('carries the envelope contract: exit codes, formats and version', () => {
assert.equal(manifest.name, 'clb')
assert.equal(manifest.version, '1.2.3')
assert.equal(manifest.apiVersion, 1)
assert.deepEqual(manifest.formats, ['agent', 'json', 'text'])
@@ -66,5 +70,6 @@ describe('buildManifest', () => {
assert.ok(manifest.examples.length >= 5)
assert.ok(manifest.examples.every((e) => !e.command.includes('--raw')))
assert.ok(manifest.examples.some((e) => e.command.includes('--format agent')))
assert.ok(manifest.examples.every((e) => e.command.startsWith('clb ')))
})
})
+12 -12
View File
@@ -1,5 +1,5 @@
/**
* `chatlab manifest`: machine-readable command tree generated from commander
* `clb manifest`: machine-readable command tree generated from commander
* registration (design §7 discovery). One call replaces N --help probes.
* Query commands carry full arg/option detail; app-level commands (import,
* start, ...) are listed by name only and stay out of the query section.
@@ -60,31 +60,31 @@ const EXIT_CODES: Record<string, string> = {
// Curated task recipes (design §13): agent format for message bodies,
// json for structural scouting; --raw never appears here.
const EXAMPLES: ManifestExample[] = [
{ task: 'What did this group talk about today?', command: 'chatlab messages list --since today --format agent' },
{ task: 'What did this group talk about today?', command: 'clb messages list --since today --format agent' },
{
task: 'Search a topic within a time range',
command: 'chatlab messages search 旅游 --since 2026-01-01 --limit 20 --format agent',
command: 'clb messages search 旅游 --since 2026-01-01 --limit 20 --format agent',
},
{
task: 'Who mentioned it first (message ids)?',
command: 'chatlab messages search 报销 --sort asc --limit 5 --format agent',
command: 'clb messages search 报销 --sort asc --limit 5 --format agent',
},
{
task: 'Follow up a single [#id] marker with context',
command: 'chatlab messages context --id 1021 --window 10 --format agent',
command: 'clb messages context --id 1021 --window 10 --format agent',
},
{
task: 'Conversation between me and a member last month',
command: 'chatlab messages between --member me --member 小红 --last 30d --format agent',
command: 'clb messages between --member me --member 小红 --last 30d --format agent',
},
{ task: 'Most active members', command: 'chatlab stats activity --top 10 --format json' },
{ task: 'Monthly hot topics', command: 'chatlab topics list --since 2026-06-01 --until 2026-06-30 --format agent' },
{ task: 'High-frequency words', command: 'chatlab stats keywords --last 30d --top 20 --format json' },
{ task: 'Most active members', command: 'clb stats activity --top 10 --format json' },
{ task: 'Monthly hot topics', command: 'clb topics list --since 2026-06-01 --until 2026-06-30 --format agent' },
{ task: 'High-frequency words', command: 'clb stats keywords --last 30d --top 20 --format json' },
{
task: 'Structural scouting without content',
command: 'chatlab messages list --last 7d --no-content --fields id,senderName,time --format json',
command: 'clb messages list --last 7d --no-content --fields id,senderName,time --format json',
},
{ task: 'Discover sessions first', command: 'chatlab sessions list --format json' },
{ task: 'Discover sessions first', command: 'clb sessions list --format json' },
]
const NOTES: string[] = [
@@ -162,7 +162,7 @@ export function buildManifest(program: Command, version: string): Manifest {
}
return {
name: 'chatlab',
name: 'clb',
version,
apiVersion: API_VERSION,
formats: ['agent', 'json', 'text'],
+5 -5
View File
@@ -33,7 +33,7 @@ export function assertLegacySqlAllowed(config: Pick<ChatLabConfig, 'cli'>, sql?:
throw new QueryError({
code: 'SQL_DISABLED',
message: 'The sql command is disabled by configuration',
hint: 'The user can re-enable it with `chatlab config set cli.allow_sql true`',
hint: 'The user can re-enable it with `clb config set cli.allow_sql true`',
})
}
if (sql) assertSqlPrivacyAllowed(sql, false)
@@ -44,12 +44,12 @@ export function assertLegacySqlAllowed(config: Pick<ChatLabConfig, 'cli'>, sql?:
function registerLegacyAliases(program: Command): void {
program
.command('search <session-id> <keyword>', { hidden: true })
.description('Deprecated: use `chatlab messages search`')
.description('Deprecated: use `clb messages search`')
.option('--limit <n>', 'Max results to return', '20')
.option('--format <format>', 'Output format (table|json)', 'table')
.action((sessionId: string, keyword: string, options: { limit: string; format: string }) => {
console.error(
'[Deprecated] `chatlab search` — use `chatlab messages search <keyword> --session <id>`; alias kept for one major version'
'[Deprecated] `clb search` — use `clb messages search <keyword> --session <id>`; alias kept for one major version'
)
const { dbManager } = initRuntime()
const db = dbManager.open(sessionId)
@@ -78,12 +78,12 @@ function registerLegacyAliases(program: Command): void {
program
.command('query <session-id>', { hidden: true })
.description('Deprecated: use `chatlab sql`')
.description('Deprecated: use `clb sql`')
.requiredOption('--sql <sql>', 'SQL query statement')
.option('--format <format>', 'Output format (table|json)', 'table')
.action((sessionId: string, options: { sql: string; format: string }) => {
console.error(
'[Deprecated] `chatlab query` — use `chatlab sql "<SELECT ...>" --session <id>`; alias kept for one major version'
'[Deprecated] `clb query` — use `clb sql "<SELECT ...>" --session <id>`; alias kept for one major version'
)
const { config, dbManager } = initRuntime()
const db = dbManager.open(sessionId)
+4 -4
View File
@@ -61,7 +61,7 @@ export function resolveSession(source: SessionSource, ref?: string): ResolvedSes
throw new QueryError({
code: 'SESSION_NOT_FOUND',
message: 'No chat sessions found',
hint: 'Import a chat export first (chatlab import <file>)',
hint: 'Import a chat export first (clb import <file>)',
})
}
throw new QueryError({
@@ -98,7 +98,7 @@ export function resolveSession(source: SessionSource, ref?: string): ResolvedSes
throw new QueryError({
code: 'SESSION_NOT_FOUND',
message: `Session '${ref}' not found`,
hint: 'Run `chatlab sessions list` to see available sessions',
hint: 'Run `clb sessions list` to see available sessions',
})
}
@@ -133,7 +133,7 @@ export function resolveMember(db: DatabaseAdapter, ref: string): ResolvedMember
throw new QueryError({
code: 'MEMBER_NOT_FOUND',
message: `Member id ${ref} not found`,
hint: 'Run `chatlab members list` to see available members',
hint: 'Run `clb members list` to see available members',
})
}
@@ -157,6 +157,6 @@ export function resolveMember(db: DatabaseAdapter, ref: string): ResolvedMember
throw new QueryError({
code: 'MEMBER_NOT_FOUND',
message: `Member '${ref}' not found`,
hint: 'Run `chatlab members list` to see available members',
hint: 'Run `clb members list` to see available members',
})
}
+1 -3
View File
@@ -260,9 +260,7 @@ export async function checkForUpdatesInteractive(): Promise<void> {
if (choice === 1) {
const result = await performCliSelfUpdate()
if (result.success) {
process.stderr.write(
` \x1b[32m🎉 Updated successfully! Please restart chatlab to use the new version.\x1b[0m\n\n`
)
process.stderr.write(` \x1b[32m🎉 Updated successfully! Please restart clb to use the new version.\x1b[0m\n\n`)
process.exit(0)
} else {
process.stderr.write(` ❌ Update failed: ${result.error}\n\n`)
+1 -1
View File
@@ -31,7 +31,7 @@ function buildEnvelope(report: ChatLabValidationReport): ValidateCommandEnvelope
error: {
code: 'INVALID_CHATLAB_FORMAT',
message: `Validation found ${report.errorCount} error(s).`,
hint: 'Fix every error, rerun validation, then preview the import with "chatlab import <file> --dry-run --json".',
hint: 'Fix every error, rerun validation, then preview the import with "clb import <file> --dry-run --json".',
},
meta: { apiVersion: 1 },
}