@thingd/cli 0.55.0 → 0.56.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.
@@ -1 +1 @@
1
- {"version":3,"file":"interactive.d.ts","sourceRoot":"","sources":["../src/interactive.ts"],"names":[],"mappings":"AA+/EA,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA0FvD"}
1
+ {"version":3,"file":"interactive.d.ts","sourceRoot":"","sources":["../src/interactive.ts"],"names":[],"mappings":"AAu8FA,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA0FvD"}
@@ -69,6 +69,7 @@ let collections = [];
69
69
  let streams = [];
70
70
  let queues = [];
71
71
  let objectsByCollection = new Map();
72
+ const collectionOptions = new Map();
72
73
  const expandedSet = new Set(["cat:collections", "cat:streams", "cat:queues"]);
73
74
  let cursorIndex = 0;
74
75
  let maintenanceCursor = 0;
@@ -244,7 +245,21 @@ async function fetchResourcesFallback() {
244
245
  objectsByCollection.clear();
245
246
  await Promise.all(collections.map(async (col) => {
246
247
  try {
247
- const list = await db.listObjects(col);
248
+ const opts = collectionOptions.get(col);
249
+ const listOpts = {};
250
+ if (opts?.sortBy) {
251
+ listOpts.sortBy = { field: opts.sortBy, direction: opts.sortDir ?? "asc" };
252
+ }
253
+ if (opts?.limit) {
254
+ listOpts.limit = opts.limit;
255
+ }
256
+ if (opts?.offset) {
257
+ listOpts.offset = opts.offset;
258
+ }
259
+ if (opts?.filter) {
260
+ listOpts.filter = opts.filter;
261
+ }
262
+ const list = await db.listObjects(col, Object.keys(listOpts).length > 0 ? listOpts : undefined);
248
263
  objectsByCollection.set(col, list.map((o) => o.id));
249
264
  }
250
265
  catch {
@@ -289,7 +304,21 @@ async function fetchResources() {
289
304
  objectsByCollection.clear();
290
305
  for (const col of collections) {
291
306
  try {
292
- const list = await db.listObjects(col);
307
+ const opts = collectionOptions.get(col);
308
+ const listOpts = {};
309
+ if (opts?.sortBy) {
310
+ listOpts.sortBy = { field: opts.sortBy, direction: opts.sortDir ?? "asc" };
311
+ }
312
+ if (opts?.limit) {
313
+ listOpts.limit = opts.limit;
314
+ }
315
+ if (opts?.offset) {
316
+ listOpts.offset = opts.offset;
317
+ }
318
+ if (opts?.filter) {
319
+ listOpts.filter = opts.filter;
320
+ }
321
+ const list = await db.listObjects(col, Object.keys(listOpts).length > 0 ? listOpts : undefined);
293
322
  objectsByCollection.set(col, list.map((o) => o.id));
294
323
  }
295
324
  catch {
@@ -758,6 +787,27 @@ async function loadContent(node) {
758
787
  const ref = node.ref;
759
788
  const objs = objectsByCollection.get(ref.name) ?? [];
760
789
  let res = `${pc.bold(ref.name)} ${pc.dim(`(${objs.length} objects)`)}\n\n`;
790
+ // Schema info
791
+ try {
792
+ const schemas = await db.schema(ref.name);
793
+ if (schemas.length > 0) {
794
+ const fields = schemas[0]?.fields ?? [];
795
+ if (fields.length > 0) {
796
+ res += `${pc.bold("Fields")}\n`;
797
+ for (const f of fields) {
798
+ const icon = f.nullable ? pc.dim("⊙") : pc.cyan("◎");
799
+ const sample = f.sampleValues.length > 0
800
+ ? pc.dim(` e.g. ${String(f.sampleValues[0]).slice(0, 20)}`)
801
+ : "";
802
+ res += ` ${icon} ${f.name}: ${f.type}${sample}\n`;
803
+ }
804
+ res += "\n";
805
+ }
806
+ }
807
+ }
808
+ catch {
809
+ // Schema not available for this store
810
+ }
761
811
  if (objs.length === 0) {
762
812
  res += pc.dim("No objects in this collection.");
763
813
  }
@@ -1041,7 +1091,7 @@ function draw() {
1041
1091
  help = ` ${pc.dim("↑↓")} nav ${pc.dim("enter")} connect ${pc.dim("q")} quit `;
1042
1092
  }
1043
1093
  else {
1044
- help = ` ${pc.dim("↑↓")} nav ${pc.dim("←→")} toggle ${pc.dim("c")} create ${pc.dim("e")} edit ${pc.dim("d")} delete ${pc.dim("/")} search ${pc.dim("n")} neighbors ${pc.dim("i")} info ${pc.dim("r")} refresh ${pc.dim("s")} switch ${pc.dim("l")} logout ${pc.dim("q")} quit `;
1094
+ help = ` ${pc.dim("↑↓")} nav ${pc.dim("←→")} toggle ${pc.dim("c")} create ${pc.dim("e")} edit ${pc.dim("d")} delete ${pc.dim("/")} search ${pc.dim("n")} neighbors ${pc.dim("N")} nlq ${pc.dim("a")} agg ${pc.dim("t")} ts ${pc.dim("o")} options ${pc.dim("b")} batch ${pc.dim("i")} info ${pc.dim("r")} refresh ${pc.dim("s")} switch ${pc.dim("l")} logout ${pc.dim("q")} quit `;
1045
1095
  }
1046
1096
  buf += `${pc.dim("─".repeat(W))}\n`;
1047
1097
  buf += padToWidth(help, W);
@@ -1661,6 +1711,316 @@ async function handleNeighbors(selected) {
1661
1711
  draw();
1662
1712
  });
1663
1713
  }
1714
+ async function handleAggregate(selected) {
1715
+ const defaultCol = selected?.type === "collection"
1716
+ ? (selected.ref?.name ?? "")
1717
+ : selected?.type === "object"
1718
+ ? (selected.ref?.collection ?? "")
1719
+ : "";
1720
+ openForm("Aggregate", [
1721
+ {
1722
+ id: "function",
1723
+ label: "Function",
1724
+ value: "count",
1725
+ options: ["count", "sum", "avg", "min", "max"],
1726
+ },
1727
+ {
1728
+ id: "collection",
1729
+ label: "Collection",
1730
+ value: defaultCol,
1731
+ options: collections,
1732
+ allowCustom: true,
1733
+ },
1734
+ { id: "field", label: "Field (for sum/avg/min/max)", placeholder: "field name" },
1735
+ { id: "groupBy", label: "Group By (optional field)", placeholder: "field name" },
1736
+ { id: "filter", label: "Filter (optional JSON)", placeholder: '{"status":"active"}' },
1737
+ ], async (vals) => {
1738
+ const func = vals.function || "count";
1739
+ const collection = (vals.collection || "").trim();
1740
+ if (!collection) {
1741
+ throw new Error("Collection is required.");
1742
+ }
1743
+ const field = (vals.field || "").trim() || undefined;
1744
+ const groupBy = (vals.groupBy || "").trim() || undefined;
1745
+ const filter = vals.filter?.trim() ? JSON.parse(vals.filter.trim()) : undefined;
1746
+ const options = { groupBy, filter };
1747
+ const a = db.aggregate;
1748
+ const result = func === "count"
1749
+ ? await a.count(collection, options)
1750
+ : func === "sum"
1751
+ ? await a.sum(collection, field, options)
1752
+ : func === "avg"
1753
+ ? await a.avg(collection, field, options)
1754
+ : func === "min"
1755
+ ? await a.min(collection, field, options)
1756
+ : await a.max(collection, field, options);
1757
+ const lines = [
1758
+ ` ${pc.bold("Aggregate")} ${pc.cyan(func)} ${pc.dim(`on ${collection}`)}`,
1759
+ "",
1760
+ ` ${pc.dim("Total:")} ${pc.bold(String(result.total))}`,
1761
+ "",
1762
+ ];
1763
+ if (result.groups && result.groups.length > 0) {
1764
+ lines.push(` ${pc.bold("Groups")}`);
1765
+ const maxVal = Math.max(...result.groups.map((g) => g.value));
1766
+ for (const g of result.groups) {
1767
+ const barLen = Math.max(1, Math.round((g.value / maxVal) * 20));
1768
+ const bar = pc.cyan("█".repeat(barLen));
1769
+ lines.push(` ${g.key}: ${bar} ${pc.dim(String(g.value))}`);
1770
+ }
1771
+ }
1772
+ viewerLines = lines;
1773
+ loadedItemId = "aggregate_result";
1774
+ draw();
1775
+ });
1776
+ }
1777
+ async function handleTimeseries(selected) {
1778
+ const defaultCol = selected?.type === "collection"
1779
+ ? (selected.ref?.name ?? "")
1780
+ : selected?.type === "object"
1781
+ ? (selected.ref?.collection ?? "")
1782
+ : "";
1783
+ openForm("Time Series", [
1784
+ {
1785
+ id: "function",
1786
+ label: "Function",
1787
+ value: "count",
1788
+ options: ["count", "sum", "avg", "min", "max"],
1789
+ },
1790
+ {
1791
+ id: "collection",
1792
+ label: "Collection",
1793
+ value: defaultCol,
1794
+ options: collections,
1795
+ allowCustom: true,
1796
+ },
1797
+ { id: "field", label: "Field (optional)", placeholder: "field name" },
1798
+ {
1799
+ id: "bucket",
1800
+ label: "Bucket",
1801
+ value: "day",
1802
+ options: ["hour", "day", "week", "month"],
1803
+ },
1804
+ { id: "from", label: "From (ISO date, optional)", placeholder: "2024-01-01" },
1805
+ { id: "to", label: "To (ISO date, optional)", placeholder: "2024-12-31" },
1806
+ { id: "filter", label: "Filter (optional JSON)", placeholder: '{"status":"active"}' },
1807
+ ], async (vals) => {
1808
+ const func = vals.function || "count";
1809
+ const collection = (vals.collection || "").trim();
1810
+ if (!collection) {
1811
+ throw new Error("Collection is required.");
1812
+ }
1813
+ const field = (vals.field || "").trim() || undefined;
1814
+ const bucket = (vals.bucket || "day");
1815
+ const from = (vals.from || "").trim() || undefined;
1816
+ const to = (vals.to || "").trim() || undefined;
1817
+ const filter = vals.filter?.trim() ? JSON.parse(vals.filter.trim()) : undefined;
1818
+ const result = await db.timeseries(collection, {
1819
+ function: func,
1820
+ field,
1821
+ bucket,
1822
+ from,
1823
+ to,
1824
+ filter,
1825
+ });
1826
+ const lines = [
1827
+ ` ${pc.bold("Time Series")} ${pc.cyan(func)} ${pc.dim(`on ${collection}, ${bucket}ly`)}`,
1828
+ "",
1829
+ ];
1830
+ if (result.buckets.length === 0) {
1831
+ lines.push(pc.dim("No data for the selected range."));
1832
+ }
1833
+ else {
1834
+ const maxVal = Math.max(...result.buckets.map((b) => b.value));
1835
+ for (const b of result.buckets) {
1836
+ const barLen = Math.max(1, Math.round((b.value / maxVal) * 20));
1837
+ const bar = pc.green("█".repeat(barLen));
1838
+ lines.push(` ${b.label}: ${bar} ${pc.dim(String(b.value))}`);
1839
+ }
1840
+ }
1841
+ viewerLines = lines;
1842
+ loadedItemId = "timeseries_result";
1843
+ draw();
1844
+ });
1845
+ }
1846
+ async function handleNlq(selected) {
1847
+ const defaultCol = selected?.type === "collection"
1848
+ ? (selected.ref?.name ?? "")
1849
+ : selected?.type === "object"
1850
+ ? (selected.ref?.collection ?? "")
1851
+ : "";
1852
+ openForm("Natural Language Query", [
1853
+ { id: "question", label: "Question", placeholder: "How many active users?" },
1854
+ {
1855
+ id: "collection",
1856
+ label: "Collection (optional)",
1857
+ value: defaultCol,
1858
+ options: collections,
1859
+ allowCustom: true,
1860
+ },
1861
+ ], async (vals) => {
1862
+ const question = (vals.question || "").trim();
1863
+ if (!question) {
1864
+ throw new Error("Question is required.");
1865
+ }
1866
+ const collection = (vals.collection || "").trim() || undefined;
1867
+ const result = await db.nlq.query(question, {
1868
+ collection,
1869
+ });
1870
+ const intent = result.intent;
1871
+ const lines = [
1872
+ ` ${pc.bold("NLQ Result")}`,
1873
+ ` ${pc.dim(`Question: ${question}`)}`,
1874
+ "",
1875
+ ` ${result.answer}`,
1876
+ "",
1877
+ ];
1878
+ if (intent) {
1879
+ lines.push(` ${pc.dim("Interpreted as:")} ${intent.action} ${pc.dim("on")} ${pc.cyan(intent.collection)}`);
1880
+ if (intent.function) {
1881
+ lines.push(` ${pc.dim("Function:")} ${intent.function}`);
1882
+ }
1883
+ if (intent.field) {
1884
+ lines.push(` ${pc.dim("Field:")} ${intent.field}`);
1885
+ }
1886
+ if (intent.query) {
1887
+ lines.push(` ${pc.dim("Query:")} ${intent.query}`);
1888
+ }
1889
+ }
1890
+ if (result.data !== undefined && result.data !== null) {
1891
+ lines.push("", highlightJson(result.data));
1892
+ }
1893
+ viewerLines = lines;
1894
+ loadedItemId = "nlq_result";
1895
+ draw();
1896
+ });
1897
+ }
1898
+ async function handleCollectionOptions(selected) {
1899
+ const colName = selected?.type === "collection"
1900
+ ? (selected.ref?.name ?? "")
1901
+ : selected?.type === "object"
1902
+ ? (selected.ref?.collection ?? "")
1903
+ : "";
1904
+ if (!colName) {
1905
+ viewerLines = [pc.yellow("Select a collection first, then press [o] to set listing options.")];
1906
+ loadedItemId = "options_info";
1907
+ draw();
1908
+ return;
1909
+ }
1910
+ const current = collectionOptions.get(colName) ?? {};
1911
+ openForm(`Listing Options: ${colName}`, [
1912
+ {
1913
+ id: "sortBy",
1914
+ label: "Sort By",
1915
+ value: current.sortBy ?? "",
1916
+ options: ["", "id", "created_at", "updated_at", "version"],
1917
+ allowCustom: true,
1918
+ },
1919
+ {
1920
+ id: "sortDir",
1921
+ label: "Sort Direction",
1922
+ value: current.sortDir ?? "asc",
1923
+ options: ["asc", "desc"],
1924
+ },
1925
+ { id: "limit", label: "Limit", value: String(current.limit ?? ""), placeholder: "50" },
1926
+ { id: "offset", label: "Offset", value: String(current.offset ?? ""), placeholder: "0" },
1927
+ {
1928
+ id: "filter",
1929
+ label: "Filter (JSON)",
1930
+ value: current.filter ? JSON.stringify(current.filter) : "",
1931
+ placeholder: '{"status":"active"}',
1932
+ },
1933
+ ], async (vals) => {
1934
+ const opts = {};
1935
+ if (vals.sortBy) {
1936
+ opts.sortBy = vals.sortBy;
1937
+ opts.sortDir = vals.sortDir || "asc";
1938
+ }
1939
+ if (vals.limit) {
1940
+ opts.limit = parseInt(vals.limit, 10) || 50;
1941
+ }
1942
+ if (vals.offset) {
1943
+ opts.offset = parseInt(vals.offset, 10) || 0;
1944
+ }
1945
+ if (vals.filter?.trim()) {
1946
+ opts.filter = JSON.parse(vals.filter.trim());
1947
+ }
1948
+ if (Object.keys(opts).length > 0) {
1949
+ collectionOptions.set(colName, opts);
1950
+ }
1951
+ else {
1952
+ collectionOptions.delete(colName);
1953
+ }
1954
+ await fetchResources();
1955
+ const tree = buildTree();
1956
+ const idx = tree.findIndex((n) => n.id === `col:${colName}`);
1957
+ if (idx !== -1) {
1958
+ cursorIndex = idx;
1959
+ }
1960
+ const n = tree[cursorIndex];
1961
+ if (n) {
1962
+ scheduleLoad(n);
1963
+ }
1964
+ });
1965
+ }
1966
+ async function handleBatchOps(selected) {
1967
+ const colName = selected?.type === "collection"
1968
+ ? (selected.ref?.name ?? "")
1969
+ : selected?.type === "object"
1970
+ ? (selected.ref?.collection ?? "")
1971
+ : "";
1972
+ if (!colName) {
1973
+ viewerLines = [pc.yellow("Select a collection first, then press [b] for batch operations.")];
1974
+ loadedItemId = "batch_info";
1975
+ draw();
1976
+ return;
1977
+ }
1978
+ openForm(`Batch Ops: ${colName}`, [
1979
+ {
1980
+ id: "action",
1981
+ label: "Action",
1982
+ value: "put",
1983
+ options: ["put", "delete"],
1984
+ },
1985
+ {
1986
+ id: "input",
1987
+ label: "JSON File Path or IDs (comma-sep for delete)",
1988
+ placeholder: "/path/to/file.json or id1,id2,id3",
1989
+ },
1990
+ ], async (vals) => {
1991
+ const action = vals.action || "";
1992
+ const input = (vals.input || "").trim();
1993
+ if (!input) {
1994
+ throw new Error("Input is required.");
1995
+ }
1996
+ if (action === "put") {
1997
+ const data = JSON.parse(await fs.promises.readFile(input, "utf-8"));
1998
+ const objects = Array.isArray(data) ? data : (data.objects ?? [data]);
1999
+ const result = await db.putBatch(colName, objects);
2000
+ viewerLines = [
2001
+ ` ${pc.bold("Batch Put Complete")}`,
2002
+ ` ${pc.dim(`Collection: ${colName}`)}`,
2003
+ ` ${pc.dim(`Objects: ${result.length}`)}`,
2004
+ ];
2005
+ loadedItemId = "batch_result";
2006
+ draw();
2007
+ }
2008
+ else if (action === "delete") {
2009
+ const ids = input
2010
+ .split(",")
2011
+ .map((s) => s.trim())
2012
+ .filter(Boolean);
2013
+ const count = await db.deleteBatch(colName, ids);
2014
+ viewerLines = [
2015
+ ` ${pc.bold("Batch Delete Complete")}`,
2016
+ ` ${pc.dim(`Collection: ${colName}`)}`,
2017
+ ` ${pc.dim(`Deleted: ${count} objects`)}`,
2018
+ ];
2019
+ loadedItemId = "batch_result";
2020
+ draw();
2021
+ }
2022
+ });
2023
+ }
1664
2024
  async function handleMaintenance() {
1665
2025
  // Cycle through maintenance operations with each press of 'm'
1666
2026
  const operations = ["health", "checkpoint", "backup"];
@@ -1956,9 +2316,24 @@ function setupKeypress() {
1956
2316
  else if (str === "i" || str === "I") {
1957
2317
  await handleInfo();
1958
2318
  }
1959
- else if (str === "n" || str === "N") {
2319
+ else if (str === "n") {
1960
2320
  await handleNeighbors(tree[cursorIndex]);
1961
2321
  }
2322
+ else if (str === "N") {
2323
+ await handleNlq(tree[cursorIndex]);
2324
+ }
2325
+ else if (str === "a" || str === "A") {
2326
+ await handleAggregate(tree[cursorIndex]);
2327
+ }
2328
+ else if (str === "t" || str === "T") {
2329
+ await handleTimeseries(tree[cursorIndex]);
2330
+ }
2331
+ else if (str === "o" || str === "O") {
2332
+ await handleCollectionOptions(tree[cursorIndex]);
2333
+ }
2334
+ else if (str === "b" || str === "B") {
2335
+ await handleBatchOps(tree[cursorIndex]);
2336
+ }
1962
2337
  else if (str === "m" || str === "M") {
1963
2338
  await handleMaintenance();
1964
2339
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thingd/cli",
3
- "version": "0.55.0",
3
+ "version": "0.56.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.55.0"
48
+ "@thingd/sdk": "0.56.0"
49
49
  },
50
50
  "engines": {
51
51
  "node": ">=24.0.0"