@thingd/cli 0.55.0 → 0.57.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":"AA8lGA,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;
@@ -91,6 +92,7 @@ let objectWriteRateHistory = [];
91
92
  let eventAppendRateHistory = [];
92
93
  let viewerLines = ["Select an item to view details."];
93
94
  let viewerScroll = 0;
95
+ let showHelp = false;
94
96
  let lastNeighborsRef = "";
95
97
  let loadedItemId = "";
96
98
  let loadTimer = null;
@@ -244,7 +246,21 @@ async function fetchResourcesFallback() {
244
246
  objectsByCollection.clear();
245
247
  await Promise.all(collections.map(async (col) => {
246
248
  try {
247
- const list = await db.listObjects(col);
249
+ const opts = collectionOptions.get(col);
250
+ const listOpts = {};
251
+ if (opts?.sortBy) {
252
+ listOpts.sortBy = { field: opts.sortBy, direction: opts.sortDir ?? "asc" };
253
+ }
254
+ if (opts?.limit) {
255
+ listOpts.limit = opts.limit;
256
+ }
257
+ if (opts?.offset) {
258
+ listOpts.offset = opts.offset;
259
+ }
260
+ if (opts?.filter) {
261
+ listOpts.filter = opts.filter;
262
+ }
263
+ const list = await db.listObjects(col, Object.keys(listOpts).length > 0 ? listOpts : undefined);
248
264
  objectsByCollection.set(col, list.map((o) => o.id));
249
265
  }
250
266
  catch {
@@ -289,7 +305,21 @@ async function fetchResources() {
289
305
  objectsByCollection.clear();
290
306
  for (const col of collections) {
291
307
  try {
292
- const list = await db.listObjects(col);
308
+ const opts = collectionOptions.get(col);
309
+ const listOpts = {};
310
+ if (opts?.sortBy) {
311
+ listOpts.sortBy = { field: opts.sortBy, direction: opts.sortDir ?? "asc" };
312
+ }
313
+ if (opts?.limit) {
314
+ listOpts.limit = opts.limit;
315
+ }
316
+ if (opts?.offset) {
317
+ listOpts.offset = opts.offset;
318
+ }
319
+ if (opts?.filter) {
320
+ listOpts.filter = opts.filter;
321
+ }
322
+ const list = await db.listObjects(col, Object.keys(listOpts).length > 0 ? listOpts : undefined);
293
323
  objectsByCollection.set(col, list.map((o) => o.id));
294
324
  }
295
325
  catch {
@@ -758,6 +788,27 @@ async function loadContent(node) {
758
788
  const ref = node.ref;
759
789
  const objs = objectsByCollection.get(ref.name) ?? [];
760
790
  let res = `${pc.bold(ref.name)} ${pc.dim(`(${objs.length} objects)`)}\n\n`;
791
+ // Schema info
792
+ try {
793
+ const schemas = await db.schema(ref.name);
794
+ if (schemas.length > 0) {
795
+ const fields = schemas[0]?.fields ?? [];
796
+ if (fields.length > 0) {
797
+ res += `${pc.bold("Fields")}\n`;
798
+ for (const f of fields) {
799
+ const icon = f.nullable ? pc.dim("⊙") : pc.cyan("◎");
800
+ const sample = f.sampleValues.length > 0
801
+ ? pc.dim(` e.g. ${String(f.sampleValues[0]).slice(0, 20)}`)
802
+ : "";
803
+ res += ` ${icon} ${f.name}: ${f.type}${sample}\n`;
804
+ }
805
+ res += "\n";
806
+ }
807
+ }
808
+ }
809
+ catch {
810
+ // Schema not available for this store
811
+ }
761
812
  if (objs.length === 0) {
762
813
  res += pc.dim("No objects in this collection.");
763
814
  }
@@ -865,6 +916,38 @@ async function loadContent(node) {
865
916
  content += ` ${pc.dim("Links".padEnd(14))} ${pc.blue(String(totalLinksCount).padEnd(6))} ${pc.dim("total")}\n`;
866
917
  content += ` ${pc.dim("Active Jobs".padEnd(14))} ${pc.yellow(String(totalActiveJobsCount).padEnd(6))} ${pc.dim("in flight")}\n`;
867
918
  content += ` ${pc.dim("Dead Jobs".padEnd(14))} ${pc.red(String(totalDeadJobsCount).padEnd(6))} ${pc.dim("failed")}\n\n`;
919
+ // Collection breakdown bar chart
920
+ if (collections.length > 0) {
921
+ content += ` ${pc.bold("Collections")}\n`;
922
+ const maxCount = Math.max(1, ...collections.map((c) => (objectsByCollection.get(c) ?? []).length));
923
+ const barMax = Math.max(5, viewW - 30);
924
+ for (const col of collections) {
925
+ const count = (objectsByCollection.get(col) ?? []).length;
926
+ const barLen = Math.max(1, Math.round((count / maxCount) * barMax));
927
+ const bar = barLen > 0 ? pc.cyan("█".repeat(barLen)) : "";
928
+ content += ` ${pc.dim(col.slice(0, 12).padEnd(12))} ${bar} ${pc.dim(String(count))}\n`;
929
+ }
930
+ content += "\n";
931
+ }
932
+ // Capacity gauge bar
933
+ const totalHistorical = totalObjects +
934
+ totalEventsCount +
935
+ totalLinksCount +
936
+ totalActiveJobsCount +
937
+ totalDeadJobsCount;
938
+ if (totalHistorical > 0) {
939
+ const capBarW = Math.max(5, viewW - 20);
940
+ const objFrac = totalObjects / totalHistorical;
941
+ const evtFrac = totalEventsCount / totalHistorical;
942
+ const linkFrac = totalLinksCount / totalHistorical;
943
+ const objChars = Math.round(objFrac * capBarW);
944
+ const evtChars = Math.round(evtFrac * capBarW);
945
+ const linkChars = Math.round(linkFrac * capBarW);
946
+ const jobChars = capBarW - objChars - evtChars - linkChars;
947
+ content += ` ${pc.bold("Distribution")}\n`;
948
+ content += ` ${pc.dim(" ")}${pc.cyan("█".repeat(Math.max(0, objChars)))}${pc.green("█".repeat(Math.max(0, evtChars)))}${pc.blue("█".repeat(Math.max(0, linkChars)))}${pc.yellow("█".repeat(Math.max(0, jobChars)))}\n`;
949
+ content += ` ${pc.cyan("■")} objs ${pc.green("■")} evts ${pc.blue("■")} links ${pc.yellow("■")} jobs\n\n`;
950
+ }
868
951
  content += ` ${pc.bold("Connection")}\n`;
869
952
  content += ` ${pc.dim("Driver".padEnd(14))} ${driverName}\n`;
870
953
  content += ` ${pc.dim("Path".padEnd(14))} ${dbPath || ":memory:"}\n`;
@@ -948,14 +1031,20 @@ function draw() {
948
1031
  scrollOffset = Math.max(0, Math.min(scrollOffset, Math.max(0, tree.length - bodyH)));
949
1032
  let buf = "\u001B[H"; // Move to top-left
950
1033
  // Header — opencode style: clean, no inverse bar
951
- if (!connected) {
952
- buf += ` ${pc.cyan("◈")} ${pc.bold("thingd")} ${pc.dim("Select Environment")}\n`;
1034
+ if (showHelp) {
1035
+ buf += ` ${pc.cyan("◈")} ${pc.bold("thingd")} ${pc.dim("Help (press any key)")}\n`;
1036
+ }
1037
+ else if (!connected) {
1038
+ buf += ` ${pc.cyan("◈")} ${pc.bold("thingd")} ${pc.dim("Select Environment")} ${pc.dim("[?] help")}\n`;
953
1039
  }
954
1040
  else if (formState?.active) {
955
- buf += ` ${pc.cyan("◈")} ${pc.bold("thingd")} ${pc.cyan(driver.toUpperCase())} ${pc.dim("Input Mode")}\n`;
1041
+ buf += ` ${pc.cyan("◈")} ${pc.bold("thingd")} ${pc.cyan(driver.toUpperCase())} ${pc.dim("Input Mode")} ${pc.dim("[?] help")}\n`;
956
1042
  }
957
1043
  else {
958
- const label = ` ${pc.cyan("◈")} ${pc.bold("thingd")} ${pc.cyan(driver.toUpperCase())} ${pc.dim(dbPath)}`;
1044
+ const breadcrumb = computeBreadcrumbs();
1045
+ const base = ` ${pc.cyan("◈")} ${pc.bold("thingd")} ${pc.cyan(driver.toUpperCase())}`;
1046
+ const dash = breadcrumb ? ` ${pc.dim("▸")} ` : "";
1047
+ const label = `${base}${dash}${breadcrumb} ${pc.dim("[?] help")}`;
959
1048
  buf += `${padToWidth(label, W)}\n`;
960
1049
  }
961
1050
  buf += `${pc.dim("─".repeat(W))}\n`;
@@ -1041,14 +1130,107 @@ function draw() {
1041
1130
  help = ` ${pc.dim("↑↓")} nav ${pc.dim("enter")} connect ${pc.dim("q")} quit `;
1042
1131
  }
1043
1132
  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 `;
1133
+ 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
1134
  }
1046
1135
  buf += `${pc.dim("─".repeat(W))}\n`;
1047
1136
  buf += padToWidth(help, W);
1048
1137
  // Clear to end
1049
1138
  buf += "\u001B[J";
1139
+ if (showHelp) {
1140
+ buf += computeHelpOverlay(W, H);
1141
+ }
1050
1142
  process.stdout.write(buf);
1051
1143
  }
1144
+ function computeBreadcrumbs() {
1145
+ if (!connected) {
1146
+ return "";
1147
+ }
1148
+ const tree = buildTree();
1149
+ const node = tree[cursorIndex];
1150
+ if (!node) {
1151
+ return "";
1152
+ }
1153
+ const labels = [];
1154
+ const climb = (id) => {
1155
+ if (!id) {
1156
+ return;
1157
+ }
1158
+ const n = tree.find((t) => t.id === id);
1159
+ if (!n) {
1160
+ return;
1161
+ }
1162
+ if (n.parentId) {
1163
+ climb(n.parentId);
1164
+ }
1165
+ const clean = n.label
1166
+ .replace(/[▾▸●○·◇◈◉⬡]/g, "")
1167
+ .trim()
1168
+ .replace(/^\S+\s+/, "")
1169
+ .trim();
1170
+ if (clean && !clean.startsWith("(")) {
1171
+ labels.push(clean);
1172
+ }
1173
+ };
1174
+ climb(node.id);
1175
+ return labels.length > 0 ? pc.dim(labels.join(" > ")) : "";
1176
+ }
1177
+ function computeHelpOverlay(W, H) {
1178
+ const lines = [
1179
+ `${pc.bold(" thingd TUI Help")}`,
1180
+ "",
1181
+ ` ${pc.bold("Navigation")}`,
1182
+ ` ${pc.dim("↑↓/j/k")} Move cursor`,
1183
+ ` ${pc.dim("←→/h/l")} Expand/collapse tree`,
1184
+ ` ${pc.dim("Enter")} Toggle expand`,
1185
+ "",
1186
+ ` ${pc.bold("Operations")}`,
1187
+ ` ${pc.dim("c")} Create resource`,
1188
+ ` ${pc.dim("e")} Edit object/job`,
1189
+ ` ${pc.dim("d")} Delete object/link/job`,
1190
+ ` ${pc.dim("r")} Refresh data`,
1191
+ ` ${pc.dim("i")} Connection info`,
1192
+ "",
1193
+ ` ${pc.bold("Data Views")}`,
1194
+ ` ${pc.dim("n")} Object neighbors (links)`,
1195
+ ` ${pc.dim("a")} Aggregate (count/sum/avg/min/max)`,
1196
+ ` ${pc.dim("t")} Time-series query`,
1197
+ ` ${pc.dim("N")} Natural language query`,
1198
+ ` ${pc.dim("/f")} Search objects`,
1199
+ ` ${pc.dim("o")} Object listing options`,
1200
+ ` ${pc.dim("b")} Batch put/delete`,
1201
+ "",
1202
+ ` ${pc.bold("System")}`,
1203
+ ` ${pc.dim("s")} Switch driver`,
1204
+ ` ${pc.dim("m")} Maintenance`,
1205
+ ` ${pc.dim("l")} Logout`,
1206
+ ` ${pc.dim("q")} Quit`,
1207
+ ` ${pc.dim("?")} Toggle this help`,
1208
+ "",
1209
+ ` ${pc.dim("Press any key to close help.")}`,
1210
+ ];
1211
+ const helpW = 44;
1212
+ const helpH = lines.length + 2;
1213
+ const col = Math.max(0, Math.floor((W - helpW) / 2));
1214
+ const row = Math.max(0, Math.floor((H - helpH) / 2));
1215
+ let out = "";
1216
+ for (let r = 0; r < H; r++) {
1217
+ if (r >= row && r < row + helpH) {
1218
+ const lineIdx = r - row;
1219
+ if (lineIdx === 0 || lineIdx === helpH - 1) {
1220
+ out += `${" ".repeat(col) + pc.dim(`┌${"─".repeat(helpW - 2)}┐`)}\n`;
1221
+ }
1222
+ else {
1223
+ const text = lines[lineIdx - 1] ?? "";
1224
+ const padded = text + " ".repeat(Math.max(0, helpW - visibleWidth(text) - 2));
1225
+ out += `${" ".repeat(col) + pc.dim("│")} ${padded} ${pc.dim("│")}\n`;
1226
+ }
1227
+ }
1228
+ else {
1229
+ out += "\n";
1230
+ }
1231
+ }
1232
+ return out;
1233
+ }
1052
1234
  /** Pad/truncate `text` to exactly `width` visible characters. */
1053
1235
  function fitToWidth(text, width, highlight) {
1054
1236
  const vw = visibleWidth(text);
@@ -1661,6 +1843,316 @@ async function handleNeighbors(selected) {
1661
1843
  draw();
1662
1844
  });
1663
1845
  }
1846
+ async function handleAggregate(selected) {
1847
+ const defaultCol = selected?.type === "collection"
1848
+ ? (selected.ref?.name ?? "")
1849
+ : selected?.type === "object"
1850
+ ? (selected.ref?.collection ?? "")
1851
+ : "";
1852
+ openForm("Aggregate", [
1853
+ {
1854
+ id: "function",
1855
+ label: "Function",
1856
+ value: "count",
1857
+ options: ["count", "sum", "avg", "min", "max"],
1858
+ },
1859
+ {
1860
+ id: "collection",
1861
+ label: "Collection",
1862
+ value: defaultCol,
1863
+ options: collections,
1864
+ allowCustom: true,
1865
+ },
1866
+ { id: "field", label: "Field (for sum/avg/min/max)", placeholder: "field name" },
1867
+ { id: "groupBy", label: "Group By (optional field)", placeholder: "field name" },
1868
+ { id: "filter", label: "Filter (optional JSON)", placeholder: '{"status":"active"}' },
1869
+ ], async (vals) => {
1870
+ const func = vals.function || "count";
1871
+ const collection = (vals.collection || "").trim();
1872
+ if (!collection) {
1873
+ throw new Error("Collection is required.");
1874
+ }
1875
+ const field = (vals.field || "").trim() || undefined;
1876
+ const groupBy = (vals.groupBy || "").trim() || undefined;
1877
+ const filter = vals.filter?.trim() ? JSON.parse(vals.filter.trim()) : undefined;
1878
+ const options = { groupBy, filter };
1879
+ const a = db.aggregate;
1880
+ const result = func === "count"
1881
+ ? await a.count(collection, options)
1882
+ : func === "sum"
1883
+ ? await a.sum(collection, field, options)
1884
+ : func === "avg"
1885
+ ? await a.avg(collection, field, options)
1886
+ : func === "min"
1887
+ ? await a.min(collection, field, options)
1888
+ : await a.max(collection, field, options);
1889
+ const lines = [
1890
+ ` ${pc.bold("Aggregate")} ${pc.cyan(func)} ${pc.dim(`on ${collection}`)}`,
1891
+ "",
1892
+ ` ${pc.dim("Total:")} ${pc.bold(String(result.total))}`,
1893
+ "",
1894
+ ];
1895
+ if (result.groups && result.groups.length > 0) {
1896
+ lines.push(` ${pc.bold("Groups")}`);
1897
+ const maxVal = Math.max(...result.groups.map((g) => g.value));
1898
+ for (const g of result.groups) {
1899
+ const barLen = Math.max(1, Math.round((g.value / maxVal) * 20));
1900
+ const bar = pc.cyan("█".repeat(barLen));
1901
+ lines.push(` ${g.key}: ${bar} ${pc.dim(String(g.value))}`);
1902
+ }
1903
+ }
1904
+ viewerLines = lines;
1905
+ loadedItemId = "aggregate_result";
1906
+ draw();
1907
+ });
1908
+ }
1909
+ async function handleTimeseries(selected) {
1910
+ const defaultCol = selected?.type === "collection"
1911
+ ? (selected.ref?.name ?? "")
1912
+ : selected?.type === "object"
1913
+ ? (selected.ref?.collection ?? "")
1914
+ : "";
1915
+ openForm("Time Series", [
1916
+ {
1917
+ id: "function",
1918
+ label: "Function",
1919
+ value: "count",
1920
+ options: ["count", "sum", "avg", "min", "max"],
1921
+ },
1922
+ {
1923
+ id: "collection",
1924
+ label: "Collection",
1925
+ value: defaultCol,
1926
+ options: collections,
1927
+ allowCustom: true,
1928
+ },
1929
+ { id: "field", label: "Field (optional)", placeholder: "field name" },
1930
+ {
1931
+ id: "bucket",
1932
+ label: "Bucket",
1933
+ value: "day",
1934
+ options: ["hour", "day", "week", "month"],
1935
+ },
1936
+ { id: "from", label: "From (ISO date, optional)", placeholder: "2024-01-01" },
1937
+ { id: "to", label: "To (ISO date, optional)", placeholder: "2024-12-31" },
1938
+ { id: "filter", label: "Filter (optional JSON)", placeholder: '{"status":"active"}' },
1939
+ ], async (vals) => {
1940
+ const func = vals.function || "count";
1941
+ const collection = (vals.collection || "").trim();
1942
+ if (!collection) {
1943
+ throw new Error("Collection is required.");
1944
+ }
1945
+ const field = (vals.field || "").trim() || undefined;
1946
+ const bucket = (vals.bucket || "day");
1947
+ const from = (vals.from || "").trim() || undefined;
1948
+ const to = (vals.to || "").trim() || undefined;
1949
+ const filter = vals.filter?.trim() ? JSON.parse(vals.filter.trim()) : undefined;
1950
+ const result = await db.timeseries(collection, {
1951
+ function: func,
1952
+ field,
1953
+ bucket,
1954
+ from,
1955
+ to,
1956
+ filter,
1957
+ });
1958
+ const lines = [
1959
+ ` ${pc.bold("Time Series")} ${pc.cyan(func)} ${pc.dim(`on ${collection}, ${bucket}ly`)}`,
1960
+ "",
1961
+ ];
1962
+ if (result.buckets.length === 0) {
1963
+ lines.push(pc.dim("No data for the selected range."));
1964
+ }
1965
+ else {
1966
+ const maxVal = Math.max(...result.buckets.map((b) => b.value));
1967
+ for (const b of result.buckets) {
1968
+ const barLen = Math.max(1, Math.round((b.value / maxVal) * 20));
1969
+ const bar = pc.green("█".repeat(barLen));
1970
+ lines.push(` ${b.label}: ${bar} ${pc.dim(String(b.value))}`);
1971
+ }
1972
+ }
1973
+ viewerLines = lines;
1974
+ loadedItemId = "timeseries_result";
1975
+ draw();
1976
+ });
1977
+ }
1978
+ async function handleNlq(selected) {
1979
+ const defaultCol = selected?.type === "collection"
1980
+ ? (selected.ref?.name ?? "")
1981
+ : selected?.type === "object"
1982
+ ? (selected.ref?.collection ?? "")
1983
+ : "";
1984
+ openForm("Natural Language Query", [
1985
+ { id: "question", label: "Question", placeholder: "How many active users?" },
1986
+ {
1987
+ id: "collection",
1988
+ label: "Collection (optional)",
1989
+ value: defaultCol,
1990
+ options: collections,
1991
+ allowCustom: true,
1992
+ },
1993
+ ], async (vals) => {
1994
+ const question = (vals.question || "").trim();
1995
+ if (!question) {
1996
+ throw new Error("Question is required.");
1997
+ }
1998
+ const collection = (vals.collection || "").trim() || undefined;
1999
+ const result = await db.nlq.query(question, {
2000
+ collection,
2001
+ });
2002
+ const intent = result.intent;
2003
+ const lines = [
2004
+ ` ${pc.bold("NLQ Result")}`,
2005
+ ` ${pc.dim(`Question: ${question}`)}`,
2006
+ "",
2007
+ ` ${result.answer}`,
2008
+ "",
2009
+ ];
2010
+ if (intent) {
2011
+ lines.push(` ${pc.dim("Interpreted as:")} ${intent.action} ${pc.dim("on")} ${pc.cyan(intent.collection)}`);
2012
+ if (intent.function) {
2013
+ lines.push(` ${pc.dim("Function:")} ${intent.function}`);
2014
+ }
2015
+ if (intent.field) {
2016
+ lines.push(` ${pc.dim("Field:")} ${intent.field}`);
2017
+ }
2018
+ if (intent.query) {
2019
+ lines.push(` ${pc.dim("Query:")} ${intent.query}`);
2020
+ }
2021
+ }
2022
+ if (result.data !== undefined && result.data !== null) {
2023
+ lines.push("", highlightJson(result.data));
2024
+ }
2025
+ viewerLines = lines;
2026
+ loadedItemId = "nlq_result";
2027
+ draw();
2028
+ });
2029
+ }
2030
+ async function handleCollectionOptions(selected) {
2031
+ const colName = selected?.type === "collection"
2032
+ ? (selected.ref?.name ?? "")
2033
+ : selected?.type === "object"
2034
+ ? (selected.ref?.collection ?? "")
2035
+ : "";
2036
+ if (!colName) {
2037
+ viewerLines = [pc.yellow("Select a collection first, then press [o] to set listing options.")];
2038
+ loadedItemId = "options_info";
2039
+ draw();
2040
+ return;
2041
+ }
2042
+ const current = collectionOptions.get(colName) ?? {};
2043
+ openForm(`Listing Options: ${colName}`, [
2044
+ {
2045
+ id: "sortBy",
2046
+ label: "Sort By",
2047
+ value: current.sortBy ?? "",
2048
+ options: ["", "id", "created_at", "updated_at", "version"],
2049
+ allowCustom: true,
2050
+ },
2051
+ {
2052
+ id: "sortDir",
2053
+ label: "Sort Direction",
2054
+ value: current.sortDir ?? "asc",
2055
+ options: ["asc", "desc"],
2056
+ },
2057
+ { id: "limit", label: "Limit", value: String(current.limit ?? ""), placeholder: "50" },
2058
+ { id: "offset", label: "Offset", value: String(current.offset ?? ""), placeholder: "0" },
2059
+ {
2060
+ id: "filter",
2061
+ label: "Filter (JSON)",
2062
+ value: current.filter ? JSON.stringify(current.filter) : "",
2063
+ placeholder: '{"status":"active"}',
2064
+ },
2065
+ ], async (vals) => {
2066
+ const opts = {};
2067
+ if (vals.sortBy) {
2068
+ opts.sortBy = vals.sortBy;
2069
+ opts.sortDir = vals.sortDir || "asc";
2070
+ }
2071
+ if (vals.limit) {
2072
+ opts.limit = parseInt(vals.limit, 10) || 50;
2073
+ }
2074
+ if (vals.offset) {
2075
+ opts.offset = parseInt(vals.offset, 10) || 0;
2076
+ }
2077
+ if (vals.filter?.trim()) {
2078
+ opts.filter = JSON.parse(vals.filter.trim());
2079
+ }
2080
+ if (Object.keys(opts).length > 0) {
2081
+ collectionOptions.set(colName, opts);
2082
+ }
2083
+ else {
2084
+ collectionOptions.delete(colName);
2085
+ }
2086
+ await fetchResources();
2087
+ const tree = buildTree();
2088
+ const idx = tree.findIndex((n) => n.id === `col:${colName}`);
2089
+ if (idx !== -1) {
2090
+ cursorIndex = idx;
2091
+ }
2092
+ const n = tree[cursorIndex];
2093
+ if (n) {
2094
+ scheduleLoad(n);
2095
+ }
2096
+ });
2097
+ }
2098
+ async function handleBatchOps(selected) {
2099
+ const colName = selected?.type === "collection"
2100
+ ? (selected.ref?.name ?? "")
2101
+ : selected?.type === "object"
2102
+ ? (selected.ref?.collection ?? "")
2103
+ : "";
2104
+ if (!colName) {
2105
+ viewerLines = [pc.yellow("Select a collection first, then press [b] for batch operations.")];
2106
+ loadedItemId = "batch_info";
2107
+ draw();
2108
+ return;
2109
+ }
2110
+ openForm(`Batch Ops: ${colName}`, [
2111
+ {
2112
+ id: "action",
2113
+ label: "Action",
2114
+ value: "put",
2115
+ options: ["put", "delete"],
2116
+ },
2117
+ {
2118
+ id: "input",
2119
+ label: "JSON File Path or IDs (comma-sep for delete)",
2120
+ placeholder: "/path/to/file.json or id1,id2,id3",
2121
+ },
2122
+ ], async (vals) => {
2123
+ const action = vals.action || "";
2124
+ const input = (vals.input || "").trim();
2125
+ if (!input) {
2126
+ throw new Error("Input is required.");
2127
+ }
2128
+ if (action === "put") {
2129
+ const data = JSON.parse(await fs.promises.readFile(input, "utf-8"));
2130
+ const objects = Array.isArray(data) ? data : (data.objects ?? [data]);
2131
+ const result = await db.putBatch(colName, objects);
2132
+ viewerLines = [
2133
+ ` ${pc.bold("Batch Put Complete")}`,
2134
+ ` ${pc.dim(`Collection: ${colName}`)}`,
2135
+ ` ${pc.dim(`Objects: ${result.length}`)}`,
2136
+ ];
2137
+ loadedItemId = "batch_result";
2138
+ draw();
2139
+ }
2140
+ else if (action === "delete") {
2141
+ const ids = input
2142
+ .split(",")
2143
+ .map((s) => s.trim())
2144
+ .filter(Boolean);
2145
+ const count = await db.deleteBatch(colName, ids);
2146
+ viewerLines = [
2147
+ ` ${pc.bold("Batch Delete Complete")}`,
2148
+ ` ${pc.dim(`Collection: ${colName}`)}`,
2149
+ ` ${pc.dim(`Deleted: ${count} objects`)}`,
2150
+ ];
2151
+ loadedItemId = "batch_result";
2152
+ draw();
2153
+ }
2154
+ });
2155
+ }
1664
2156
  async function handleMaintenance() {
1665
2157
  // Cycle through maintenance operations with each press of 'm'
1666
2158
  const operations = ["health", "checkpoint", "backup"];
@@ -1745,6 +2237,12 @@ function setupKeypress() {
1745
2237
  if (!key) {
1746
2238
  return;
1747
2239
  }
2240
+ // Help overlay dismisses on any keypress
2241
+ if (showHelp) {
2242
+ showHelp = false;
2243
+ draw();
2244
+ return;
2245
+ }
1748
2246
  // Quit
1749
2247
  if ((key.ctrl && key.name === "c") || key.name === "q") {
1750
2248
  if (formState?.active && key.name !== "q") {
@@ -1861,13 +2359,17 @@ function setupKeypress() {
1861
2359
  }
1862
2360
  }
1863
2361
  else if (!connected) {
1864
- // Driver selection mode — only Enter works
2362
+ // Driver selection mode — only Enter and ? work
1865
2363
  if (key.name === "return") {
1866
2364
  const node = tree[cursorIndex];
1867
2365
  if (node) {
1868
2366
  await handleConnect(node);
1869
2367
  }
1870
2368
  }
2369
+ else if (str === "?") {
2370
+ showHelp = !showHelp;
2371
+ draw();
2372
+ }
1871
2373
  }
1872
2374
  else {
1873
2375
  // Connected mode — full set of shortcuts
@@ -1953,12 +2455,31 @@ function setupKeypress() {
1953
2455
  else if (str === "/" || str === "f" || str === "F") {
1954
2456
  await handleSearch();
1955
2457
  }
2458
+ else if (str === "?") {
2459
+ showHelp = !showHelp;
2460
+ draw();
2461
+ }
1956
2462
  else if (str === "i" || str === "I") {
1957
2463
  await handleInfo();
1958
2464
  }
1959
- else if (str === "n" || str === "N") {
2465
+ else if (str === "n") {
1960
2466
  await handleNeighbors(tree[cursorIndex]);
1961
2467
  }
2468
+ else if (str === "N") {
2469
+ await handleNlq(tree[cursorIndex]);
2470
+ }
2471
+ else if (str === "a" || str === "A") {
2472
+ await handleAggregate(tree[cursorIndex]);
2473
+ }
2474
+ else if (str === "t" || str === "T") {
2475
+ await handleTimeseries(tree[cursorIndex]);
2476
+ }
2477
+ else if (str === "o" || str === "O") {
2478
+ await handleCollectionOptions(tree[cursorIndex]);
2479
+ }
2480
+ else if (str === "b" || str === "B") {
2481
+ await handleBatchOps(tree[cursorIndex]);
2482
+ }
1962
2483
  else if (str === "m" || str === "M") {
1963
2484
  await handleMaintenance();
1964
2485
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thingd/cli",
3
- "version": "0.55.0",
3
+ "version": "0.57.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.57.0"
49
49
  },
50
50
  "engines": {
51
51
  "node": ">=24.0.0"