mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
feat(cli): make clb web the primary web command
This commit is contained in:
1 parent
c6db9f0c4c
commit
47299a94cb
64 files changed
+264
-270
No files matched your search
@@ -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
@@ -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`)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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 限制。
|
||||
*
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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`',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 ')))
|
||||
})
|
||||
})
|
||||
@@ -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'],
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
}
|
||||
@@ -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`)
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user