@meshxdata/fops 0.1.34 → 0.1.36

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meshxdata/fops",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "CLI to install and manage data mesh platforms",
5
5
  "keywords": [
6
6
  "fops",
package/src/agent/llm.js CHANGED
@@ -559,11 +559,11 @@ function resolveProvider(opts, { anthropicKey, openaiKey, azureConfig, useClaude
559
559
  */
560
560
  function defaultModelForProvider(provider, opts, { anthropicKey, azureConfig }) {
561
561
  if (opts.model) return opts.model;
562
- if (provider === "anthropic" || provider === "claude_code") return process.env.ANTHROPIC_MODEL?.trim() || "claude-opus-4-20250514";
562
+ if (provider === "anthropic" || provider === "claude_code") return process.env.ANTHROPIC_MODEL?.trim() || "claude-sonnet-4-6";
563
563
  if (provider === "azure") return azureConfig?.deployment || "gpt-4o";
564
564
  if (provider === "openai") return process.env.OPENAI_MODEL?.trim() || "gpt-5";
565
565
  // Fallback
566
- return anthropicKey ? (process.env.ANTHROPIC_MODEL?.trim() || "claude-opus-4-20250514") : azureConfig ? azureConfig.deployment : "gpt-5";
566
+ return anthropicKey ? (process.env.ANTHROPIC_MODEL?.trim() || "claude-sonnet-4-6") : azureConfig ? azureConfig.deployment : "gpt-5";
567
567
  }
568
568
 
569
569
  export async function streamAssistantReply(root, messages, systemContent, opts) {
@@ -794,7 +794,7 @@ export async function callClaudeWithTools(messages, systemContent, tools, opts =
794
794
  const cached = getAnthropicModule();
795
795
  const { default: Anthropic } = cached || await import("@anthropic-ai/sdk");
796
796
  const client = new Anthropic({ apiKey: anthropicKey });
797
- const model = opts.model || process.env.ANTHROPIC_MODEL?.trim() || "claude-opus-4-20250514";
797
+ const model = opts.model || process.env.ANTHROPIC_MODEL?.trim() || "claude-sonnet-4-6";
798
798
  const claudeTools = toClaudeTools(tools);
799
799
 
800
800
  // Redact PII from context before sending
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "fops-plugin-dai-ttyd",
3
+ "name": "DAI Talk-to-your-Data",
4
+ "version": "0.1.0",
5
+ "description": "Natural language queries against the DAI TTYD (Talk-to-your-Data) service"
6
+ }
@@ -0,0 +1,182 @@
1
+ import chalk from "chalk";
2
+ import { DaiTTYDClient } from "./lib/client.js";
3
+
4
+ const DIM = chalk.dim;
5
+ const OK = chalk.green;
6
+ const ERR = chalk.red;
7
+ const ACCENT = chalk.cyan;
8
+ const BOLD = chalk.bold;
9
+ const WARN = chalk.yellow;
10
+
11
+ export function register(api) {
12
+ const client = new DaiTTYDClient(api.config);
13
+
14
+ function clientFor({ url, jwt } = {}) {
15
+ if (!url && !jwt) return client;
16
+ return new DaiTTYDClient({
17
+ ...api.config,
18
+ ...(url && { baseUrl: url }),
19
+ ...(jwt && { authToken: jwt }),
20
+ });
21
+ }
22
+
23
+ // ─── Doctor check ────────────────────────────────────────────────────────
24
+ api.registerDoctorCheck({
25
+ name: "dai-ttyd: auth token",
26
+ fn: async () => {
27
+ const token =
28
+ api.config?.authToken?.trim() || process.env.DAI_AUTH_TOKEN?.trim();
29
+ if (!token) {
30
+ return {
31
+ ok: false,
32
+ message: "DAI_AUTH_TOKEN not set — configure via env or ~/.fops.json plugins.entries.fops-plugin-dai-ttyd.config.authToken",
33
+ };
34
+ }
35
+ return { ok: true, message: "DAI_AUTH_TOKEN is set" };
36
+ },
37
+ });
38
+
39
+ // ─── Agent tools ─────────────────────────────────────────────────────────
40
+
41
+ api.registerTool({
42
+ name: "ttyd_thread_create",
43
+ description: "Create a new TTYD conversation thread. Returns a thread_id to use with ttyd_ask.",
44
+ inputSchema: {
45
+ type: "object",
46
+ properties: {
47
+ title: { type: "string", description: "Optional title for the thread" },
48
+ },
49
+ },
50
+ async execute({ title } = {}) {
51
+ const threadId = await client.createThread(title || "fops session");
52
+ return JSON.stringify({ thread_id: threadId });
53
+ },
54
+ });
55
+
56
+ api.registerTool({
57
+ name: "ttyd_ask",
58
+ description:
59
+ "Ask a natural-language question to the DAI Talk-to-your-Data service. " +
60
+ "Returns the generated SQL and a text summary/answer. " +
61
+ "Optionally scoped to a dashboard or tile for context. " +
62
+ "If thread_id is omitted, a new thread is created automatically.",
63
+ inputSchema: {
64
+ type: "object",
65
+ properties: {
66
+ question: { type: "string", description: "The natural-language question to ask" },
67
+ thread_id: { type: "number", description: "Existing thread id for multi-turn conversations" },
68
+ dashboard_id: { type: "number", description: "Optional dashboard id for context" },
69
+ tile_id: { type: "number", description: "Optional tile id for context" },
70
+ },
71
+ required: ["question"],
72
+ },
73
+ async execute({ question, thread_id, dashboard_id, tile_id }) {
74
+ const result = await client.ask(question, {
75
+ threadId: thread_id,
76
+ dashboardId: dashboard_id,
77
+ tileId: tile_id,
78
+ });
79
+ return JSON.stringify(result);
80
+ },
81
+ });
82
+
83
+ // ─── CLI commands ─────────────────────────────────────────────────────────
84
+
85
+ api.registerCommand((program) => {
86
+ const ttyd = program
87
+ .command("ttyd")
88
+ .description("Talk-to-your-Data — query data with natural language")
89
+ .option("--url <url>", "Override the TTYD base URL (e.g. https://api-live.dashboards.ai)")
90
+ .option("--jwt <token>", "Bearer token (overrides DAI_AUTH_TOKEN env)");
91
+
92
+ // fops ttyd ask "<question>"
93
+ ttyd
94
+ .command("ask <question>")
95
+ .description("Ask a natural-language question and stream the answer")
96
+ .option("--thread <id>", "Reuse an existing thread id (for follow-up questions)", (v) => Number(v))
97
+ .option("--dashboard <id>", "Scope to a dashboard id", (v) => Number(v))
98
+ .option("--tile <id>", "Scope to a tile id", (v) => Number(v))
99
+ .option("--json", "Output raw JSON instead of formatted text")
100
+ .option("--debug", "Print raw SSE events")
101
+ .action(async (question, opts) => {
102
+ const { url, jwt } = ttyd.opts();
103
+ const c = clientFor({ url, jwt });
104
+
105
+ let threadId = opts.thread ?? null;
106
+ if (threadId == null) {
107
+ process.stdout.write(DIM(" Creating thread... "));
108
+ try {
109
+ threadId = await c.createThread();
110
+ process.stdout.write(DIM(`thread ${threadId}\n`));
111
+ } catch (e) {
112
+ process.stdout.write(WARN(`skipped (${e.message})\n`));
113
+ // Proceed without a thread_id — server may create one implicitly
114
+ }
115
+ }
116
+
117
+ console.log(ACCENT(`\n ── Asking ${"─".repeat(44)}`));
118
+ console.log(DIM(` ${question}\n`));
119
+
120
+ let sql = "";
121
+ const summaryParts = [];
122
+
123
+ try {
124
+ for await (const event of c.askStream(question, threadId, {
125
+ dashboardId: opts.dashboard,
126
+ tileId: opts.tile,
127
+ })) {
128
+ if (opts.debug) console.error(DIM(`[sse] ${JSON.stringify(event)}`));
129
+ if (event.type === "sql") {
130
+ sql = event.content ?? "";
131
+ if (!opts.json) {
132
+ console.log(BOLD(" SQL:"));
133
+ for (const line of sql.split("\n")) {
134
+ console.log(DIM(` ${line}`));
135
+ }
136
+ console.log();
137
+ }
138
+ } else if (event.type === "answer") {
139
+ const chunk = event.content ?? "";
140
+ summaryParts.push(chunk);
141
+ if (!opts.json) process.stdout.write(chunk);
142
+ } else if (event.type === "error") {
143
+ throw new Error(event.message ?? "TTYD error");
144
+ } else if (event.type === "done") {
145
+ break;
146
+ }
147
+ }
148
+ } catch (e) {
149
+ console.error(ERR(`\n ✗ ${e.message}`));
150
+ return;
151
+ }
152
+
153
+ if (!opts.json) {
154
+ if (summaryParts.length > 0) process.stdout.write("\n");
155
+ if (threadId != null) {
156
+ console.log(DIM(`\n thread: ${threadId}`));
157
+ console.log(DIM(" Use --thread to continue this conversation.\n"));
158
+ } else {
159
+ console.log();
160
+ }
161
+ } else {
162
+ console.log(JSON.stringify({ thread_id: threadId, sql, summary: summaryParts.join("") }, null, 2));
163
+ }
164
+ });
165
+
166
+ // fops ttyd thread new [title]
167
+ const thread = ttyd
168
+ .command("thread")
169
+ .description("Manage TTYD threads");
170
+
171
+ thread
172
+ .command("new [title]")
173
+ .description("Create a new conversation thread")
174
+ .action(async (title = "fops session") => {
175
+ const { url, jwt } = ttyd.opts();
176
+ const c = clientFor({ url, jwt });
177
+ const threadId = await c.createThread(title);
178
+ console.log(OK(` ✓ Thread created: ${threadId}`));
179
+ console.log(DIM(` Use: fops ttyd ask --thread ${threadId} "<question>"\n`));
180
+ });
181
+ });
182
+ }
@@ -0,0 +1,164 @@
1
+ import http from "node:http";
2
+ import https from "node:https";
3
+
4
+ const DEFAULT_BASE_URL = "https://api.dashboards.ai";
5
+
6
+ function resolveBaseUrl(config) {
7
+ return (
8
+ config?.baseUrl?.trim() ||
9
+ process.env.DAI_TTYD_URL?.trim() ||
10
+ DEFAULT_BASE_URL
11
+ ).replace(/\/+$/, "");
12
+ }
13
+
14
+ function resolveAuthToken(config) {
15
+ const raw = config?.authToken || process.env.DAI_AUTH_TOKEN || "";
16
+ return raw.replace(/\s+/g, ""); // strip whitespace/newlines from env or shell wrapping
17
+ }
18
+
19
+ export class DaiTTYDClient {
20
+ constructor(config = {}) {
21
+ this.baseUrl = resolveBaseUrl(config);
22
+ this.authToken = resolveAuthToken(config);
23
+ }
24
+
25
+ _headers() {
26
+ return {
27
+ "Content-Type": "application/json",
28
+ Accept: "application/json",
29
+ Authorization: `Bearer ${this.authToken}`,
30
+ };
31
+ }
32
+
33
+ /** POST /ttyd/threads → thread id (number) */
34
+ async createThread(title = "fops session") {
35
+ const url = `${this.baseUrl}/ttyd/threads`;
36
+ let { status, data } = await request("POST", url, this._headers(), { title });
37
+ // Backend may return 400 "User with email already exists" on the first call
38
+ // when auto-provisioning the user — retry once, it should succeed
39
+ if (status === 400 && data.includes("already exists")) {
40
+ ({ status, data } = await request("POST", url, this._headers(), { title }));
41
+ }
42
+ if (status >= 400) throw new Error(`Failed to create thread (HTTP ${status}): ${data}`);
43
+ const body = JSON.parse(data);
44
+ const id = body.id ?? body.thread_id;
45
+ if (id == null) throw new Error(`No thread id in response: ${data}`);
46
+ return id;
47
+ }
48
+
49
+ /**
50
+ * POST /ttyd/answer/stream — async generator yielding SSE event objects.
51
+ * Each event: { type: "sql"|"answer"|"done"|"error", content?: string, message?: string }
52
+ */
53
+ async *askStream(question, threadId, { dashboardId, tileId } = {}) {
54
+ const url = `${this.baseUrl}/ttyd/answer/stream`;
55
+ const payload = { question, thread_id: threadId };
56
+ if (dashboardId != null) payload.dashboard_id = Number(dashboardId);
57
+ if (tileId != null) payload.tile_id = Number(tileId);
58
+ yield* streamSSE("POST", url, this._headers(), payload);
59
+ }
60
+
61
+ /** Convenience: create a thread, ask one question, collect full sql + summary. */
62
+ async ask(question, { threadId, dashboardId, tileId } = {}) {
63
+ const id = threadId ?? (await this.createThread());
64
+ let sql = "";
65
+ const summaryParts = [];
66
+ for await (const event of this.askStream(question, id, { dashboardId, tileId })) {
67
+ if (event.type === "sql") sql = event.content ?? "";
68
+ else if (event.type === "answer") summaryParts.push(event.content ?? "");
69
+ else if (event.type === "error") throw new Error(event.message ?? "TTYD error");
70
+ else if (event.type === "done") break;
71
+ }
72
+ return { threadId: id, sql, summary: summaryParts.join("") };
73
+ }
74
+
75
+ /** Verify connectivity — returns true/false. */
76
+ async ping() {
77
+ try {
78
+ const url = `${this.baseUrl}/ttyd/threads`;
79
+ const { status } = await request("POST", url, this._headers(), { title: "ping" });
80
+ return status < 500;
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+ }
86
+
87
+ // ─── Helpers ────────────────────────────────────────────────────────────────
88
+
89
+ function request(method, url, headers, body, timeoutMs = 15_000) {
90
+ return new Promise((resolve, reject) => {
91
+ const parsed = new URL(url);
92
+ const lib = parsed.protocol === "https:" ? https : http;
93
+ const encoded = JSON.stringify(body);
94
+ const options = {
95
+ hostname: parsed.hostname,
96
+ port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
97
+ path: parsed.pathname + parsed.search,
98
+ method,
99
+ headers: { ...headers, "Content-Length": Buffer.byteLength(encoded) },
100
+ timeout: timeoutMs,
101
+ };
102
+ const req = lib.request(options, (res) => {
103
+ let data = "";
104
+ res.on("data", (c) => { data += c; });
105
+ res.on("end", () => resolve({ status: res.statusCode, data }));
106
+ });
107
+ req.on("error", reject);
108
+ req.on("timeout", () => { req.destroy(); reject(new Error("Request timeout")); });
109
+ req.write(encoded);
110
+ req.end();
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Async generator that streams SSE from an HTTP endpoint.
116
+ * Yields parsed JSON objects from each "data: {...}" line.
117
+ */
118
+ async function* streamSSE(method, url, headers, body, timeoutMs = 300_000) {
119
+ const parsed = new URL(url);
120
+ const lib = parsed.protocol === "https:" ? https : http;
121
+ const encoded = JSON.stringify(body);
122
+ const options = {
123
+ hostname: parsed.hostname,
124
+ port: parsed.port || (parsed.protocol === "https:" ? 443 : 80),
125
+ path: parsed.pathname + parsed.search,
126
+ method,
127
+ headers: {
128
+ ...headers,
129
+ "Content-Length": Buffer.byteLength(encoded),
130
+ Accept: "text/event-stream",
131
+ },
132
+ timeout: timeoutMs,
133
+ };
134
+
135
+ const res = await new Promise((resolve, reject) => {
136
+ const req = lib.request(options, resolve);
137
+ req.on("error", reject);
138
+ req.on("timeout", () => { req.destroy(); reject(new Error("Stream timeout")); });
139
+ req.write(encoded);
140
+ req.end();
141
+ });
142
+
143
+ if (res.statusCode >= 400) {
144
+ let errData = "";
145
+ for await (const chunk of res) errData += chunk;
146
+ throw new Error(`HTTP ${res.statusCode}: ${errData}`);
147
+ }
148
+
149
+ let buffer = "";
150
+ for await (const chunk of res) {
151
+ buffer += chunk.toString();
152
+ const lines = buffer.split("\n");
153
+ buffer = lines.pop(); // keep any incomplete line
154
+ for (const line of lines) {
155
+ if (!line.startsWith("data:")) continue;
156
+ const raw = line.slice("data:".length).trim();
157
+ if (!raw) continue;
158
+ let data;
159
+ try { data = JSON.parse(raw); } catch { continue; }
160
+ yield data;
161
+ if (data?.type === "done" || data?.type === "error") return;
162
+ }
163
+ }
164
+ }
@@ -0,0 +1 @@
1
+ { "type": "module" }
@@ -1527,8 +1527,19 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
1527
1527
  }
1528
1528
 
1529
1529
  @objc func runUpdate() {
1530
+ updateItem.title = "⬆ Updating…"
1531
+ updateItem.isEnabled = false
1530
1532
  let npm = "/opt/homebrew/bin/npm"
1531
- openTerminal(command: npm + " install -g @meshxdata/fops")
1533
+ let p = Process()
1534
+ p.executableURL = URL(fileURLWithPath: npm)
1535
+ p.arguments = ["install", "-g", "@meshxdata/fops"]
1536
+ p.terminationHandler = { _ in
1537
+ DispatchQueue.main.async {
1538
+ updateItem.title = "✓ Updated — restart tray to apply"
1539
+ updateItem.isEnabled = false
1540
+ }
1541
+ }
1542
+ try? p.run()
1532
1543
  }
1533
1544
 
1534
1545
  // Rebuild Compose submenu each time it opens
package/src/ui/tui/App.js CHANGED
@@ -2790,7 +2790,7 @@ function TuiApp({ core, version, root }) {
2790
2790
  h(Box, { flexShrink: 0 },
2791
2791
  h(StatusBar, {
2792
2792
  agentName: activeSession?.agent,
2793
- model: core.opts.model || (providerLabel === "azure-openai" ? "azure-openai" : providerLabel === "openai" ? "openai" : "claude-sonnet"),
2793
+ model: core.opts.model || (providerLabel === "azure-openai" ? "azure-openai" : providerLabel === "openai" ? "openai" : process.env.ANTHROPIC_MODEL?.trim() || "claude-sonnet-4-6"),
2794
2794
  tokenCount: tokenCount || (activeSession ? (core.sessions.get(activeId)?.tokenCount || 0) : 0),
2795
2795
  cacheReadTokens,
2796
2796
  version,