diff --git a/docs/datagpt-naming-review.md b/docs/datagpt-naming-review.md new file mode 100644 index 0000000000..c92bcb73a3 --- /dev/null +++ b/docs/datagpt-naming-review.md @@ -0,0 +1,99 @@ +# DataGPT CLI 命名规范审查报告 + +## 现有 cz-cli 命名模式(参考基准) + +| 模式 | 示例 | +|---|---| +| 简单 CRUD 动词 | `list`, `create`, `describe`, `drop`, `delete`, `update` | +| 资源-名词式子命令 | `datasource catalogs`, `datasource objects` | +| 状态/信息查看 | `workspace current`, `profile status`, `runs stats` | +| 获取结果 | `job result`, `runs logs` | +| 操作动词 | `execute`, `deploy`, `undeploy`, `refill`, `rerun`, `stop` | +| 连接测试 | `datasource test` | +| 异步模式 | `sql --sync`(用 flag 控制,不拆分独立命令) | +| 分页 | `--page` / `--page-size` 选项(不拆分独立命令) | +| 别名 | `["detail", "show"]` 数组语法,`["logs", "log"]` | + +--- + +## 审查发现(按严重程度排列) + +### 阻断级 — 与现有命令直接冲突 + +| # | 文档当前写法 | 应改为 | 原因 | +|---|---|---|---| +| 1 | `datasource verify` | `datasource test` | 现有 `datasource test` 已实现连接验证,同一概念应用同一动词。 | +| 2 | `datasource list-databases` | `datasource catalogs` | 现有 `datasource catalogs ` 使用名词形式。 | +| 3 | `datasource list-tables` | `datasource objects` | 现有 `datasource objects ` 已实现该功能。 | +| 4 | `datasource list-columns` | **删除**,用 `datasource describe` | 现有 `datasource describe ` 已返回列详情。 | +| 5 | `datasource preview-table` | `datasource preview` | 遵循 `table preview ` 模式,仅保留动词。 | +| 6 | `session get-answer` | `session result` | 遵循 `job result` 模式,用名词表示获取输出。 | +| 7 | `session poll-answer` | `session wait` | 遵循 `runs wait ` 模式。 | +| 8 | `datasource search` | **删除**,用 `datasource list --search` | 现有 `datasource list` 已支持 `--name` 过滤,加 `--search` 即可。 | +| 9 | `datasource load-async` | `datasource load --async` | 遵循 `sql --sync` 模式,用 flag 控制,不拆命令。 | +| 10 | `metric search` | `metric list` + 过滤选项 | 同 #8。 | + +### 高优 — 动词-名词复合形式应改为资源优先 + +| # | 文档当前写法 | 应改为 | 原因 | +|---|---|---|---| +| 11 | `domain add-table` | `domain table add` | 资源优先模式,table 是被操作的资源。 | +| 12 | `domain remove-table` | `domain table remove` | 同上。 | +| 13 | `domain discover-joins` | `domain joins discover` | 资源优先。 | +| 14 | `domain apply-joins` | `domain joins apply` | 资源优先。 | +| 15 | `domain get-discover-joins-result` | `domain joins result` | 5 个词 → 3 个词,过于冗长。 | +| 16 | `domain add-relation` / `remove-relation` | `domain relation add` / `domain relation remove` | 资源优先。 | +| 17 | `domain list-relations` | `domain relation list` | 资源优先。 | +| 18 | `domain list-relations-by-target` | `domain relation list --target-type X --target-id Y` | 用选项代替复合名称。 | +| 19 | `table set-semantics` | `table semantics set` | 资源优先。 | +| 20 | `table get-semantics` | `table semantics get` | 资源优先。 | +| 21 | `table set-semantics-prop` | `table semantics prop` | 连字符过多。 | +| 22 | `table batch-detail` | `table semantics list` | 对齐 `list` 模式。 | +| 23 | `column set-virtual` | `column virtual set` | 资源优先。 | +| 24 | `column compile-virtual` | `column virtual compile` | 资源优先。 | +| 25 | `column remove-virtual` | `column virtual delete` | 资源优先,且 `remove` → `delete`。 | +| 26 | `column list-virtual` | `column virtual list` | 资源优先。 | +| 27 | `datasource query-tables` | `datasource tables` 或合并到 `objects` | 动词-名词复合形式。 | + +### 高优 — `--safe`/`--open`/`--paged` 等模式变体应改用 flag + +| # | 文档当前写法 | 应改为 | 原因 | +|---|---|---|---| +| 28 | `session create-safe` | `session create` | 当前 open flow 固定创建 safe session,不再暴露 safe 模式开关。 | +| 29 | `session open` / `open-paged` | `session list --page N --page-size M` | 分页在现有 `datasource list` 中通过选项控制。 | +| 30 | `session run-open` | `session run` | 当前 open flow 固定调用 `/open/text2insight/query`,不再暴露 open 模式开关。 | +| 31 | `session stop-open` | `session stop` | 当前 open flow 固定调用 `/open/text2insight/stop`。 | +| 32 | `metric calculate-async` | `metric calculate --async` | 与 `load --async` 模式一致。 | + +### 高优 — 动词选择不一致 + +| # | 文档当前写法 | 应改为 | 原因 | +|---|---|---|---| +| 33 | `knowledge add` | `knowledge create` | `add` 用于向父资源添加子项。创建独立资源用 `create`(`schema create`、`table create`、`profile create`、`task create`)。 | +| 34 | `knowledge remove` | `knowledge delete` | `remove` 用于从父资源中移除。删除独立资源用 `delete` 或 `drop`(`profile delete`、`schema drop`)。 | +| 35 | `tenant is-allowed` | `tenant status` 或 `tenant allowed` | 遵循 `workspace current` / `profile status` 模式。 | + +### 中优 — 结构 / 命名冲突 + +| # | 问题 | 建议 | +|---|---|---| +| 36 | `datagpt table` 与顶层 `table` 命令冲突 | 用户容易混淆。顶层 `table` 管理数据库表(DESCRIBE、DROP 等),`datagpt table` 做语义配置。建议重命名为 `datagpt semantics`(其子命令就是 `set`/`get` 语义)或 `datagpt dataset`(API 已使用 `--dataset-id`)。 | +| 37 | `datagpt domain delete` vs `schema drop` | `delete` 可以(与 `profile delete` 一致),但应在 DataGPT 内部保持一致,不要混用 `delete` 和 `remove`。 | +| 38 | `answer-builder` 顶层含连字符 | `ai-guide` 已使用连字符,此处一致。但名称偏长,可考虑 `builder` 或 `answer`。 | +| 39 | `knowledge maintain` | 文档中语义不明。如果是更新操作,应叫 `knowledge update`。 | + +--- + +## 总结 + +文档中的命名问题可归纳为四类: + +1. **同一概念、不同动词** — 应使用 cz-cli 已有动词:`verify`→`test`、`get-answer`→`result`、`poll-answer`→`wait`、`add`→`create`、`remove`→`delete` + +2. **动词-名词复合形式** — 应重组为资源优先的子命令结构:`add-table`→`table add`、`set-semantics`→`semantics set`、`set-virtual`→`virtual set`、`discover-joins`→`joins discover`、`list-relations`→`relation list` + +3. **模式变体拆分独立命令** — 应合并或内聚到固定 open flow:`*-async`→`--async`、`*-paged`→`--page`;session 的 safe/open 变体由当前 open API 路径固定承载,不再暴露为 CLI flag + +4. **冗余命令** — 应合并到已有模式:`search`→`list --filter`、`list-columns`→`describe`、`list-tables`→`objects`、`list-databases`→`catalogs` + +老板给出的 `ask`→`run` 示例正是此类修改的典型代表——同一概念在 cz-cli 中已有对应动词时,优先沿用。 diff --git a/packages/cz-cli/src/cli.ts b/packages/cz-cli/src/cli.ts index 2a8729fd68..58f5f7e34e 100644 --- a/packages/cz-cli/src/cli.ts +++ b/packages/cz-cli/src/cli.ts @@ -104,7 +104,7 @@ export function createCli(args: string[]) { const aiMessage = "Run the command with --help to see available options and usage." const message = (msg && msg.trim() !== "") ? msg : (() => { const KNOWN_FLAGS = new Set(["profile", "p", "jdbc", "pat", "username", "password", "service", "protocol", "instance", "workspace", "schema", "s", "vcluster", "v", "format", "field", "debug", "d", "help", "h", "version", "target", "t"]) - const KNOWN_COMMANDS = new Set(["sql", "schema", "table", "workspace", "status", "profile", "task", "runs", "attempts", "job", "agent", "serve", "setup", "update", "datasource", "ai-gateway"]) + const KNOWN_COMMANDS = new Set(["sql", "schema", "table", "workspace", "status", "profile", "task", "runs", "attempts", "job", "agent", "serve", "setup", "update", "datasource", "ai-gateway", "analytics-agent"]) const unknownFlags = args.filter((a) => a.startsWith("-")).map((a) => a.replace(/^-+/, "").split("=")[0]).filter((a) => !KNOWN_FLAGS.has(a)) if (unknownFlags.length > 0) return `Unknown argument: ${unknownFlags[0]}` const topLevelCmd = args.find((a) => !a.startsWith("-")) diff --git a/packages/cz-cli/src/commands/analytics-agent.ts b/packages/cz-cli/src/commands/analytics-agent.ts new file mode 100644 index 0000000000..cd7e6ff25a --- /dev/null +++ b/packages/cz-cli/src/commands/analytics-agent.ts @@ -0,0 +1,756 @@ +import type { Argv } from "yargs" +import { createTraceparent } from "@clickzetta/sdk" +import type { GlobalArgs } from "../cli.js" +import { commandGroup } from "../command-group.js" +import { readAgentEndpoint } from "../connection/profile-store.js" +import { success, error, handledError, isHandledCliError } from "../output/index.js" +import { getStudioContext } from "./studio-context.js" +import { logOperation } from "../logger.js" + +const ROUTES = { + datasourceTypes: { method: "GET", path: "/open/api/v1/datasources/types" }, + datasourceSchema: { method: "GET", path: "/open/api/v1/datasources/schema" }, + datasourceList: { method: "GET", path: "/open/api/v1/datasources" }, + datasourceMeta: { method: "GET", path: (argv: Record) => `/open/api/v1/datasources/${encodePath(argv["datasource-id"])}/meta` }, + datasourceBrowse: { method: "GET", path: (argv: Record) => `/open/api/v1/datasources/${encodePath(argv["datasource-id"])}/browse` }, + datasourceSearchTables: { method: "GET", path: (argv: Record) => `/open/api/v1/datasources/${encodePath(argv["datasource-id"])}/tables/search` }, + datasourceShowTable: { method: "GET", path: (argv: Record) => `/open/api/v1/datasources/${encodePath(argv["datasource-id"])}/tables/${encodePath(argv["table-name"])}` }, + datasourceLoad: { method: "POST", path: (argv: Record) => `/open/api/v1/datasources/${encodePath(argv["datasource-id"])}/load` }, + datasourceCreate: { method: "POST", path: "/open/api/v1/datasources" }, + datasourceUpdate: { method: "PUT", path: (argv: Record) => `/open/api/v1/datasources/${encodePath(argv["datasource-id"])}` }, + datasourceDelete: { method: "DELETE", path: (argv: Record) => `/open/api/v1/datasources/${encodePath(argv["datasource-id"])}` }, + datagptEnabled: { method: "GET", path: "/open/api/v1/analytics-agent/datagpt/enabled" }, + domainList: { method: "GET", path: "/open/api/v1/analytics-agent/domains" }, + domainCreate: { method: "POST", path: "/open/api/v1/analytics-agent/domains" }, + domainUpdate: { method: "PUT", path: (argv: Record) => `/open/api/v1/analytics-agent/domains/${encodePath(argv["domain-id"])}` }, + domainDetail: { method: "GET", path: (argv: Record) => `/open/api/v1/analytics-agent/domains/${encodePath(argv["domain-id"])}` }, + domainDelete: { method: "DELETE", path: (argv: Record) => `/open/api/v1/analytics-agent/domains/${encodePath(argv["domain-id"])}` }, + domainTableAdd: { method: "POST", path: (argv: Record) => `/open/api/v1/analytics-agent/domains/${encodePath(argv["domain-id"])}/tables` }, + domainTableRemove: { method: "DELETE", path: (argv: Record) => `/open/api/v1/analytics-agent/domains/${encodePath(argv["domain-id"])}/tables/${encodePath(argv["table-id"])}` }, + sessionList: { method: "POST", path: "/open/session/list" }, + sessionCreate: { method: "POST", path: "/open/session/safe_new", openSessionAuth: true }, + sessionRun: { method: "POST", path: "/open/text2insight/query", openSessionAuth: true }, + sessionResult: { method: "POST", path: "/open/safe_question_poll", openSessionAuth: true }, + sessionStop: { method: "POST", path: "/open/text2insight/stop", openSessionAuth: true }, +} as const + +type AnalyticsRoute = { + method: string + path: string | ((argv: Record) => string) + tenantIdQuery?: boolean + openSessionAuth?: boolean +} + +interface AnalyticsRequestInfo { + method: string + path: string + query: Record + tenantId: number | string + status?: number + requestId?: string +} + +class AnalyticsHttpError extends Error { + constructor( + message: string, + readonly request: AnalyticsRequestInfo, + ) { + super(message) + } +} + +function parseJsonObject(raw: string | undefined, fieldName: string): Record { + if (!raw) return {} + try { + const parsed = JSON.parse(raw) + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error(`${fieldName} must be a JSON object`) + } + return parsed as Record + } catch (err) { + throw new Error(`Invalid ${fieldName}: ${err instanceof Error ? err.message : String(err)}`) + } +} + +function parseOptionalJsonObject(raw: string | undefined, fieldName: string): Record | undefined { + if (!raw) return undefined + return parseJsonObject(raw, fieldName) +} + +function parseJsonArray(raw: string | undefined, fieldName: string): unknown[] | undefined { + if (!raw) return undefined + try { + const parsed = JSON.parse(raw) + if (!Array.isArray(parsed)) { + throw new Error(`${fieldName} must be a JSON array`) + } + return parsed + } catch (err) { + throw new Error(`Invalid ${fieldName}: ${err instanceof Error ? err.message : String(err)}`) + } +} + +function mergeBody( + body: Record, + extra: Record, +): Record { + return Object.entries(extra).reduce>( + (result, [key, value]) => (value === undefined ? result : { ...result, [key]: value }), + { ...body }, + ) +} + +function undefinedIfEmpty(value: Record): Record | undefined { + return Object.keys(value).length === 0 ? undefined : value +} + +function encodePath(value: unknown): string { + return encodeURIComponent(String(value ?? "")) +} + +function normalizeEndpoint(value: string): string { + return value.replace(/\/+$/, "") +} + +function routePath(route: AnalyticsRoute, argv: Record): string { + return typeof route.path === "string" ? route.path : route.path(argv) +} + +function buildUrl( + endpoint: string, + route: AnalyticsRoute, + argv: Record, + query: Record, + tenantId: number | string, +): string { + const url = new URL(normalizeEndpoint(endpoint) + routePath(route, argv)) + Object.entries({ ...(route.tenantIdQuery === false ? {} : { tenantId }), ...query }).forEach(([key, value]) => { + if (value !== undefined) url.searchParams.set(key, String(value)) + }) + return url.toString() +} + +function requestInfo( + url: string, + route: AnalyticsRoute, + argv: Record, + tenantId: number | string, + status?: number, + requestId?: string, +): AnalyticsRequestInfo { + return { + method: route.method, + path: routePath(route, argv), + query: Object.fromEntries(new URL(url).searchParams.entries()), + tenantId, + ...(status !== undefined ? { status } : {}), + ...(requestId ? { requestId } : {}), + } +} + +function responseRequestId(text: string): string | undefined { + try { + const parsed = JSON.parse(text) as Record + return typeof parsed.requestId === "string" ? parsed.requestId : undefined + } catch { + return undefined + } +} + +async function requestAnalytics( + argv: Record, + route: AnalyticsRoute, + body: Record, + query: Record = {}, +): Promise { + const format = typeof argv.format === "string" ? argv.format : "json" + const endpoint = readAgentEndpoint(typeof argv.profile === "string" ? argv.profile : undefined) + if (!endpoint) { + handledError( + "NO_ANALYSIS_AGENT_ENDPOINT", + "No analysis agent endpoint configured for the active profile. Set profiles..analysis_agent_endpoint first.", + { + format, + extra: { + next_steps: [ + "cz-cli profile update analysis_agent_endpoint ", + "cz-cli profile create ... --analysis-agent-endpoint ", + ], + }, + }, + ) + } + + const studio = await getStudioContext(argv) + const headers: Record = { + "Content-Type": "application/json", + Accept: "application/json", + "x-clickzetta-token": studio.token, + Authorization: studio.token, + traceparent: createTraceparent(), + userId: String(studio.userId), + instanceId: String(studio.instanceId), + accountId: String(studio.tenantId), + tenantId: String(studio.tenantId), + instanceName: studio.instanceName, + workspaceName: studio.workspaceName, + workspaceId: String(studio.workspaceId), + projectId: String(studio.projectId), + ...studio.customHeaders, + } + const url = buildUrl(endpoint, route, argv, query, studio.tenantId) + const requestBody = route.openSessionAuth + ? mergeBody(body, { tenantId: studio.tenantId, userId: studio.userId, loginToken: studio.token }) + : body + const response = await fetch(url, { + method: route.method, + headers, + ...(route.method === "GET" ? {} : { body: JSON.stringify(requestBody) }), + signal: AbortSignal.timeout(300_000), + }) + const text = await response.text() + if (!response.ok) { + throw new AnalyticsHttpError( + `HTTP ${response.status}: ${text.slice(0, 500)}`, + requestInfo(url, route, argv, studio.tenantId, response.status, responseRequestId(text)), + ) + } + if (!text) return {} + try { + return JSON.parse(text) + } catch (err) { + throw new Error(`Invalid JSON response: ${err instanceof Error ? err.message : String(err)}`) + } +} + +function unwrapResponse(payload: unknown): unknown { + if (!payload || typeof payload !== "object" || Array.isArray(payload)) return payload + const data = (payload as Record).data + return data ?? payload +} + +/** + * Analytics Agent backend always returns HTTP 200, using `success: false` + * inside the envelope to signal business errors. Detect that here so callers + * can route to the error path instead of the success path. + * + * The response can be either: + * - `{data: {code, message, success: false, ...}}` (domain/datasource APIs) + * - `{code, message, success: false, ...}` (already unwrapped by some routes) + */ +function extractBusinessError(payload: unknown): { code: string; message: string } | null { + if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null + const p = payload as Record + + // Case 1: top-level envelope — {data: {success: false, code, message}} + const inner = p.data + if (inner && typeof inner === "object" && !Array.isArray(inner)) { + const d = inner as Record + if (d.success === false) { + return { + code: typeof d.code === "string" ? d.code : "ANALYTICS_AGENT_ERROR", + message: typeof d.message === "string" ? d.message : "Unknown error", + } + } + } + + // Case 2: already-unwrapped — {success: false, code, message} + if (p.success === false) { + return { + code: typeof p.code === "string" ? p.code : "ANALYTICS_AGENT_ERROR", + message: typeof p.message === "string" ? p.message : "Unknown error", + } + } + + return null +} + +function latestResponseDataType(payload: unknown): string { + const data = unwrapResponse(payload) + if (!data || typeof data !== "object" || Array.isArray(data)) return "" + const responses = (data as Record).responses + if (!Array.isArray(responses) || responses.length === 0) return "" + const latest = responses.at(-1) + if (!latest || typeof latest !== "object" || Array.isArray(latest)) return "" + const dataType = (latest as Record).dataType + return typeof dataType === "string" ? dataType : "" +} + +function isTerminalResponse(payload: unknown): boolean { + return ["finish", "finish_stop", "error"].includes(latestResponseDataType(payload)) +} + +async function executeAnalyticsCommand( + name: string, + argv: Record, + route: AnalyticsRoute, + body: Record, + query: Record = {}, +): Promise { + const format = typeof argv.format === "string" ? argv.format : "json" + const t0 = Date.now() + try { + const payload = await requestAnalytics(argv, route, body, query) + const bizErr = extractBusinessError(payload) + if (bizErr) { + logOperation(name, { ok: false, timeMs: Date.now() - t0 }) + error(bizErr.code, bizErr.message, { format }) + return + } + logOperation(name, { ok: true, timeMs: Date.now() - t0 }) + success(unwrapResponse(payload), { format, timeMs: Date.now() - t0 }) + } catch (err) { + logOperation(name, { ok: false, timeMs: Date.now() - t0 }) + if (isHandledCliError(err)) return + error("ANALYTICS_AGENT_ERROR", err instanceof Error ? err.message : String(err), { + format, + ...(err instanceof AnalyticsHttpError ? { extra: { request: err.request } } : {}), + }) + } +} + +async function executeAnalyticsPollCommand( + name: string, + argv: Record, + route: AnalyticsRoute, + body: Record, + query: Record = {}, +): Promise { + const format = typeof argv.format === "string" ? argv.format : "json" + const timeoutMs = typeof argv["timeout-ms"] === "number" ? argv["timeout-ms"] : 360_000 + const intervalMs = typeof argv["interval-ms"] === "number" ? argv["interval-ms"] : 2_000 + const t0 = Date.now() + try { + const deadline = Date.now() + timeoutMs + const poll = async (): Promise => { + const payload = await requestAnalytics(argv, route, body, query) + if (isTerminalResponse(payload) || Date.now() >= deadline) return payload + await Bun.sleep(intervalMs) + return poll() + } + const payload = await poll() + logOperation(name, { ok: true, timeMs: Date.now() - t0 }) + success(unwrapResponse(payload), { format, timeMs: Date.now() - t0 }) + } catch (err) { + logOperation(name, { ok: false, timeMs: Date.now() - t0 }) + if (isHandledCliError(err)) return + error("ANALYTICS_AGENT_ERROR", err instanceof Error ? err.message : String(err), { + format, + ...(err instanceof AnalyticsHttpError ? { extra: { request: err.request } } : {}), + }) + } +} + +export function registerAnalyticsAgentCommand(cli: Argv): void { + cli.command("analytics-agent", "Analytics Agent APIs", (yargs) => { + yargs + .command("datasource", "Manage Analytics Agent datasources", (datasource) => { + datasource + .command( + "types", + "List supported datasource types", + (y) => y, + async (argv) => { + await executeAnalyticsCommand("analytics-agent datasource types", argv as Record, ROUTES.datasourceTypes, {}) + }, + ) + .command( + "schema", + "Show datasource connection schema", + (y) => + y.option("type", { type: "string", demandOption: true, describe: "Datasource type" }), + async (argv) => { + await executeAnalyticsCommand("analytics-agent datasource schema", argv as Record, ROUTES.datasourceSchema, {}, { + type: argv.type, + }) + }, + ) + .command( + "list", + "List datasources", + (y) => + y + .option("name", { type: "string", describe: "Filter by datasource name" }) + .option("with-detail", { type: "boolean", describe: "Include datasource detail" }), + async (argv) => { + await executeAnalyticsCommand("analytics-agent datasource list", argv as Record, ROUTES.datasourceList, {}, { + name: argv.name, + withDetail: argv["with-detail"], + }) + }, + ) + .command( + "meta ", + "Show datasource browse metadata", + (y) => y.positional("datasource-id", { type: "number", demandOption: true, describe: "Datasource ID" }), + async (argv) => { + await executeAnalyticsCommand("analytics-agent datasource meta", argv as Record, ROUTES.datasourceMeta, {}) + }, + ) + .command( + "browse ", + "Browse datasource children", + (y) => + y + .positional("datasource-id", { type: "number", demandOption: true, describe: "Datasource ID" }) + .option("path", { type: "string", describe: "Browse path, for example workspace:w/schema:s" }) + .option("name", { type: "string", describe: "Filter child names" }) + .option("page-num", { type: "number", describe: "Page number" }) + .option("page-size", { type: "number", describe: "Page size" }), + async (argv) => { + await executeAnalyticsCommand("analytics-agent datasource browse", argv as Record, ROUTES.datasourceBrowse, {}, { + path: argv.path, + name: argv.name, + pageNum: argv["page-num"], + pageSize: argv["page-size"], + }) + }, + ) + .command( + "search-tables ", + "Search tables in a datasource", + (y) => + y + .positional("datasource-id", { type: "number", demandOption: true, describe: "Datasource ID" }) + .option("keyword", { type: "string", demandOption: true, describe: "Table search keyword" }) + .option("path", { type: "string", describe: "Search path" }) + .option("page-num", { type: "number", describe: "Page number" }) + .option("page-size", { type: "number", describe: "Page size" }), + async (argv) => { + await executeAnalyticsCommand("analytics-agent datasource search-tables", argv as Record, ROUTES.datasourceSearchTables, {}, { + keyword: argv.keyword, + path: argv.path, + pageNum: argv["page-num"], + pageSize: argv["page-size"], + }) + }, + ) + .command( + "show-table ", + "Show datasource table metadata", + (y) => + y + .positional("datasource-id", { type: "number", demandOption: true, describe: "Datasource ID" }) + .positional("table-name", { type: "string", demandOption: true, describe: "Table name" }) + .option("path", { type: "string", describe: "Table path" }) + .option("include-columns", { type: "boolean", describe: "Include column metadata" }) + .option("include-preview", { type: "boolean", describe: "Include preview rows" }) + .option("preview-size", { type: "number", describe: "Preview row count" }), + async (argv) => { + await executeAnalyticsCommand("analytics-agent datasource show-table", argv as Record, ROUTES.datasourceShowTable, {}, { + path: argv.path, + includeColumns: argv["include-columns"], + includePreview: argv["include-preview"], + previewSize: argv["preview-size"], + }) + }, + ) + .command( + "create", + "Create datasource", + (y) => + y + .option("name", { type: "string", describe: "Datasource name" }) + .option("type", { type: "string", describe: "Datasource type" }) + .option("connection", { type: "string", describe: "Datasource connection JSON object" }) + .option("validate-only", { type: "boolean", describe: "Validate connection without creating datasource" }) + .option("body", { type: "string", describe: "Full request body as JSON object" }), + async (argv) => { + const body = mergeBody(parseJsonObject(argv.body, "--body"), { + name: argv.name, + type: argv.type, + connection: parseOptionalJsonObject(argv.connection, "--connection"), + validateOnly: argv["validate-only"], + }) + await executeAnalyticsCommand("analytics-agent datasource create", argv as Record, ROUTES.datasourceCreate, body) + }, + ) + .command( + "update ", + "Update datasource", + (y) => + y + .positional("datasource-id", { type: "number", demandOption: true, describe: "Datasource ID" }) + .option("name", { type: "string", describe: "Datasource name" }) + .option("type", { type: "string", describe: "Datasource type" }) + .option("connection", { type: "string", describe: "Datasource connection JSON object" }) + .option("validate-only", { type: "boolean", describe: "Validate connection without updating datasource" }) + .option("body", { type: "string", describe: "Full request body as JSON object" }), + async (argv) => { + const body = mergeBody(parseJsonObject(argv.body, "--body"), { + name: argv.name, + type: argv.type, + connection: parseOptionalJsonObject(argv.connection, "--connection"), + validateOnly: argv["validate-only"], + }) + await executeAnalyticsCommand("analytics-agent datasource update", argv as Record, ROUTES.datasourceUpdate, body) + }, + ) + .command( + "delete ", + "Delete datasource", + (y) => y.positional("datasource-id", { type: "number", demandOption: true, describe: "Datasource ID" }), + async (argv) => { + await executeAnalyticsCommand("analytics-agent datasource delete", argv as Record, ROUTES.datasourceDelete, {}) + }, + ) + .command( + "load ", + "Load datasource table into Analytics Agent", + (y) => + y + .positional("datasource-id", { type: "number", demandOption: true, describe: "Datasource ID" }) + .option("path", { type: "string", describe: "Table path" }) + .option("table-name", { type: "string", describe: "Table name" }) + .option("display-name", { type: "string", describe: "Dataset display name" }) + .option("description", { type: "string", describe: "Dataset description" }) + .option("domain-ids", { type: "string", describe: "Domain IDs JSON array" }) + .option("mode", { type: "number", describe: "Dataset mode" }) + .option("body", { type: "string", describe: "Full request body as JSON object" }), + async (argv) => { + const body = mergeBody(parseJsonObject(argv.body, "--body"), { + path: argv.path, + tableName: argv["table-name"], + displayName: argv["display-name"], + description: argv.description, + domainIds: parseJsonArray(argv["domain-ids"], "--domain-ids"), + mode: argv.mode, + }) + await executeAnalyticsCommand("analytics-agent datasource load", argv as Record, ROUTES.datasourceLoad, body) + }, + ) + return commandGroup(datasource, "analytics-agent datasource") + }) + .command("domain", "Manage Analytics Agent domains", (domain) => { + domain + .command( + "list", + "List domains", + (y) => y.option("with-tables", { type: "boolean", describe: "Include bound tables" }), + async (argv) => { + await executeAnalyticsCommand("analytics-agent domain list", argv as Record, ROUTES.domainList, {}, { + withTables: argv["with-tables"], + }) + }, + ) + .command( + "create", + "Create domain", + (y) => + y + .option("name", { type: "string", describe: "Domain name" }) + .option("description", { type: "string", describe: "Domain description" }) + .option("datasource-id", { type: "number", describe: "Datasource ID" }) + .option("sample-questions", { type: "string", describe: "Sample questions JSON array" }) + .option("body", { type: "string", describe: "Full request body as JSON object" }), + async (argv) => { + const body = mergeBody(parseJsonObject(argv.body, "--body"), { + name: argv.name, + description: argv.description, + datasourceId: argv["datasource-id"], + sampleQuestions: parseJsonArray(argv["sample-questions"], "--sample-questions"), + }) + await executeAnalyticsCommand("analytics-agent domain create", argv as Record, ROUTES.domainCreate, body) + }, + ) + .command( + "update ", + "Update domain", + (y) => + y + .positional("domain-id", { type: "number", demandOption: true, describe: "Domain ID" }) + .option("name", { type: "string", describe: "Domain name" }) + .option("description", { type: "string", describe: "Domain description" }) + .option("datasource-id", { type: "number", describe: "Datasource ID" }) + .option("sample-questions", { type: "string", describe: "Sample questions JSON array" }) + .option("body", { type: "string", describe: "Full request body as JSON object" }), + async (argv) => { + const body = mergeBody(parseJsonObject(argv.body, "--body"), { + name: argv.name, + description: argv.description, + datasourceId: argv["datasource-id"], + sampleQuestions: parseJsonArray(argv["sample-questions"], "--sample-questions"), + }) + await executeAnalyticsCommand("analytics-agent domain update", argv as Record, ROUTES.domainUpdate, body) + }, + ) + .command( + "detail ", + "Show domain detail", + (y) => + y + .positional("domain-id", { type: "number", demandOption: true, describe: "Domain ID" }) + .option("with-tables", { type: "boolean", describe: "Include bound tables" }), + async (argv) => { + await executeAnalyticsCommand("analytics-agent domain detail", argv as Record, ROUTES.domainDetail, {}, { + withTables: argv["with-tables"], + }) + }, + ) + .command( + "delete ", + "Delete domain", + (y) => y.positional("domain-id", { type: "number", demandOption: true, describe: "Domain ID" }), + async (argv) => { + await executeAnalyticsCommand("analytics-agent domain delete", argv as Record, ROUTES.domainDelete, {}) + }, + ) + .command( + "table", + "Manage domain tables", + (table) => { + table.command( + "add ", + "Add table to domain", + (y) => + y + .positional("domain-id", { type: "number", demandOption: true, describe: "Domain ID" }) + .option("datasource-id", { type: "number", describe: "Datasource ID" }) + .option("path", { type: "string", describe: "Table path" }) + .option("table-name", { type: "string", describe: "Table name" }) + .option("display-name", { type: "string", describe: "Dataset display name" }) + .option("description", { type: "string", describe: "Dataset description" }) + .option("body", { type: "string", describe: "Full request body as JSON object" }), + async (argv) => { + const body = mergeBody(parseJsonObject(argv.body, "--body"), { + datasourceId: argv["datasource-id"], + path: argv.path, + tableName: argv["table-name"], + displayName: argv["display-name"], + description: argv.description, + }) + await executeAnalyticsCommand("analytics-agent domain table add", argv as Record, ROUTES.domainTableAdd, body) + }, + ) + table.command( + "remove ", + "Remove table from domain", + (y) => + y + .positional("domain-id", { type: "number", demandOption: true, describe: "Domain ID" }) + .positional("table-id", { type: "number", demandOption: true, describe: "Table ID" }), + async (argv) => { + await executeAnalyticsCommand("analytics-agent domain table remove", argv as Record, ROUTES.domainTableRemove, {}) + }, + ) + return commandGroup(table, "analytics-agent domain table") + }, + ) + return commandGroup(domain, "analytics-agent domain") + }) + .command("service", "Check Analytics Agent service capability", (service) => { + service.command( + "enabled", + "Check whether the current tenant has Analytics Agent enabled", + (y) => y, + async (argv) => { + await executeAnalyticsCommand("analytics-agent service enabled", argv as Record, ROUTES.datagptEnabled, {}) + }, + ) + return commandGroup(service, "analytics-agent service") + }) + .command("session", "Manage Analytics Agent text2insight sessions", (session) => { + session + .command( + "list", + "List text2insight sessions", + (y) => + y + .option("domain-id", { type: "number", demandOption: true, describe: "Domain ID" }) + .option("source-type", { type: "string", describe: "Session sourceType" }) + .option("source-id", { type: "number", describe: "Session sourceId" }) + .option("body", { type: "string", describe: "Full request body as JSON object" }), + async (argv) => { + const body = mergeBody(parseJsonObject(argv.body, "--body"), { + domainId: argv["domain-id"], + sourceType: argv["source-type"], + sourceId: argv["source-id"], + }) + await executeAnalyticsCommand("analytics-agent session list", argv as Record, ROUTES.sessionList, body) + }, + ) + .command( + "create", + "Create a safe text2insight session", + (y) => + y + .option("domain-id", { type: "number", demandOption: true, describe: "Domain ID" }) + .option("title", { type: "string", describe: "Session title" }) + .option("source-type", { type: "string", describe: "Session sourceType" }) + .option("source-id", { type: "number", describe: "Session sourceId" }) + .option("body", { type: "string", describe: "Full request body as JSON object" }), + async (argv) => { + const body = mergeBody(parseJsonObject(argv.body, "--body"), { + domainId: argv["domain-id"], + title: argv.title, + sourceType: argv["source-type"], + sourceId: argv["source-id"], + }) + await executeAnalyticsCommand("analytics-agent session create", argv as Record, ROUTES.sessionCreate, body) + }, + ) + .command( + "run [session-id]", + "Start a text2insight query in a session", + (y) => + y + .positional("session-id", { type: "number", describe: "Session ID" }) + .option("domain-id", { type: "number", describe: "Domain ID" }) + .option("msg", { type: "string", describe: "Question text" }) + .option("model-name", { type: "string", describe: "Model name" }) + .option("body", { type: "string", describe: "Full request body as JSON object" }), + async (argv) => { + const modelSettings = undefinedIfEmpty(mergeBody({}, { + model_name: argv["model-name"], + })) + const body = mergeBody(parseJsonObject(argv.body, "--body"), { + domainId: argv["domain-id"], + sessionId: argv["session-id"], + msg: argv.msg, + modelSettings, + }) + await executeAnalyticsCommand("analytics-agent session run", argv as Record, ROUTES.sessionRun, body) + }, + ) + .command( + "result ", + "Poll a text2insight question result", + (y) => + y + .positional("question-id", { type: "number", demandOption: true, describe: "Question ID" }) + .option("wait", { type: "boolean", describe: "Poll until finish, finish_stop, error, or timeout" }) + .option("interval-ms", { type: "number", describe: "Polling interval in milliseconds" }) + .option("timeout-ms", { type: "number", describe: "Polling timeout in milliseconds" }) + .option("body", { type: "string", describe: "Full request body as JSON object" }), + async (argv) => { + const body = mergeBody(parseJsonObject(argv.body, "--body"), { + questionId: argv["question-id"], + }) + if (argv.wait) { + await executeAnalyticsPollCommand("analytics-agent session result", argv as Record, ROUTES.sessionResult, body) + return + } + await executeAnalyticsCommand("analytics-agent session result", argv as Record, ROUTES.sessionResult, body) + }, + ) + .command( + "stop [session-id] [question-id]", + "Stop a running text2insight question", + (y) => + y + .positional("session-id", { type: "number", describe: "Session ID" }) + .positional("question-id", { type: "number", describe: "Question ID" }) + .option("body", { type: "string", describe: "Full request body as JSON object" }), + async (argv) => { + const body = mergeBody(parseJsonObject(argv.body, "--body"), { + sessionId: argv["session-id"], + questionId: argv["question-id"], + }) + await executeAnalyticsCommand("analytics-agent session stop", argv as Record, ROUTES.sessionStop, body) + }, + ) + return commandGroup(session, "analytics-agent session") + }) + return commandGroup(yargs, "analytics-agent") + }) +} diff --git a/packages/cz-cli/src/commands/profile.ts b/packages/cz-cli/src/commands/profile.ts index 052a49a9da..d627016238 100644 --- a/packages/cz-cli/src/commands/profile.ts +++ b/packages/cz-cli/src/commands/profile.ts @@ -59,6 +59,7 @@ function parseJdbcUrl(jdbc: string): JdbcConfig | undefined { const VALID_UPDATE_KEYS = [ "pat", "username", "password", "service", "protocol", "instance", "workspace", "schema", "vcluster", + "analysis_agent_endpoint", ] function loadFullFile(): Record { @@ -109,6 +110,9 @@ export function registerProfileCommand(cli: Argv): void { workspace: String(p.workspace ?? ""), is_default: name === defaultProfile, } + if (typeof p.analysis_agent_endpoint === "string") { + entry.analysis_agent_endpoint = p.analysis_agent_endpoint + } if (argv["show-secret"] && !pat && p.password) { entry.password = String(p.password) } @@ -181,6 +185,7 @@ export function registerProfileCommand(cli: Argv): void { .option("workspace", { type: "string", describe: "Workspace name" }) .option("schema", { type: "string", describe: "Default schema" }) .option("vcluster", { type: "string", describe: "Virtual cluster" }) + .option("analysis-agent-endpoint", { type: "string", describe: "Analysis agent endpoint" }) .option("header", { type: "string", array: true, describe: "Custom HTTP header KEY=VALUE (repeatable)" }) .option("skip-verify", { type: "boolean", default: false, describe: "Skip connection verification" }), async (argv) => { @@ -221,6 +226,7 @@ export function registerProfileCommand(cli: Argv): void { workspace: ws, schema: argv.schema ?? jdbcCfg?.schema ?? "public", vcluster: argv.vcluster ?? jdbcCfg?.vcluster ?? "default", + ...(argv["analysis-agent-endpoint"] ? { analysis_agent_endpoint: argv["analysis-agent-endpoint"] } : {}), } if (hasPat) { profileObj.pat = argv.pat! @@ -284,11 +290,11 @@ export function registerProfileCommand(cli: Argv): void { ) .command( "update ", - `Update a profile field. Valid keys: pat, username, password, service, protocol, instance, workspace, schema, vcluster, header.`, + `Update a profile field. Valid keys: pat, username, password, service, protocol, instance, workspace, schema, vcluster, analysis_agent_endpoint, header.`, (y) => y .positional("name", { type: "string", demandOption: true, describe: "Profile name" }) - .positional("key", { type: "string", demandOption: true, describe: "Field to update: pat | username | password | service | protocol | instance | workspace | schema | vcluster | header." }) + .positional("key", { type: "string", demandOption: true, describe: "Field to update: pat | username | password | service | protocol | instance | workspace | schema | vcluster | analysis_agent_endpoint | header." }) .positional("value", { type: "string", demandOption: true, describe: "New value" }), (argv) => { const format = argv.format diff --git a/packages/cz-cli/src/commands/setup.ts b/packages/cz-cli/src/commands/setup.ts index 9e9a2c3b4a..c33e653bb0 100644 --- a/packages/cz-cli/src/commands/setup.ts +++ b/packages/cz-cli/src/commands/setup.ts @@ -235,6 +235,7 @@ function applyCredentialToProfiles( pat: String(cred.accessToken), service: String(cred.service ?? "dev-api.clickzetta.com"), protocol: String(cred.protocol ?? "https"), + ...(typeof cred.analysisAgentEndpoint === "string" ? { analysis_agent_endpoint: cred.analysisAgentEndpoint } : {}), ...(typeof cred.aimeshEndpointBaseUrl === "string" && { ai_gateway_url: String(cred.aimeshEndpointBaseUrl) }), }, }, @@ -1727,6 +1728,7 @@ export function registerSetupCommand(cli: Argv): void { pat: accessToken, service: (cred.service as string) ?? "dev-api.clickzetta.com", protocol: (cred.protocol as string) ?? "https", + ...(typeof cred.analysisAgentEndpoint === "string" ? { analysis_agent_endpoint: cred.analysisAgentEndpoint } : {}), } try { const data = loadFullFile() diff --git a/packages/cz-cli/src/connection/profile-store.ts b/packages/cz-cli/src/connection/profile-store.ts index ddeda17d85..06b50bb33f 100644 --- a/packages/cz-cli/src/connection/profile-store.ts +++ b/packages/cz-cli/src/connection/profile-store.ts @@ -138,7 +138,12 @@ export function readAgentEndpoint(profileName?: string): string | undefined { const name = profileName ?? (data.default_profile as string | undefined) ?? Object.keys((data.profiles ?? {}) as Record)[0] if (!name) return undefined const profiles = (data.profiles ?? {}) as Record> - const agent = profiles[name]?.agent as Record | undefined + const profile = profiles[name] + if (!profile) return undefined + if (typeof profile.analysis_agent_endpoint === "string" && profile.analysis_agent_endpoint) { + return profile.analysis_agent_endpoint + } + const agent = profile.agent as Record | undefined return (agent?.endpoint as string) || undefined } catch { return undefined diff --git a/packages/cz-cli/src/register-commands.ts b/packages/cz-cli/src/register-commands.ts index f065bcfdd1..fab841619b 100644 --- a/packages/cz-cli/src/register-commands.ts +++ b/packages/cz-cli/src/register-commands.ts @@ -15,6 +15,7 @@ import { registerSetupCommand } from "./commands/setup.js" import { registerUpdateCommand } from "./commands/update.js" import { registerDatasourceCommand } from "./commands/datasource.js" import { registerGatewayCommand } from "./commands/ai-gateway.js" +import { registerAnalyticsAgentCommand } from "./commands/analytics-agent.js" export function registerCommands(cli: Argv): Argv { registerSqlCommand(cli) @@ -32,5 +33,6 @@ export function registerCommands(cli: Argv): Argv { registerUpdateCommand(cli) registerDatasourceCommand(cli) registerGatewayCommand(cli) + registerAnalyticsAgentCommand(cli) return cli } diff --git a/packages/cz-cli/src/run-cli.ts b/packages/cz-cli/src/run-cli.ts index fdbf0db69c..ecc2f00abb 100644 --- a/packages/cz-cli/src/run-cli.ts +++ b/packages/cz-cli/src/run-cli.ts @@ -31,6 +31,7 @@ const PROFILE_REQUIRED_COMMANDS = new Set([ "attempts", "job", "datasource", + "analytics-agent", ]) const LLM_ONBOARDING = { diff --git a/packages/cz-cli/test/e2e-command-surface.ts b/packages/cz-cli/test/e2e-command-surface.ts index 2d3b20b07c..dd9d3cd44e 100644 --- a/packages/cz-cli/test/e2e-command-surface.ts +++ b/packages/cz-cli/test/e2e-command-surface.ts @@ -5,7 +5,7 @@ * Run: bun test/e2e-command-surface.ts */ import { spawnSync } from "child_process" -import { mkdirSync, rmSync } from "fs" +import { mkdirSync, rmSync, writeFileSync } from "fs" import { join } from "path" import { tmpdir } from "os" @@ -40,6 +40,26 @@ function withFakeHome() { return { home, cleanup: () => rmSync(home, { recursive: true, force: true }) } } +function withProfileButNoAnalysisEndpoint() { + const { home, cleanup } = withFakeHome() + writeFileSync( + join(home, ".clickzetta", "profiles.toml"), + [ + 'default_profile = "default"', + "", + "[profiles.default]", + 'pat = "test-pat"', + 'service = "cn-shanghai-alicloud.api.clickzetta.com"', + 'instance = "test-instance"', + 'workspace = "test-workspace"', + 'schema = "public"', + 'vcluster = "default"', + "", + ].join("\n"), + ) + return { home, cleanup } +} + interface TestCase { name: string run: () => { pass: boolean; detail?: string } @@ -74,6 +94,12 @@ const noProfileCases = [ ["job", "profile", "1"], ["datasource", "list"], ["datasource", "catalogs", "ds"], + ["analytics-agent", "datasource", "list"], + ["analytics-agent", "service", "enabled"], + ["analytics-agent", "session", "create"], + ["analytics-agent", "session", "run", "1"], + ["analytics-agent", "session", "result", "1"], + ["analytics-agent", "session", "stop", "1", "1"], ] as const const tests: TestCase[] = [ @@ -99,6 +125,23 @@ const tests: TestCase[] = [ } finally { cleanup() } }, }, + { + name: "ANALYTICS_AGENT: configured profile without analysis_agent_endpoint returns a dedicated config error", + run() { + const { home, cleanup } = withProfileButNoAnalysisEndpoint() + try { + const result = run(["analytics-agent", "service", "enabled"], { HOME: home, CLICKZETTA_TEST_HOME: home }) + const combined = result.stdout + result.stderr + if (result.exitCode !== 1) { + return { pass: false, detail: `exit=${result.exitCode}` } + } + if (!expectCode(result.stdout, "NO_ANALYSIS_AGENT_ENDPOINT")) { + return { pass: false, detail: `unexpected output=${combined.slice(0, 200)}` } + } + return { pass: true } + } finally { cleanup() } + }, + }, { name: "AGENT_ENTRY: bare agent and agent run return NO_ACTIVE_LLM without a configured llm", run() { diff --git a/packages/cz-cli/test/profile-store.test.ts b/packages/cz-cli/test/profile-store.test.ts new file mode 100644 index 0000000000..1d257c35c3 --- /dev/null +++ b/packages/cz-cli/test/profile-store.test.ts @@ -0,0 +1,54 @@ +import { afterEach, describe, expect, test } from "bun:test" +import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import path from "node:path" + +const previousHome = process.env.HOME +const previousTestHome = process.env.CLICKZETTA_TEST_HOME + +afterEach(() => { + if (previousHome === undefined) delete process.env.HOME + else process.env.HOME = previousHome + if (previousTestHome === undefined) delete process.env.CLICKZETTA_TEST_HOME + else process.env.CLICKZETTA_TEST_HOME = previousTestHome +}) + +function writeProfilesToml(content: string) { + const home = mkdtempSync(path.join(tmpdir(), "cz-cli-profile-store-")) + const clickzettaDir = path.join(home, ".clickzetta") + mkdirSync(clickzettaDir, { recursive: true }) + writeFileSync(path.join(clickzettaDir, "profiles.toml"), content) + process.env.HOME = home + process.env.CLICKZETTA_TEST_HOME = home +} + +describe("readAgentEndpoint", () => { + test("prefers profiles..analysis_agent_endpoint", async () => { + writeProfilesToml([ + 'default_profile = "default"', + "", + "[profiles.default]", + 'analysis_agent_endpoint = "https://analysis-agent.clickzetta.com"', + "[profiles.default.agent]", + 'endpoint = "https://legacy-agent.clickzetta.com"', + "", + ].join("\n")) + + const { readAgentEndpoint } = await import(`../src/connection/profile-store.ts?${Date.now()}`) + expect(readAgentEndpoint()).toBe("https://analysis-agent.clickzetta.com") + }) + + test("falls back to profiles..agent.endpoint", async () => { + writeProfilesToml([ + 'default_profile = "default"', + "", + "[profiles.default]", + "[profiles.default.agent]", + 'endpoint = "https://legacy-agent.clickzetta.com"', + "", + ].join("\n")) + + const { readAgentEndpoint } = await import(`../src/connection/profile-store.ts?${Date.now()}`) + expect(readAgentEndpoint()).toBe("https://legacy-agent.clickzetta.com") + }) +}) diff --git a/packages/cz-cli/test/setup-credential-llm.test.ts b/packages/cz-cli/test/setup-credential-llm.test.ts index 725003c430..6059a84500 100644 --- a/packages/cz-cli/test/setup-credential-llm.test.ts +++ b/packages/cz-cli/test/setup-credential-llm.test.ts @@ -64,6 +64,7 @@ describe("setup --credential", () => { schema: "clickzetta_account", virtualCluster: "CXH_TEST_1", accessToken: "czt_test_pat", + analysisAgentEndpoint: "https://analysis-agent.clickzetta.com", apiKey: "ck_test_api_key", aimeshEndpointBaseUrl: "https://uat-aimesh.clickzetta.com/", }) @@ -88,6 +89,7 @@ describe("setup --credential", () => { pat: "czt_test_pat", service: "https://uat-api.clickzetta.com", protocol: "https", + analysis_agent_endpoint: "https://analysis-agent.clickzetta.com", ai_gateway_url: "https://uat-aimesh.clickzetta.com/", }, }) diff --git a/packages/opencode/src/cli/cmd/setup.ts b/packages/opencode/src/cli/cmd/setup.ts index 88ef9228f4..6215064176 100644 --- a/packages/opencode/src/cli/cmd/setup.ts +++ b/packages/opencode/src/cli/cmd/setup.ts @@ -49,6 +49,7 @@ interface Credential { accessToken?: string apiKey?: string aimeshEndpointBaseUrl?: string + analysisAgentEndpoint?: string } function isRecord(value: unknown): value is Record { @@ -78,6 +79,7 @@ export function applyCredentialToProfiles( ...(cred.service && { service: cred.service }), protocol: cred.service?.startsWith("http://") ? "http" : "https", ...(cred.username && { username: cred.username }), + ...(typeof cred.analysisAgentEndpoint === "string" && { analysis_agent_endpoint: cred.analysisAgentEndpoint }), ...(cred.aimeshEndpointBaseUrl && { ai_gateway_url: cred.aimeshEndpointBaseUrl }), } diff --git a/packages/opencode/src/main.ts b/packages/opencode/src/main.ts index 3dd2d448aa..f51041c0da 100644 --- a/packages/opencode/src/main.ts +++ b/packages/opencode/src/main.ts @@ -185,6 +185,7 @@ export async function main(args: string[], agentRuntime = false): Promise