@thingd/cli 0.49.9 → 0.50.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.
@@ -7,8 +7,9 @@ import readline from "node:readline";
7
7
  import { ThingD } from "@thingd/sdk";
8
8
  import pc from "picocolors";
9
9
  import { listInstances, listProjects } from "./lib/cloud-api.js";
10
- import { readCloudConfig } from "./lib/cloud-config.js";
10
+ import { readCloudConfig, removeCloudConfig, resolveCloudUrl, writeCloudConfig, } from "./lib/cloud-config.js";
11
11
  import { logoText } from "./logo.js";
12
+ import { defaultThingdDbPath } from "./paths.js";
12
13
  // ── Helpers ──────────────────────────────────────────────────────────
13
14
  function highlightJson(val) {
14
15
  const str = JSON.stringify(val, null, 2);
@@ -77,6 +78,8 @@ let totalObjects = 0;
77
78
  let totalEventsCount = 0;
78
79
  let totalActiveJobsCount = 0;
79
80
  let totalDeadJobsCount = 0;
81
+ let totalLinksCount = 0;
82
+ let cloudError = null;
80
83
  let objectsHistory = [];
81
84
  let eventsHistory = [];
82
85
  let activeJobsHistory = [];
@@ -91,7 +94,7 @@ let loadTimer = null;
91
94
  let pollTimer = null;
92
95
  let keypressHandler = null;
93
96
  let formState = null;
94
- function openForm(title, fields, onSubmit) {
97
+ function openForm(title, fields, onSubmit, keepViewer) {
95
98
  formState = {
96
99
  active: true,
97
100
  title,
@@ -126,6 +129,10 @@ function openForm(title, fields, onSubmit) {
126
129
  try {
127
130
  await onSubmit(vals);
128
131
  formState = null;
132
+ if (keepViewer) {
133
+ draw();
134
+ return;
135
+ }
129
136
  viewerLines = [];
130
137
  loadedItemId = ""; // Force reload
131
138
  await fetchResources();
@@ -190,12 +197,13 @@ function formatUptime(ms) {
190
197
  return `${h}h ${m % 60}m`;
191
198
  }
192
199
  async function fetchResourcesFallback() {
200
+ cloudError = null;
193
201
  try {
194
202
  const nativeCollections = await db.listCollections();
195
203
  const nativeStreams = await db.listStreams();
196
204
  // Maintain default collections/streams for UI if none exist
197
- const defaultCols = new Set(["decisions", "load-test"]);
198
- const defaultStrs = new Set(["project:thingd", "load-events", "activity-log"]);
205
+ const defaultCols = new Set();
206
+ const defaultStrs = new Set();
199
207
  for (const c of nativeCollections) {
200
208
  defaultCols.add(c);
201
209
  }
@@ -210,10 +218,16 @@ async function fetchResourcesFallback() {
210
218
  totalActiveJobsCount = await db.countActiveJobs();
211
219
  totalDeadJobsCount = await db.countDeadJobs();
212
220
  }
213
- catch {
221
+ catch (err) {
222
+ const msg = err instanceof Error ? err.message : String(err);
223
+ cloudError = `Failed to load resources: ${msg}`;
214
224
  collections = [];
215
225
  streams = [];
216
226
  totalObjects = 0;
227
+ // Show error in viewer if it's currently showing the default message
228
+ if (viewerLines.length === 1 && viewerLines[0] === "Select an item to view details.") {
229
+ viewerLines = [pc.yellow(cloudError), pc.dim("Press 'r' to retry or 's' to switch driver.")];
230
+ }
217
231
  }
218
232
  // Queues
219
233
  try {
@@ -221,18 +235,37 @@ async function fetchResourcesFallback() {
221
235
  queues = (listed ?? []).sort();
222
236
  }
223
237
  catch {
224
- queues = ["embed", "load-queue", "worker-queue"];
238
+ queues = [];
239
+ }
240
+ // Link count
241
+ try {
242
+ totalLinksCount = await db.countLinks();
243
+ }
244
+ catch {
245
+ totalLinksCount = 0;
246
+ }
247
+ // Populate objectsByCollection from live data
248
+ objectsByCollection.clear();
249
+ for (const col of collections) {
250
+ try {
251
+ const list = await db.listObjects(col);
252
+ objectsByCollection.set(col, list.map((o) => o.id));
253
+ }
254
+ catch {
255
+ objectsByCollection.set(col, []);
256
+ }
225
257
  }
226
258
  }
227
259
  async function fetchResources() {
228
260
  if (driver === "native" && dbPath) {
229
261
  try {
230
262
  // Override the tracked totals with the actual exact DB count!
231
- const [objCount, evtCount, activeCount, deadCount, nativeCollections, nativeStreams, nativeQueues,] = await Promise.all([
263
+ const [objCount, evtCount, activeCount, deadCount, linkCount, nativeCollections, nativeStreams, nativeQueues,] = await Promise.all([
232
264
  db.countObjects(),
233
265
  db.countEvents(),
234
266
  db.countActiveJobs(),
235
267
  db.countDeadJobs(),
268
+ db.countLinks(),
236
269
  db.listCollections(),
237
270
  db.listStreams(),
238
271
  db.listQueues?.() ?? Promise.resolve([]),
@@ -243,12 +276,10 @@ async function fetchResources() {
243
276
  Number.isNaN(activeCount) || activeCount === 0 ? totalActiveJobsCount : activeCount;
244
277
  totalDeadJobsCount =
245
278
  Number.isNaN(deadCount) || deadCount === 0 ? totalDeadJobsCount : deadCount;
246
- collections = nativeCollections.length > 0 ? nativeCollections : ["decisions", "load-test"];
247
- streams =
248
- nativeStreams.length > 0
249
- ? nativeStreams
250
- : ["project:thingd", "load-events", "activity-log"];
251
- queues = nativeQueues.length > 0 ? nativeQueues : ["embed", "load-queue", "worker-queue"];
279
+ totalLinksCount = Number.isNaN(linkCount) ? totalLinksCount : linkCount;
280
+ collections = nativeCollections.length > 0 ? nativeCollections : [];
281
+ streams = nativeStreams.length > 0 ? nativeStreams : [];
282
+ queues = nativeQueues.length > 0 ? nativeQueues : [];
252
283
  }
253
284
  catch {
254
285
  // Fallback if sqlite3 fails
@@ -258,6 +289,17 @@ async function fetchResources() {
258
289
  else {
259
290
  await fetchResourcesFallback();
260
291
  }
292
+ // Populate objectsByCollection from live data
293
+ objectsByCollection.clear();
294
+ for (const col of collections) {
295
+ try {
296
+ const list = await db.listObjects(col);
297
+ objectsByCollection.set(col, list.map((o) => o.id));
298
+ }
299
+ catch {
300
+ objectsByCollection.set(col, []);
301
+ }
302
+ }
261
303
  // Calculate Deltas for Operations Throughput Rates
262
304
  const prevObjects = objectsHistory.length > 0
263
305
  ? (objectsHistory[objectsHistory.length - 1] ?? totalObjects)
@@ -532,7 +574,7 @@ function buildTree() {
532
574
  depth: 0,
533
575
  expandable: true,
534
576
  children: [
535
- { id: "maintenance:integrity", label: "Run Integrity Check" },
577
+ { id: "maintenance:integrity", label: "Run Health Check" },
536
578
  { id: "maintenance:checkpoint", label: "WAL Checkpoint" },
537
579
  { id: "maintenance:backup", label: "Create Backup" },
538
580
  ],
@@ -675,12 +717,18 @@ async function loadContent(node) {
675
717
  content += ` ${pc.bold("Capacity & Storage")}\n`;
676
718
  content += ` ${pc.dim("Objects".padEnd(14))} ${pc.cyan(String(totalObjects).padEnd(6))} ${pc.dim("total")}\n`;
677
719
  content += ` ${pc.dim("Events".padEnd(14))} ${pc.green(String(totalEventsCount).padEnd(6))} ${pc.dim("total")}\n`;
720
+ content += ` ${pc.dim("Links".padEnd(14))} ${pc.blue(String(totalLinksCount).padEnd(6))} ${pc.dim("total")}\n`;
678
721
  content += ` ${pc.dim("Active Jobs".padEnd(14))} ${pc.yellow(String(totalActiveJobsCount).padEnd(6))} ${pc.dim("in flight")}\n`;
679
722
  content += ` ${pc.dim("Dead Jobs".padEnd(14))} ${pc.red(String(totalDeadJobsCount).padEnd(6))} ${pc.dim("failed")}\n\n`;
680
723
  content += ` ${pc.bold("Connection")}\n`;
681
724
  content += ` ${pc.dim("Driver".padEnd(14))} ${driverName}\n`;
682
725
  content += ` ${pc.dim("Path".padEnd(14))} ${dbPath || ":memory:"}\n`;
683
- content += ` ${pc.dim("Size".padEnd(14))} ${dbSizeStr}\n\n`;
726
+ content += ` ${pc.dim("Size".padEnd(14))} ${dbSizeStr}\n`;
727
+ if (driver === "native") {
728
+ const sizeSpark = drawSparkline(dbSizeHistory, 5, Math.max(10, viewW - 55));
729
+ content += ` ${pc.dim("Size History".padEnd(14))} ${pc.cyan(sizeSpark)}\n`;
730
+ }
731
+ content += `\n`;
684
732
  // ── Throughput & Activity Metrics
685
733
  const currentWrite = objectWriteRateHistory[objectWriteRateHistory.length - 1] ?? 0;
686
734
  const currentAppend = eventAppendRateHistory[eventAppendRateHistory.length - 1] ?? 0;
@@ -832,7 +880,7 @@ function draw() {
832
880
  help = ` ${pc.dim("↑↓")} nav ${pc.dim("enter")} connect ${pc.dim("q")} quit `;
833
881
  }
834
882
  else {
835
- help = ` ${pc.dim("↑↓")} nav ${pc.dim("←→")} toggle ${pc.dim("c")} create ${pc.dim("e")} edit ${pc.dim("d")} delete ${pc.dim("/")} search ${pc.dim("i")} info ${pc.dim("r")} refresh ${pc.dim("s")} switch ${pc.dim("q")} quit `;
883
+ help = ` ${pc.dim("↑↓")} nav ${pc.dim("←→")} toggle ${pc.dim("c")} create ${pc.dim("e")} edit ${pc.dim("d")} delete ${pc.dim("/")} search ${pc.dim("i")} info ${pc.dim("r")} refresh ${pc.dim("s")} switch ${pc.dim("l")} logout ${pc.dim("q")} quit `;
836
884
  }
837
885
  buf += `${pc.dim("─".repeat(W))}\n`;
838
886
  buf += padToWidth(help, W);
@@ -1127,7 +1175,10 @@ async function handleDelete(selected) {
1127
1175
  if ((vals.confirm || "").toLowerCase() !== "yes") {
1128
1176
  throw new Error("Canceled");
1129
1177
  }
1130
- await db.delete(ref.collection, ref.id);
1178
+ const result = await db.delete(ref.collection, ref.id);
1179
+ if (result && !result.deleted) {
1180
+ throw new Error(`Object '${ref.id}' not found in collection '${ref.collection}'`);
1181
+ }
1131
1182
  });
1132
1183
  }
1133
1184
  else if (selected.type === "queue" && selected.ref) {
@@ -1188,7 +1239,7 @@ async function handleSearch() {
1188
1239
  }),
1189
1240
  ];
1190
1241
  loadedItemId = "search_results";
1191
- });
1242
+ }, true);
1192
1243
  }
1193
1244
  async function handleInfo() {
1194
1245
  const lines = [
@@ -1241,7 +1292,7 @@ async function handleInfo() {
1241
1292
  }
1242
1293
  async function handleMaintenance() {
1243
1294
  // Cycle through maintenance operations with each press of 'm'
1244
- const operations = ["integrity", "checkpoint", "backup"];
1295
+ const operations = ["health", "checkpoint", "backup"];
1245
1296
  const idx = maintenanceCursor % operations.length;
1246
1297
  maintenanceCursor = (maintenanceCursor + 1) % operations.length;
1247
1298
  const op = operations[idx];
@@ -1285,18 +1336,25 @@ async function handleMaintenance() {
1285
1336
  }
1286
1337
  }
1287
1338
  else {
1339
+ // Health check — verify read path
1288
1340
  try {
1289
1341
  if (typeof db !== "undefined" && typeof db.countObjects === "function") {
1290
- await db.countObjects();
1291
- viewerLines = [` Integrity check passed`, ``];
1342
+ const objCount = await db.countObjects();
1343
+ const evtCount = await db.countEvents();
1344
+ const jobCount = typeof db.countActiveJobs === "function" ? await db.countActiveJobs() : 0;
1345
+ viewerLines = [
1346
+ ` Health check passed`,
1347
+ ` Objects: ${objCount}, Events: ${evtCount}, Active jobs: ${jobCount}`,
1348
+ ``,
1349
+ ];
1292
1350
  }
1293
1351
  else {
1294
- viewerLines = [` Integrity check not available`, ``];
1352
+ viewerLines = [` Health check not available`, ``];
1295
1353
  }
1296
1354
  }
1297
1355
  catch (err) {
1298
1356
  viewerLines = [
1299
- ` Integrity check failed: ${err instanceof Error ? err.message : String(err)}`,
1357
+ ` Health check failed: ${err instanceof Error ? err.message : String(err)}`,
1300
1358
  ``,
1301
1359
  ];
1302
1360
  }
@@ -1530,6 +1588,9 @@ function setupKeypress() {
1530
1588
  else if (str === "m" || str === "M") {
1531
1589
  await handleMaintenance();
1532
1590
  }
1591
+ else if (str === "l" || str === "L") {
1592
+ await handleLogout();
1593
+ }
1533
1594
  }
1534
1595
  };
1535
1596
  process.stdin.on("keypress", keypressHandler);
@@ -1563,27 +1624,61 @@ async function handleConnect(node) {
1563
1624
  return;
1564
1625
  }
1565
1626
  const selectedDriver = node.ref.driver;
1566
- // Cloud with saved credentials — fetch projects/instances and connect
1627
+ // Cloud with saved credentials — fetch projects/instances and let user pick
1567
1628
  if (selectedDriver === "cloud") {
1568
1629
  const cloudCfg = readCloudConfig();
1569
- if (cloudCfg?.token && cloudCfg?.url) {
1630
+ if (cloudCfg?.token) {
1570
1631
  try {
1571
1632
  const { projects } = await listProjects(cloudCfg);
1633
+ const instanceOptions = [];
1572
1634
  for (const project of projects) {
1573
1635
  try {
1574
1636
  const { instances } = await listInstances(cloudCfg, project.id);
1575
- if (instances.length > 0) {
1576
- const instance = instances[0];
1577
- if (instance?.mcpUrl) {
1578
- await connectToDriver("cloud", instance.mcpUrl, instance.mcpUrl, cloudCfg.token);
1579
- return;
1637
+ for (const inst of instances) {
1638
+ if (inst.mcpUrl) {
1639
+ instanceOptions.push({
1640
+ label: `${project.slug}/${inst.slug}`,
1641
+ mcpUrl: inst.mcpUrl,
1642
+ projectSlug: project.slug,
1643
+ instanceSlug: inst.slug,
1644
+ });
1580
1645
  }
1581
1646
  }
1582
1647
  }
1583
1648
  catch {
1584
- // Try next project
1649
+ // Skip projects that fail to list instances
1585
1650
  }
1586
1651
  }
1652
+ if (instanceOptions.length > 0) {
1653
+ // Pre-select the saved instance if it exists
1654
+ const savedIdx = cloudCfg.instanceUrl
1655
+ ? instanceOptions.findIndex((o) => o.mcpUrl === cloudCfg.instanceUrl)
1656
+ : -1;
1657
+ const defaultVal = savedIdx >= 0 ? instanceOptions[savedIdx]?.label : instanceOptions[0]?.label;
1658
+ openForm("Connect to Cloud", [
1659
+ {
1660
+ id: "instance",
1661
+ label: "Instance",
1662
+ value: defaultVal ?? "",
1663
+ options: instanceOptions.map((o) => o.label),
1664
+ },
1665
+ ], async (vals) => {
1666
+ const selected = instanceOptions.find((o) => o.label === vals.instance);
1667
+ if (!selected) {
1668
+ viewerLines = [pc.red("No instance selected.")];
1669
+ draw();
1670
+ return;
1671
+ }
1672
+ // Save selection to cloud config
1673
+ cloudCfg.instanceUrl = selected.mcpUrl;
1674
+ cloudCfg.projectSlug = selected.projectSlug;
1675
+ cloudCfg.instanceSlug = selected.instanceSlug;
1676
+ writeCloudConfig(cloudCfg);
1677
+ await connectToDriver("cloud", selected.mcpUrl, selected.mcpUrl, cloudCfg.token);
1678
+ });
1679
+ return;
1680
+ }
1681
+ // No instances found — fall through to manual form
1587
1682
  viewerLines = [
1588
1683
  pc.yellow("No cloud instances found."),
1589
1684
  pc.dim("Create one at https://thingd.cloud, or enter the Cloud URL manually."),
@@ -1641,18 +1736,39 @@ async function handleConnect(node) {
1641
1736
  {
1642
1737
  id: "path",
1643
1738
  label: "Database Path",
1644
- value: path.join(os.homedir(), "Downloads", "data.db"),
1739
+ value: defaultThingdDbPath(),
1645
1740
  },
1646
1741
  ]),
1647
1742
  ], async (vals) => {
1648
1743
  let cloudUrl;
1649
1744
  if (selectedDriver === "cloud") {
1650
1745
  if (isCloudWithConfig) {
1746
+ if (!vals.project || !vals.instance) {
1747
+ viewerLines = [pc.red("Project and instance slugs are required.")];
1748
+ draw();
1749
+ return;
1750
+ }
1651
1751
  // Construct URL from project + instance slugs
1652
- cloudUrl = `${baseUrl}/mcp/${encodeURIComponent(vals.project || "")}/${encodeURIComponent(vals.instance || "")}`;
1752
+ cloudUrl = `${baseUrl}/mcp/${encodeURIComponent(vals.project)}/${encodeURIComponent(vals.instance)}`;
1753
+ // Save selection to cloud config
1754
+ const cfg = cloudCfg ?? { token: vals.token, url: baseUrl };
1755
+ cfg.instanceUrl = cloudUrl;
1756
+ cfg.projectSlug = vals.project;
1757
+ cfg.instanceSlug = vals.instance;
1758
+ if (vals.token) {
1759
+ cfg.token = vals.token;
1760
+ }
1761
+ writeCloudConfig(cfg);
1653
1762
  }
1654
1763
  else {
1655
- cloudUrl = vals.url || "";
1764
+ if (!vals.url) {
1765
+ viewerLines = [pc.red("Cloud URL is required.")];
1766
+ draw();
1767
+ return;
1768
+ }
1769
+ cloudUrl = vals.url;
1770
+ // Save manual credentials to cloud config
1771
+ writeCloudConfig({ token: vals.token || "", url: cloudUrl });
1656
1772
  }
1657
1773
  await connectToDriver(selectedDriver, cloudUrl, cloudUrl, vals.token);
1658
1774
  }
@@ -1722,6 +1838,39 @@ async function handleSwitch() {
1722
1838
  scheduleLoad(first);
1723
1839
  }
1724
1840
  }
1841
+ async function handleLogout() {
1842
+ // Close current connection if any
1843
+ if (connected && db) {
1844
+ try {
1845
+ await db.close();
1846
+ }
1847
+ catch {
1848
+ // ignore close errors
1849
+ }
1850
+ }
1851
+ // Remove cloud credentials from disk
1852
+ removeCloudConfig();
1853
+ // Reset state
1854
+ connected = false;
1855
+ driver = "";
1856
+ dbPath = "";
1857
+ authToken = "";
1858
+ cloudError = null;
1859
+ collections = [];
1860
+ streams = [];
1861
+ queues = [];
1862
+ objectsByCollection = new Map();
1863
+ cursorIndex = 0;
1864
+ scrollOffset = 0;
1865
+ loadedItemId = "";
1866
+ viewerLines = [pc.green("Logged out."), pc.dim("Select an environment to connect.")];
1867
+ draw();
1868
+ const tree = buildTree();
1869
+ const first = tree[cursorIndex];
1870
+ if (first) {
1871
+ scheduleLoad(first);
1872
+ }
1873
+ }
1725
1874
  // ── Entry Point ──────────────────────────────────────────────────────
1726
1875
  export async function runInteractiveCli() {
1727
1876
  // Go straight into the TUI — no pre-prompts
@@ -1741,12 +1890,20 @@ export async function runInteractiveCli() {
1741
1890
  }
1742
1891
  // Auto-connect to cloud if credentials exist
1743
1892
  const cloudCfg = readCloudConfig();
1744
- if (cloudCfg?.token && cloudCfg?.url) {
1745
- try {
1746
- await connectToDriver("cloud", cloudCfg.url, cloudCfg.url, cloudCfg.token);
1747
- }
1748
- catch {
1749
- // Auto-connect failed — fall through to environment selection
1893
+ if (cloudCfg?.token) {
1894
+ const cloudUrl = resolveCloudUrl(cloudCfg);
1895
+ if (cloudUrl) {
1896
+ try {
1897
+ await connectToDriver("cloud", cloudUrl, cloudUrl, cloudCfg.token);
1898
+ }
1899
+ catch (err) {
1900
+ const msg = err instanceof Error ? err.message : String(err);
1901
+ viewerLines = [
1902
+ pc.yellow(`Auto-connect failed: ${msg}`),
1903
+ pc.dim("Select an environment to connect, or press 'r' to retry."),
1904
+ ];
1905
+ draw();
1906
+ }
1750
1907
  }
1751
1908
  }
1752
1909
  setupKeypress();
@@ -87,4 +87,14 @@ export declare function pollCliAuth(config: CloudConfig, code: string): Promise<
87
87
  } | {
88
88
  status: string;
89
89
  }>;
90
+ export type ResolvedInstance = {
91
+ mcpUrl: string;
92
+ projectSlug: string;
93
+ instanceSlug: string;
94
+ };
95
+ /**
96
+ * Fetch the first available cloud instance for the logged-in user.
97
+ * Returns null if no projects or instances exist.
98
+ */
99
+ export declare function resolveFirstInstance(config: CloudConfig): Promise<ResolvedInstance | null>;
90
100
  //# sourceMappingURL=cloud-api.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cloud-api.d.ts","sourceRoot":"","sources":["../../src/lib/cloud-api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cloud-api.d.ts","sourceRoot":"","sources":["../../src/lib/cloud-api.ts"],"names":[],"mappings":"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"}
@@ -99,3 +99,32 @@ export async function startCliAuth(config) {
99
99
  export async function pollCliAuth(config, code) {
100
100
  return requestUnauthenticated(config.url ?? DEFAULT_API_URL, "/auth/cli/poll", { code });
101
101
  }
102
+ /**
103
+ * Fetch the first available cloud instance for the logged-in user.
104
+ * Returns null if no projects or instances exist.
105
+ */
106
+ export async function resolveFirstInstance(config) {
107
+ try {
108
+ const { projects } = await listProjects(config);
109
+ for (const project of projects) {
110
+ try {
111
+ const { instances } = await listInstances(config, project.id);
112
+ const instance = instances[0];
113
+ if (instance?.mcpUrl) {
114
+ return {
115
+ mcpUrl: instance.mcpUrl,
116
+ projectSlug: project.slug,
117
+ instanceSlug: instance.slug,
118
+ };
119
+ }
120
+ }
121
+ catch {
122
+ // Skip projects that fail to list instances
123
+ }
124
+ }
125
+ }
126
+ catch {
127
+ // API unreachable or token invalid
128
+ }
129
+ return null;
130
+ }
@@ -4,9 +4,20 @@ export type CloudConfig = {
4
4
  url?: string;
5
5
  /** Currently active organization context (set by `thingd cloud org use`). */
6
6
  organizationId?: string;
7
+ /** Resolved MCP URL for the active cloud instance (auto-discovered or set by `thingd cloud instance use`). */
8
+ instanceUrl?: string;
9
+ /** Active project slug (set when instanceUrl is resolved). */
10
+ projectSlug?: string;
11
+ /** Active instance slug (set when instanceUrl is resolved). */
12
+ instanceSlug?: string;
7
13
  };
8
14
  export declare function cloudConfigPath(): string;
9
15
  export declare function readCloudConfig(): CloudConfig | null;
10
16
  export declare function writeCloudConfig(config: CloudConfig): void;
17
+ /**
18
+ * Returns the best available cloud MCP URL from saved config.
19
+ * Priority: instanceUrl > url (with /mcp appended if bare).
20
+ */
21
+ export declare function resolveCloudUrl(config: CloudConfig): string | undefined;
11
22
  export declare function removeCloudConfig(): void;
12
23
  //# sourceMappingURL=cloud-config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cloud-config.d.ts","sourceRoot":"","sources":["../../src/lib/cloud-config.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,wBAAgB,eAAe,IAAI,WAAW,GAAG,IAAI,CAUpD;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAG1D;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAMxC"}
1
+ {"version":3,"file":"cloud-config.d.ts","sourceRoot":"","sources":["../../src/lib/cloud-config.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8GAA8G;IAC9G,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,wBAAgB,eAAe,IAAI,WAAW,GAAG,IAAI,CAUpD;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAG1D;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAYvE;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAMxC"}
@@ -21,6 +21,23 @@ export function writeCloudConfig(config) {
21
21
  ensureThingdDir();
22
22
  writeFileSync(cloudConfigPath(), JSON.stringify(config, null, 2), "utf-8");
23
23
  }
24
+ /**
25
+ * Returns the best available cloud MCP URL from saved config.
26
+ * Priority: instanceUrl > url (with /mcp appended if bare).
27
+ */
28
+ export function resolveCloudUrl(config) {
29
+ if (config.instanceUrl) {
30
+ return config.instanceUrl;
31
+ }
32
+ if (config.url) {
33
+ const u = new URL(config.url);
34
+ if (u.pathname === "" || u.pathname === "/") {
35
+ u.pathname = "/mcp";
36
+ }
37
+ return u.toString();
38
+ }
39
+ return undefined;
40
+ }
24
41
  export function removeCloudConfig() {
25
42
  try {
26
43
  unlinkSync(cloudConfigPath());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thingd/cli",
3
- "version": "0.49.9",
3
+ "version": "0.50.0",
4
4
  "description": "CLI, Interactive TUI Dashboard, and MCP server for thingd — a fast object-first data engine for applications and AI agents.",
5
5
  "type": "module",
6
6
  "homepage": "https://engine.thingd.cloud",
@@ -45,7 +45,7 @@
45
45
  "cli-table3": "^0.6.5",
46
46
  "picocolors": "^1.1.1",
47
47
  "zod": "^4.4.3",
48
- "@thingd/sdk": "0.49.9"
48
+ "@thingd/sdk": "0.50.0"
49
49
  },
50
50
  "engines": {
51
51
  "node": ">=24.0.0"