@embrasure/ember 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,125 @@
1
+ import type { EmbrasureApiClient } from "@embrasure/api-client";
2
+ import type { PluginCatalogEdit, PluginIngestionEdit } from "./types.js";
3
+ export declare function editPluginCatalog(client: EmbrasureApiClient, workspaceId: string, input: PluginCatalogEdit): Promise<{
4
+ preview: boolean;
5
+ key: string;
6
+ title: string;
7
+ summary: string;
8
+ object_ids: string[];
9
+ warnings: string[];
10
+ jobs: {
11
+ kind: string;
12
+ status: string;
13
+ }[];
14
+ object_id?: undefined;
15
+ current?: undefined;
16
+ changes?: undefined;
17
+ object?: undefined;
18
+ } | {
19
+ preview: boolean;
20
+ object_id: string;
21
+ current: {
22
+ object_id: string;
23
+ title: unknown;
24
+ summary: unknown;
25
+ verification_status: unknown;
26
+ truth_kind: unknown;
27
+ };
28
+ changes: {
29
+ note?: string | undefined;
30
+ summary?: string | undefined;
31
+ title?: string | undefined;
32
+ };
33
+ key?: undefined;
34
+ title?: undefined;
35
+ summary?: undefined;
36
+ object_ids?: undefined;
37
+ warnings?: undefined;
38
+ jobs?: undefined;
39
+ object?: undefined;
40
+ } | {
41
+ preview: boolean;
42
+ object_id: string;
43
+ object: {
44
+ object_id: string;
45
+ title: unknown;
46
+ summary: unknown;
47
+ verification_status: unknown;
48
+ truth_kind: unknown;
49
+ };
50
+ warnings: unknown;
51
+ key?: undefined;
52
+ title?: undefined;
53
+ summary?: undefined;
54
+ object_ids?: undefined;
55
+ jobs?: undefined;
56
+ current?: undefined;
57
+ changes?: undefined;
58
+ }>;
59
+ export declare function editPluginIngestion(client: EmbrasureApiClient, workspaceId: string, input: PluginIngestionEdit): Promise<{
60
+ ingestion_run_id: string;
61
+ table: {
62
+ table_id: string;
63
+ source_schema: string;
64
+ source_table: string;
65
+ destination_schema: string;
66
+ destination_table: string;
67
+ selected_columns: string[];
68
+ primary_key_columns: string[];
69
+ cursor_column: string | null | undefined;
70
+ status: string;
71
+ source_columns: {};
72
+ schema_change_status: string;
73
+ };
74
+ preview?: undefined;
75
+ table_id?: undefined;
76
+ source_schema?: undefined;
77
+ source_table?: undefined;
78
+ current_columns?: undefined;
79
+ requested_columns?: undefined;
80
+ effective_columns?: undefined;
81
+ primary_key_columns?: undefined;
82
+ cursor_column?: undefined;
83
+ warning?: undefined;
84
+ status?: undefined;
85
+ } | {
86
+ preview: boolean;
87
+ ingestion_run_id: string;
88
+ table_id: string;
89
+ source_schema: string;
90
+ source_table: string;
91
+ current_columns: string[];
92
+ requested_columns: string[];
93
+ effective_columns: string[];
94
+ primary_key_columns: string[];
95
+ cursor_column: string | null | undefined;
96
+ warning: string;
97
+ table?: undefined;
98
+ status?: undefined;
99
+ } | {
100
+ preview: boolean;
101
+ ingestion_run_id: string;
102
+ table_id: string;
103
+ status: string;
104
+ table: {
105
+ table_id: string;
106
+ source_schema: string;
107
+ source_table: string;
108
+ destination_schema: string;
109
+ destination_table: string;
110
+ selected_columns: string[];
111
+ primary_key_columns: string[];
112
+ cursor_column: string | null | undefined;
113
+ status: string;
114
+ source_columns: {};
115
+ schema_change_status: string;
116
+ };
117
+ source_schema?: undefined;
118
+ source_table?: undefined;
119
+ current_columns?: undefined;
120
+ requested_columns?: undefined;
121
+ effective_columns?: undefined;
122
+ primary_key_columns?: undefined;
123
+ cursor_column?: undefined;
124
+ warning?: undefined;
125
+ }>;
@@ -0,0 +1,73 @@
1
+ export async function editPluginCatalog(client, workspaceId, input) {
2
+ const preview = input.preview !== false;
3
+ if (input.action === "save") {
4
+ // Namespace stable keys so saving plugin notes cannot overwrite catalog anchors.
5
+ const ref = `embrasure-plugin:${input.key}`;
6
+ const data = await client.writeContext({
7
+ workspace_id: workspaceId, source: { kind: "manual", ref }, mode: "append",
8
+ processing_mode: "sync", apply_policy: preview ? "dry_run" : "auto",
9
+ content: { objects: [{
10
+ stable_key: ref, object_type: "memory", title: input.title, summary: input.summary,
11
+ truth_kind: "semantic_claim", verification_status: "observed", confidence: 1,
12
+ }] },
13
+ infer: { objects: false, relationships: false, semantic_definitions: false, taxonomy_links: false, extract_facts: false },
14
+ });
15
+ return { preview, key: input.key, title: input.title, summary: input.summary,
16
+ object_ids: data.applied.object_ids, warnings: data.warnings,
17
+ jobs: data.jobs.map(({ kind, status }) => ({ kind, status })) };
18
+ }
19
+ if (input.title === undefined && input.summary === undefined)
20
+ throw new Error("Provide a title or summary to correct.");
21
+ if (preview) {
22
+ const current = await client.getContextObject(workspaceId, input.object_id);
23
+ return { preview: true, object_id: input.object_id, current: contextSummary(current.object), changes: {
24
+ ...(input.title !== undefined ? { title: input.title } : {}),
25
+ ...(input.summary !== undefined ? { summary: input.summary } : {}),
26
+ ...(input.note !== undefined ? { note: input.note } : {}),
27
+ } };
28
+ }
29
+ const data = await client.request(`/v1/context/objects/${encodeURIComponent(input.object_id)}/correct`, {
30
+ method: "POST", body: { workspace_id: workspaceId, title: input.title, summary: input.summary, note: input.note },
31
+ });
32
+ if (data.ok !== true || data.object_id !== input.object_id)
33
+ throw new Error("The API did not confirm this context edit. Inspect the object before retrying.");
34
+ return { preview: false, object_id: input.object_id, object: contextSummary(data.object), warnings: data.warnings };
35
+ }
36
+ function contextSummary(value) {
37
+ const object = value;
38
+ if (!object || typeof object.id !== "string")
39
+ throw new Error("The API did not return the context object. Inspect the catalog before retrying.");
40
+ return { object_id: object.id, title: object.title, summary: object.summary, verification_status: object.verification_status, truth_kind: object.truth_kind };
41
+ }
42
+ export async function editPluginIngestion(client, workspaceId, input) {
43
+ const connection = await client.getWarehouseIngestionConnection(workspaceId, input.ingestion_run_id);
44
+ const table = connection.tables.find((item) => item.id === input.table_id);
45
+ if (!table)
46
+ throw new Error("Ingestion table was not found in this connection and workspace.");
47
+ const describeTable = (value) => ({
48
+ table_id: value.id, source_schema: value.source_schema, source_table: value.source_table,
49
+ destination_schema: value.destination_schema, destination_table: value.destination_table,
50
+ selected_columns: value.selected_columns, primary_key_columns: value.primary_key_columns,
51
+ cursor_column: value.cursor_column, status: value.status,
52
+ source_columns: value.metadata?.all_source_columns ?? value.metadata?.source_columns ?? [],
53
+ schema_change_status: value.schema_change_status,
54
+ });
55
+ if (input.action === "inspect_table")
56
+ return { ingestion_run_id: input.ingestion_run_id, table: describeTable(table) };
57
+ if (input.preview !== false) {
58
+ return {
59
+ preview: true, ingestion_run_id: input.ingestion_run_id, table_id: input.table_id,
60
+ source_schema: table.source_schema, source_table: table.source_table,
61
+ current_columns: table.selected_columns, requested_columns: input.selected_columns,
62
+ effective_columns: [...new Set([...input.selected_columns, ...table.primary_key_columns, ...(table.cursor_column ? [table.cursor_column] : [])])],
63
+ primary_key_columns: table.primary_key_columns, cursor_column: table.cursor_column,
64
+ warning: "Required key and cursor columns are retained. Removing columns can affect downstream queries. Flow stages changes for resync; inspect the returned status before claiming they are active.",
65
+ };
66
+ }
67
+ const data = await client.updateWarehouseIngestionTable(workspaceId, input.ingestion_run_id, input.table_id, { selected_columns: input.selected_columns });
68
+ const updated = data.tables.find((item) => item.id === input.table_id);
69
+ if (!updated)
70
+ throw new Error("The API did not return the edited table. Inspect ingestion status before retrying.");
71
+ return { preview: false, ingestion_run_id: input.ingestion_run_id, table_id: input.table_id,
72
+ status: data.status, table: describeTable(updated) };
73
+ }
@@ -0,0 +1,71 @@
1
+ import type { EmbrasureApiClient } from "@embrasure/api-client";
2
+ import type { PluginPolicyInput } from "./types.js";
3
+ /** Requirements and reported evidence only; this never executes customer code. */
4
+ export declare function operatePluginPolicy(client: EmbrasureApiClient, workspaceId: string, input: PluginPolicyInput): Promise<import("@embrasure/api-client").DataPolicyContext | import("@embrasure/api-client").SaveDataPolicyResponse | import("@embrasure/api-client").RecordDataPolicyAssessmentResponse | {
5
+ assessments: import("@embrasure/api-client").DataPolicyAssessment[];
6
+ assessment_history: {
7
+ total_count: number;
8
+ omitted_count: number;
9
+ selection: string;
10
+ policy_url: string;
11
+ };
12
+ policy_id: string;
13
+ key: string;
14
+ title: string;
15
+ revision: string;
16
+ source: {
17
+ content: string;
18
+ url: string | null;
19
+ };
20
+ requirements: import("@embrasure/api-client").DataPolicyRequirement[];
21
+ object_ids: string[];
22
+ assets: import("@embrasure/api-client").DataPolicyAsset[];
23
+ edges: Array<Record<string, unknown>>;
24
+ coverage: {
25
+ status: "bounded";
26
+ gaps: string[];
27
+ fingerprint: string;
28
+ };
29
+ implementation_guidance: string[];
30
+ synthetic?: boolean;
31
+ execution?: import("@embrasure/api-client").DataPolicyDemoState;
32
+ prepared_implementation?: {
33
+ status: "ready" | "needs_review";
34
+ implementation_revision: string;
35
+ source_path: string;
36
+ requirement_ids: string[];
37
+ unresolved_requirement_ids: string[];
38
+ blockers: string[];
39
+ steps: Array<{
40
+ id: string;
41
+ title: string;
42
+ requirement_ids: string[];
43
+ operation: string;
44
+ verification: string;
45
+ }>;
46
+ };
47
+ preview?: undefined;
48
+ requirement_count?: undefined;
49
+ assessment?: undefined;
50
+ } | {
51
+ preview: boolean;
52
+ policy_id: string;
53
+ revision: string;
54
+ title: string;
55
+ requirement_count: number;
56
+ assessment?: undefined;
57
+ } | {
58
+ preview: boolean;
59
+ policy_id: string;
60
+ revision: string;
61
+ assessment: {
62
+ id: string;
63
+ status: "reported";
64
+ policy_revision: string;
65
+ code_revision: string;
66
+ check_count: number;
67
+ created_at: string;
68
+ };
69
+ title?: undefined;
70
+ requirement_count?: undefined;
71
+ }>;
@@ -0,0 +1,47 @@
1
+ /** Requirements and reported evidence only; this never executes customer code. */
2
+ export async function operatePluginPolicy(client, workspaceId, input) {
3
+ if (input.action === "policy_context") {
4
+ const data = await client.getDataPolicyContext(workspaceId, input.policy_id);
5
+ if (data.policy_id !== input.policy_id)
6
+ throw new Error("The API did not return the selected policy. Inspect the policy handle before retrying.");
7
+ // Older API responses may omit history; preserve that shape rather than inventing an empty assessment.
8
+ if (!Array.isArray(data.assessments))
9
+ return data;
10
+ const assessments = [...data.assessments].sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at)).slice(0, 1);
11
+ return {
12
+ ...data,
13
+ assessments,
14
+ assessment_history: {
15
+ total_count: data.assessments.length,
16
+ omitted_count: Math.max(0, data.assessments.length - assessments.length),
17
+ selection: "Latest recorded assessment; older assessments are available on the policy page. This does not establish current or complete coverage.",
18
+ policy_url: `https://app.embrasure.ai/context/policies?policy=${encodeURIComponent(data.policy_id)}&view=evidence`,
19
+ },
20
+ };
21
+ }
22
+ if (input.action === "save_requirements") {
23
+ const { action: _action, ...body } = input;
24
+ const data = await client.saveDataPolicy(workspaceId, body);
25
+ if (data.preview !== (input.preview !== false) || !data.policy_id || !data.revision || data.context?.policy_id !== data.policy_id) {
26
+ throw new Error("The API did not confirm this requirement source. Read its context before retrying or claiming it was saved.");
27
+ }
28
+ return data.preview ? data : {
29
+ preview: false, policy_id: data.policy_id, revision: data.revision,
30
+ title: data.context.title, requirement_count: data.context.requirements.length,
31
+ };
32
+ }
33
+ const { action: _action, policy_id, ...body } = input;
34
+ const data = await client.recordDataPolicyAssessment(workspaceId, policy_id, body);
35
+ if (data.preview !== (input.preview !== false) || data.assessment?.status !== "reported"
36
+ || data.assessment.policy_revision !== input.policy_revision || data.assessment.code_revision !== input.code_revision
37
+ || data.assessment.scope_fingerprint !== input.scope_fingerprint
38
+ || data.context?.policy_id !== policy_id) {
39
+ throw new Error("The API did not confirm the reported assessment. Read policy context before retrying. Reported results are not independent verification.");
40
+ }
41
+ return data.preview ? data : {
42
+ preview: false, policy_id, revision: data.context.revision,
43
+ assessment: { id: data.assessment.id, status: data.assessment.status,
44
+ policy_revision: data.assessment.policy_revision, code_revision: data.assessment.code_revision,
45
+ check_count: data.assessment.checks.length, created_at: data.assessment.created_at },
46
+ };
47
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function createProgram(): Command;
3
+ export declare function runCli(argv?: string[]): Promise<void>;
@@ -0,0 +1,274 @@
1
+ import { EMBER_VERSION } from "./version.js";
2
+ import { spawn } from "node:child_process";
3
+ import { readFile } from "node:fs/promises";
4
+ import { Command, Option } from "commander";
5
+ import { browserLogin, logout, resolveRuntime } from "./auth.js";
6
+ import { serveEmberMcpStdio } from "./mcp.js";
7
+ import { EmberOperator } from "./operator.js";
8
+ function globals(command) { return command.optsWithGlobals(); }
9
+ async function runtime(command) {
10
+ const options = globals(command);
11
+ return resolveRuntime({ apiBaseUrl: options.apiUrl, webBaseUrl: options.webUrl, workspaceId: options.workspace });
12
+ }
13
+ function operator(current) {
14
+ if (!current.accessToken)
15
+ throw new Error("Not signed in. Run `ember auth login` or set EMBER_API_TOKEN.");
16
+ if (!current.workspaceId)
17
+ throw new Error("No workspace selected. Sign in again or set EMBER_WORKSPACE_ID.");
18
+ return new EmberOperator({ apiBaseUrl: current.apiBaseUrl, accessToken: current.accessToken, workspaceId: current.workspaceId, clientName: "ember-cli" });
19
+ }
20
+ function output(command, value, human) {
21
+ const options = globals(command);
22
+ if (options.quiet)
23
+ return;
24
+ process.stdout.write(options.json ? `${JSON.stringify(value)}\n` : `${human ?? render(value)}\n`);
25
+ }
26
+ function render(value, indent = 0) {
27
+ if (value === null || typeof value !== "object")
28
+ return String(value);
29
+ if (Array.isArray(value))
30
+ return value.map((item) => `${" ".repeat(indent)}- ${render(item, indent + 2)}`).join("\n");
31
+ return Object.entries(value).map(([key, item]) => {
32
+ if (item && typeof item === "object")
33
+ return `${" ".repeat(indent)}${key}:\n${render(item, indent + 2)}`;
34
+ return `${" ".repeat(indent)}${key}: ${String(item)}`;
35
+ }).join("\n");
36
+ }
37
+ export function createProgram() {
38
+ const program = new Command().name("ember").description("Operate your Ember data warehouse.").version(EMBER_VERSION)
39
+ .option("--api-url <url>", "Embrasure API URL", process.env.EMBER_API_BASE_URL)
40
+ .option("--web-url <url>", "Embrasure app URL", process.env.EMBER_WEB_BASE_URL)
41
+ .option("--workspace <id>", "Workspace ID", process.env.EMBER_WORKSPACE_ID)
42
+ .option("--json", "Write one JSON document to stdout")
43
+ .option("--quiet", "Suppress normal output");
44
+ program.command("setup")
45
+ .description("Idempotently set up the managed Ember warehouse and show MCP configuration")
46
+ .addOption(new Option("--client <client>", "MCP client").choices(["auto", "codex", "claude", "cursor", "generic"]).default("auto"))
47
+ .option("--plan", "Show the setup plan without applying it")
48
+ .action(async (options, command) => {
49
+ const result = await operator(await runtime(command)).setup({ action: options.plan ? "plan" : "apply" });
50
+ const mcp = options.plan ? clientSetupInstructions(options.client) : await configureClient(options.client);
51
+ output(command, { ...result, mcp }, `${render(result)}\n\nMCP: ${mcp.instruction}`);
52
+ });
53
+ const auth = program.command("auth").description("Manage the saved browser session");
54
+ auth.command("login").option("--no-open", "Do not open the browser").action(async (options, command) => {
55
+ const next = await browserLogin(await runtime(command), options.open);
56
+ output(command, { authenticated: true, workspace_id: next.workspaceId }, `Signed in. Workspace: ${next.workspaceId}`);
57
+ });
58
+ auth.command("status").action(async (_options, command) => {
59
+ const current = await runtime(command);
60
+ const authenticated = Boolean(current.accessToken);
61
+ output(command, { authenticated, workspace_id: current.workspaceId, api_base_url: current.apiBaseUrl }, authenticated ? `Signed in. Workspace: ${current.workspaceId}` : "Not signed in.");
62
+ if (!authenticated)
63
+ process.exitCode = 2;
64
+ });
65
+ auth.command("logout").action(async (_options, command) => { const result = await logout(); output(command, { authenticated: false, session_revoked: result.sessionRevoked }, result.sessionRevoked ? "Signed out and revoked the session." : "Signed out locally."); });
66
+ program.command("status").description("Show warehouse readiness and freshness")
67
+ .option("--watch", "Poll until ready or the watch limit is reached")
68
+ .option("--interval <seconds>", "Polling interval", "5")
69
+ .option("--max-wait <seconds>", "Maximum watch time", "300")
70
+ .action(async (options, command) => {
71
+ const ember = operator(await runtime(command));
72
+ let result = await ember.status();
73
+ if (options.watch) {
74
+ const maxWait = numericOption(options.maxWait, "max wait", 5, 1800);
75
+ const interval = numericOption(options.interval, "polling interval", 1, 60);
76
+ const deadline = Date.now() + maxWait * 1000;
77
+ const controller = new AbortController();
78
+ const stop = () => controller.abort();
79
+ process.once("SIGINT", stop);
80
+ try {
81
+ while (!controller.signal.aborted && Date.now() < deadline && result.data.phase !== "ready") {
82
+ if (!globals(command).quiet)
83
+ process.stderr.write("Waiting for Ember...\n");
84
+ await delay(interval * 1000, controller.signal);
85
+ if (!controller.signal.aborted)
86
+ result = await ember.status();
87
+ }
88
+ }
89
+ finally {
90
+ process.removeListener("SIGINT", stop);
91
+ }
92
+ if (controller.signal.aborted)
93
+ process.exitCode = 130;
94
+ else if (result.data.phase !== "ready")
95
+ process.exitCode = 3;
96
+ }
97
+ output(command, result);
98
+ });
99
+ const source = program.command("source").description("Connect and inspect data sources");
100
+ source.command("types").action(run((ember) => ember.sources({ action: "types" })));
101
+ source.command("list").action(run((ember) => ember.sources({ action: "list" })));
102
+ source.command("connect <kind>").description("Connect a source through a browser handoff or credentials read from env/stdin")
103
+ .option("--name <name>").option("--mode <mode>").option("--credential-env <name>", "Read credentials from an environment variable")
104
+ .option("--credential-stdin", "Read credentials from stdin").option("--no-open", "Do not open browser handoffs")
105
+ .option("--max-wait <seconds>", "Maximum OAuth wait time", "300")
106
+ .action(async (kind, options, command) => {
107
+ const current = await runtime(command);
108
+ const ember = operator(current);
109
+ if (options.credentialEnv || options.credentialStdin) {
110
+ if (!["postgres", "supabase"].includes(String(kind).toLowerCase())) {
111
+ throw new Error("Environment and stdin credentials are only supported for Postgres and Supabase. Use the browser handoff for this source.");
112
+ }
113
+ const raw = options.credentialEnv ? process.env[options.credentialEnv] : await readStdin();
114
+ if (!raw)
115
+ throw new Error("The credential reference was empty.");
116
+ const credentials = parseCredentials(raw);
117
+ const connection = await ember.client.createConnector({ workspace_id: ember.workspaceId, name: options.name ?? `${kind} source`, kind, connector_usage: "warehouse_ingestion" });
118
+ await ember.client.upsertConnectorCredentials(connection.id, { workspace_id: ember.workspaceId, credentials, scope: "workspace" });
119
+ const result = await ember.sources({ action: "verify", connection_id: connection.id });
120
+ output(command, result);
121
+ return;
122
+ }
123
+ const result = await ember.sources({ action: "connect", kind, name: options.name, mode: options.mode });
124
+ const url = typeof result.data.authorization_url === "string" ? result.data.authorization_url : null;
125
+ if (url && !globals(command).quiet)
126
+ process.stderr.write(`Open to connect ${kind}:\n${url}\n`);
127
+ if (url && options.open)
128
+ await openUrl(url);
129
+ const connectionId = typeof result.data.connector_id === "string" ? result.data.connector_id : null;
130
+ if (result.data.auth_strategy === "oauth" && connectionId) {
131
+ const watched = await waitForSourceConnection(ember, connectionId, numericOption(options.maxWait, "max wait", 5, 1800), command);
132
+ output(command, { ...result, data: { ...result.data, connection: watched.data } });
133
+ return;
134
+ }
135
+ output(command, result);
136
+ });
137
+ source.command("status <connection-id>").action(run((ember, id) => ember.sources({ action: "status", connection_id: required(id, "connection id") })));
138
+ source.command("verify <connection-id>").action(run((ember, id) => ember.sources({ action: "verify", connection_id: required(id, "connection id") })));
139
+ const ingest = program.command("ingest").description("Plan and control ingestion");
140
+ for (const action of ["plan", "start"])
141
+ ingest.command(`${action} <connection-id>`)
142
+ .requiredOption("--source-kind <kind>").option("--name <name>").option("--database-id <id>")
143
+ .addOption(new Option("--engine <engine>", "Ingestion engine").choices(["worker_batch", "aws_dms_firehose", "embrasure_flow"]).default("worker_batch")).option("--cadence <minutes>")
144
+ .option("--tables <json-or-file>", "Override recommended tables with a JSON array or @path")
145
+ .action(async (connectionId, options, command) => {
146
+ const tables = options.tables ? await jsonArgument(options.tables) : undefined;
147
+ const result = await operator(await runtime(command)).ingestion({ action, connection_id: connectionId, source_kind: options.sourceKind, name: options.name, database_id: options.databaseId, ingestion_engine: options.engine, cadence_minutes: options.cadence ? numericOption(options.cadence, "cadence", 15, 10080) : undefined, tables });
148
+ output(command, result);
149
+ });
150
+ ingest.command("status [ingestion-run-id]").action(run((ember, id) => ember.ingestion({ action: "status", ingestion_run_id: id })));
151
+ for (const action of ["sync", "pause", "resume"])
152
+ ingest.command(`${action} <ingestion-run-id>`).action(run((ember, id) => ember.ingestion({ action, ingestion_run_id: required(id, "ingestion run id") })));
153
+ const query = program.command("query").description("Run read-only SQL against qualified warehouse relations and inspect query handles");
154
+ query.command("run <sql>").option("--database <name>", "Default database", "ember").option("--max-rows <number>", "Retained-result cap (10000); use SQL LIMIT for fewer rows")
155
+ .action(async (sql, options, command) => output(command, await operator(await runtime(command)).query({ action: "run", sql, database: options.database, max_result_rows: options.maxRows ? numericOption(options.maxRows, "max rows", 1, 10000) : undefined })));
156
+ query.command("status <query-id>").action(run((ember, id) => ember.query({ action: "status", query_id: required(id, "query id") })));
157
+ query.command("results <query-id>").option("--limit <number>").option("--next-token <token>")
158
+ .action(async (id, options, command) => output(command, await operator(await runtime(command)).query({ action: "results", query_id: id, limit: options.limit ? numericOption(options.limit, "limit", 1, 100) : undefined, next_token: options.nextToken })));
159
+ query.command("history").option("--limit <number>").action(async (options, command) => output(command, await operator(await runtime(command)).query({ action: "history", limit: options.limit ? numericOption(options.limit, "limit", 1, 100) : undefined })));
160
+ query.command("cancel <query-id>").action(run((ember, id) => ember.query({ action: "cancel", query_id: required(id, "query id") })));
161
+ const warehouse = program.command("warehouse").description("Set up the warehouse and inspect readiness or usage");
162
+ for (const action of ["plan", "apply"]) {
163
+ warehouse.command(action).option("--database <name>").action(async (options, command) => output(command, await operator(await runtime(command)).execute({ tool: "warehouse", input: { action, database: options.database } })));
164
+ }
165
+ warehouse.command("status").action(run((ember) => ember.execute({ tool: "warehouse", input: { action: "status" } })));
166
+ warehouse.command("usage").addOption(new Option("--window <window>").choices(["month_to_date", "7d", "30d", "90d"]).default("month_to_date")).action(async (options, command) => output(command, await operator(await runtime(command)).execute({ tool: "warehouse", input: { action: "usage", window: options.window } })));
167
+ const catalog = program.command("catalog").description("Search business definitions and inspect selected warehouse schemas");
168
+ catalog.command("search <query>").option("--limit <number>", "Maximum matches", "10").option("--refs <refs...>", "Source or relation references to anchor retrieval")
169
+ .action(async (query, options, command) => output(command, await operator(await runtime(command)).execute({ tool: "catalog", input: { action: "search", query, limit: numericOption(options.limit, "limit", 1, 20), refs: options.refs } })));
170
+ catalog.command("describe <object-id>").option("--no-related", "Return only the selected definition")
171
+ .action(async (id, options, command) => output(command, await operator(await runtime(command)).execute({ tool: "catalog", input: { action: "describe", target: "context", object_id: id, include_related: options.related } })));
172
+ catalog.command("tables <database>").option("--limit <number>", "Page size", "20").option("--next-token <token>")
173
+ .action(async (database, options, command) => output(command, await operator(await runtime(command)).execute({ tool: "catalog", input: { action: "list", database, limit: numericOption(options.limit, "limit", 1, 100), next_token: options.nextToken } })));
174
+ catalog.command("table <database> <table>").option("--column-limit <number>", "Column page size", "50").option("--column-offset <number>", "Column offset", "0")
175
+ .action(async (database, table, options, command) => output(command, await operator(await runtime(command)).execute({ tool: "catalog", input: { action: "describe", target: "table", database, table, column_limit: numericOption(options.columnLimit, "column limit", 1, 100), column_offset: numericOption(options.columnOffset, "column offset", 0, 100000) } })));
176
+ program.command("mcp").description("MCP transport commands").command("serve").description("Serve the five Ember tools over stdio").action(async (_options, command) => {
177
+ operator(await runtime(command));
178
+ serveEmberMcpStdio(async (signal) => {
179
+ const current = await runtime(command);
180
+ if (!current.accessToken || !current.workspaceId)
181
+ return operator(current);
182
+ return new EmberOperator({ apiBaseUrl: current.apiBaseUrl, accessToken: current.accessToken, workspaceId: current.workspaceId, clientName: "ember-mcp-stdio", signal });
183
+ });
184
+ });
185
+ return program;
186
+ }
187
+ export async function runCli(argv = process.argv) { await createProgram().parseAsync(argv); }
188
+ function run(action) {
189
+ return async (...args) => { const command = args.at(-1); const first = typeof args[0] === "string" ? args[0] : undefined; output(command, await action(operator(await runtime(command)), first)); };
190
+ }
191
+ function clientSetupInstructions(client) {
192
+ const selected = client === "auto" ? "generic" : client;
193
+ const remoteUrl = "https://embrasure.ai/api/mcp/ember";
194
+ const instructions = { codex: "Run `codex mcp login ember` to approve the workspace.", claude: "Open `/mcp` in Claude Code to approve the workspace.", cursor: `Add {\"ember\":{\"url\":\"${remoteUrl}\"}} to Cursor MCP settings.`, generic: `Connect your MCP client to ${remoteUrl}, or run: ember mcp serve` };
195
+ return { client: selected, configured: false, remote_url: remoteUrl, stdio_command: "ember mcp serve", instruction: instructions[selected] ?? instructions.generic };
196
+ }
197
+ async function configureClient(client) {
198
+ const selected = client === "auto" ? await detectedClient() : client;
199
+ const setup = clientSetupInstructions(selected);
200
+ if (selected === "codex" && await commandExists("codex")) {
201
+ await addMcpServer("codex", ["mcp", "add", "ember", "--url", setup.remote_url]);
202
+ return { ...setup, configured: true };
203
+ }
204
+ if (selected === "claude" && await commandExists("claude")) {
205
+ await addMcpServer("claude", ["mcp", "add", "--transport", "http", "-s", "user", "ember", setup.remote_url]);
206
+ return { ...setup, configured: true };
207
+ }
208
+ return setup;
209
+ }
210
+ async function addMcpServer(command, args) {
211
+ try {
212
+ await runProcess(command, args);
213
+ }
214
+ catch (error) {
215
+ const message = error instanceof Error ? error.message : String(error);
216
+ if (!/already (exists|configured|registered)|duplicate/i.test(message))
217
+ throw error;
218
+ }
219
+ }
220
+ async function detectedClient() { if (await commandExists("codex"))
221
+ return "codex"; if (await commandExists("claude"))
222
+ return "claude"; return "generic"; }
223
+ function commandExists(command) { return runProcess("sh", ["-c", `command -v ${command}`]).then(() => true).catch(() => false); }
224
+ function runProcess(command, args) { return new Promise((resolve, reject) => { const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] }); let stderr = ""; child.stderr.on("data", (chunk) => { stderr += chunk; }); child.once("error", reject); child.once("close", (code) => code === 0 ? resolve() : reject(new Error(stderr.trim() || `${command} failed.`))); }); }
225
+ async function jsonArgument(value) { return JSON.parse(value.startsWith("@") ? await readFile(value.slice(1), "utf8") : value); }
226
+ function parseCredentials(raw) { try {
227
+ const parsed = JSON.parse(raw);
228
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
229
+ throw new Error();
230
+ return parsed;
231
+ }
232
+ catch {
233
+ return { database_url: raw.trim() };
234
+ } }
235
+ async function readStdin() { let value = ""; process.stdin.setEncoding("utf8"); for await (const chunk of process.stdin)
236
+ value += chunk; return value.trim(); }
237
+ function openUrl(url) { const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open"; const args = process.platform === "win32" ? ["/c", "start", "", url] : [url]; return new Promise((resolve) => { const child = spawn(command, args, { detached: true, stdio: "ignore" }); child.once("error", () => resolve()); child.once("spawn", () => { child.unref(); resolve(); }); }); }
238
+ function clamp(value, min, max) { return Number.isFinite(value) ? Math.min(Math.max(value, min), max) : min; }
239
+ function numericOption(value, label, min, max) {
240
+ const parsed = Number(value);
241
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
242
+ throw new Error(`${label} must be an integer from ${min} to ${max}.`);
243
+ }
244
+ return parsed;
245
+ }
246
+ function required(value, label) { if (!value)
247
+ throw new Error(`Missing ${label}.`); return value; }
248
+ function delay(ms, signal) { return new Promise((resolve) => { const timeout = setTimeout(resolve, ms); signal.addEventListener("abort", () => { clearTimeout(timeout); resolve(); }, { once: true }); }); }
249
+ async function waitForSourceConnection(ember, connectionId, maxWaitSeconds, command) {
250
+ const deadline = Date.now() + clamp(maxWaitSeconds, 5, 1800) * 1000;
251
+ const controller = new AbortController();
252
+ const stop = () => controller.abort();
253
+ process.once("SIGINT", stop);
254
+ let result = await ember.sources({ action: "status", connection_id: connectionId });
255
+ try {
256
+ while (!controller.signal.aborted && Date.now() < deadline && result.data.status !== "connected") {
257
+ if (!globals(command).quiet)
258
+ process.stderr.write("Waiting for source authorization...\n");
259
+ await delay(2_000, controller.signal);
260
+ if (!controller.signal.aborted)
261
+ result = await ember.sources({ action: "status", connection_id: connectionId });
262
+ }
263
+ }
264
+ finally {
265
+ process.removeListener("SIGINT", stop);
266
+ }
267
+ if (controller.signal.aborted) {
268
+ process.exitCode = 130;
269
+ return result;
270
+ }
271
+ if (result.data.status !== "connected")
272
+ process.exitCode = 3;
273
+ return result;
274
+ }
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ export declare const queryResponseSchema: z.ZodObject<{
3
+ id: z.ZodString;
4
+ status: z.ZodEnum<{
5
+ running: "running";
6
+ failed: "failed";
7
+ queued: "queued";
8
+ succeeded: "succeeded";
9
+ cancelled: "cancelled";
10
+ }>;
11
+ }, z.core.$loose>;
12
+ export declare const queryHistoryResponseSchema: z.ZodArray<z.ZodObject<{
13
+ id: z.ZodString;
14
+ status: z.ZodEnum<{
15
+ running: "running";
16
+ failed: "failed";
17
+ queued: "queued";
18
+ succeeded: "succeeded";
19
+ cancelled: "cancelled";
20
+ }>;
21
+ }, z.core.$loose>>;
22
+ export declare const queryResultsResponseSchema: z.ZodObject<{
23
+ query: z.ZodObject<{
24
+ id: z.ZodString;
25
+ status: z.ZodLiteral<"succeeded">;
26
+ }, z.core.$loose>;
27
+ columns: z.ZodArray<z.ZodString>;
28
+ column_types: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
29
+ rows: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
30
+ next_token: z.ZodOptional<z.ZodNullable<z.ZodString>>;
31
+ }, z.core.$loose>;
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+ // Validate only the fields Ember needs to identify an operation or read its page.
3
+ // Keep additional API fields so optional metadata can evolve independently.
4
+ export const queryResponseSchema = z.looseObject({
5
+ id: z.string().min(1).max(128).regex(/^\S+$/),
6
+ status: z.enum(["queued", "running", "succeeded", "failed", "cancelled"]),
7
+ });
8
+ export const queryHistoryResponseSchema = z.array(queryResponseSchema);
9
+ export const queryResultsResponseSchema = z.looseObject({
10
+ query: queryResponseSchema.extend({ status: z.literal("succeeded") }),
11
+ columns: z.array(z.string()),
12
+ column_types: z.record(z.string(), z.string()).optional(),
13
+ rows: z.array(z.record(z.string(), z.unknown())),
14
+ next_token: z.string().min(1).max(4096).nullable().optional(),
15
+ });