@thingd/cli 0.58.2 → 0.60.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":"cloud.d.ts","sourceRoot":"","sources":["../../src/commands/cloud.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,UAAU,EAA6B,MAAM,aAAa,CAAC;AAyIzE,wBAAsB,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA+BjE"}
1
+ {"version":3,"file":"cloud.d.ts","sourceRoot":"","sources":["../../src/commands/cloud.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,UAAU,EAA6B,MAAM,aAAa,CAAC;AAiHzE,wBAAsB,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAmCjE"}
@@ -1,9 +1,10 @@
1
1
  import { execSync } from "node:child_process";
2
+ import * as os from "node:os";
2
3
  import { createInterface } from "node:readline/promises";
3
4
  import { setTimeout as sleep } from "node:timers/promises";
4
5
  import pc from "picocolors";
5
6
  import { requiredToken, stringFlag } from "../index.js";
6
- import { addOrganizationMember, CloudApiError, createApiKey, createInstance, createOrganization, createProject, getMe, getOrganization, listInstances, listOrganizationMembers, listOrganizations, listProjects, pollCliAuth, removeOrganizationMember, resolveAllInstances, startCliAuth, } from "../lib/cloud-api.js";
7
+ import { addOrganizationMember, CloudApiError, createApiKey, createInstance, createOrganization, createProject, createUserToken, getMe, getOrganization, listInstances, listOrganizationMembers, listOrganizations, listProjects, listUserTokens, parseUserTokenId, pollCliAuth, removeOrganizationMember, resolveAllInstances, revokeUserToken, startCliAuth, updateUserToken, } from "../lib/cloud-api.js";
7
8
  import { readCloudConfig, removeCloudConfig, writeCloudConfig, } from "../lib/cloud-config.js";
8
9
  const POLL_INTERVAL_MS = 2_000;
9
10
  const POLL_TIMEOUT_MS = 5 * 60 * 1_000;
@@ -37,7 +38,7 @@ function openBrowser(url) {
37
38
  }
38
39
  }
39
40
  function makeBaseConfig(context) {
40
- return { token: "", url: cliApiUrl(context) };
41
+ return { url: cliApiUrl(context) };
41
42
  }
42
43
  async function askQuestion(rl, query) {
43
44
  return rl.question(query);
@@ -77,35 +78,6 @@ async function pickAndSaveInstance(context, cloudConfig) {
77
78
  rl.close();
78
79
  }
79
80
  }
80
- /**
81
- * Ensure a data-plane API key exists in the cloud config.
82
- * Best-effort — if creation fails, the JWT is still usable for management API.
83
- */
84
- async function ensureApiKey(context, cloudConfig) {
85
- if (cloudConfig.apiKey) {
86
- return;
87
- }
88
- try {
89
- let projectId = cloudConfig.projectId;
90
- if (!projectId) {
91
- const { projects } = await listProjects(cloudConfig);
92
- if (projects.length === 0) {
93
- return;
94
- }
95
- projectId = projects[0]?.id;
96
- }
97
- if (!projectId) {
98
- return;
99
- }
100
- const result = await createApiKey(cloudConfig, projectId);
101
- cloudConfig.apiKey = result.token;
102
- writeCloudConfig(cloudConfig);
103
- context.stderr.write(` ${pc.dim("API key created for data access\n")}`);
104
- }
105
- catch {
106
- // API key creation is best-effort
107
- }
108
- }
109
81
  export async function runCloud(context) {
110
82
  const sub = requiredToken(context.parsed, 1, "subcommand");
111
83
  switch (sub) {
@@ -118,6 +90,9 @@ export async function runCloud(context) {
118
90
  case "status":
119
91
  await runCloudStatus(context);
120
92
  return;
93
+ case "token":
94
+ await runToken(context);
95
+ return;
121
96
  case "project":
122
97
  await runProject(context);
123
98
  return;
@@ -125,6 +100,7 @@ export async function runCloud(context) {
125
100
  await runInstance(context);
126
101
  return;
127
102
  case "api-key":
103
+ context.stderr.write(pc.yellow("Deprecated. Use `thingd cloud token create` for CLI tokens. Project API keys are managed in the dashboard.\n"));
128
104
  await runApiKey(context);
129
105
  return;
130
106
  case "org":
@@ -132,7 +108,7 @@ export async function runCloud(context) {
132
108
  return;
133
109
  default:
134
110
  context.stderr.write(`Unknown cloud subcommand: ${sub}\n` +
135
- "Available: login, logout, status, org, project, instance, api-key\n");
111
+ "Available: login, logout, status, token, org, project, instance, api-key\n");
136
112
  }
137
113
  }
138
114
  async function runLogin(context) {
@@ -140,17 +116,19 @@ async function runLogin(context) {
140
116
  const token = context.parsed.tokens[3] ?? stringFlag(context.parsed, "token");
141
117
  // ── Manual --code --token flow (fallback) ─────────────────────────
142
118
  if (code && token) {
143
- const config = { token, url: cliApiUrl(context) };
119
+ const config = { url: cliApiUrl(context) };
144
120
  try {
145
- const { user } = await getMe(config);
146
- const cloudConfig = { ...config, email: user.email };
147
- // Save config immediately so login always persists
121
+ // Verify JWT and get user info
122
+ const { user } = await getMe({ ...config, token });
123
+ // Create a permanent user token
124
+ const hostname = os.hostname();
125
+ const { token: userToken } = await createUserToken({ ...config, token }, `cli-${hostname}`);
126
+ // Save config with user token (not JWT)
127
+ const cloudConfig = { userToken, email: user.email, ...config };
148
128
  writeCloudConfig(cloudConfig);
149
129
  context.stdout.write(pc.green(`✓ Logged in as ${user.email}\n`));
150
130
  // Discover and select an instance (interactive if multiple)
151
131
  await pickAndSaveInstance(context, cloudConfig);
152
- // Create a data-plane API key for REST/MCP operations
153
- await ensureApiKey(context, cloudConfig);
154
132
  }
155
133
  catch (err) {
156
134
  if (err instanceof CloudApiError && err.status === 401) {
@@ -187,17 +165,20 @@ async function runLogin(context) {
187
165
  try {
188
166
  const result = await pollCliAuth(baseConfig, deviceCode);
189
167
  if ("token" in result) {
190
- const tokenConfig = { token: result.token, url: cliApiUrl(context) };
168
+ const jwt = result.token;
169
+ const tokenConfig = { url: cliApiUrl(context) };
191
170
  try {
192
- const { user } = await getMe(tokenConfig);
193
- const cloudConfig = { ...tokenConfig, email: user.email };
194
- // Save config immediately so login always persists
171
+ // Verify temporary JWT
172
+ const { user } = await getMe({ ...tokenConfig, token: jwt });
173
+ // Create permanent user token
174
+ const hostname = os.hostname();
175
+ const { token: userToken } = await createUserToken({ ...tokenConfig, token: jwt }, `cli-${hostname}`);
176
+ // Save config with user token (not JWT)
177
+ const cloudConfig = { userToken, email: user.email, ...tokenConfig };
195
178
  writeCloudConfig(cloudConfig);
196
179
  context.stdout.write(pc.green(`\r✓ Logged in as ${user.email}\n`));
197
180
  // Discover and select an instance (interactive if multiple)
198
181
  await pickAndSaveInstance(context, cloudConfig);
199
- // Create a data-plane API key for REST/MCP operations
200
- await ensureApiKey(context, cloudConfig);
201
182
  return;
202
183
  }
203
184
  catch {
@@ -219,6 +200,18 @@ async function runLogin(context) {
219
200
  context.stderr.write(pc.red("\nTimed out. Run `thingd cloud login` again.\n"));
220
201
  }
221
202
  async function runLogout(context) {
203
+ const config = readCloudConfig();
204
+ if (config?.userToken) {
205
+ try {
206
+ const tokenId = parseUserTokenId(config.userToken);
207
+ if (tokenId) {
208
+ await revokeUserToken(config, tokenId);
209
+ }
210
+ }
211
+ catch {
212
+ // Best-effort — token may already be revoked or API unreachable
213
+ }
214
+ }
222
215
  removeCloudConfig();
223
216
  context.stdout.write(pc.green("✓ Logged out\n"));
224
217
  }
@@ -228,10 +221,31 @@ async function runCloudStatus(context) {
228
221
  context.stdout.write(`Not logged in. Run ${pc.cyan("thingd cloud login")}\n`);
229
222
  return;
230
223
  }
224
+ // Warn about old config format
225
+ if (config.token && !config.userToken) {
226
+ context.stdout.write(`${pc.yellow("Your credentials use an older format.")} Run ${pc.cyan("thingd cloud login")} to upgrade to a persistent CLI token.\n\n`);
227
+ }
231
228
  try {
232
229
  const { user } = await getMe(config);
233
- context.stdout.write(`Logged in as ${pc.green(user.email)} (${user.role})\n` +
234
- `API: ${config.url ?? "https://api.thingd.cloud"}\n`);
230
+ context.stdout.write(`Logged in as ${pc.green(user.email)} (${user.role})\n`);
231
+ // Show token info
232
+ if (config.userToken) {
233
+ try {
234
+ const { userTokens } = await listUserTokens(config);
235
+ const active = userTokens.find((t) => !t.revokedAt);
236
+ if (active) {
237
+ context.stdout.write(`CLI Token: ${pc.cyan(active.name)}\n` +
238
+ ` Prefix: ${pc.dim(active.prefix)}\n` +
239
+ ` Created: ${pc.dim(formatTimeAgo(active.createdAt))}\n` +
240
+ ` Last used:${pc.dim(active.lastUsedAt ? formatTimeAgo(active.lastUsedAt) : "never")}\n` +
241
+ ` Access: ${pc.dim(active.projectAccess === "all" ? "All projects" : active.projectAccess)}\n`);
242
+ }
243
+ }
244
+ catch {
245
+ context.stdout.write(` ${pc.dim("(token info unavailable)")}\n`);
246
+ }
247
+ }
248
+ context.stdout.write(`API: ${config.url ?? "https://api.thingd.cloud"}\n`);
235
249
  if (config.projectSlug && config.instanceSlug && config.instanceUrl) {
236
250
  context.stdout.write(`Instance: ${pc.cyan(config.projectSlug)}/${pc.cyan(config.instanceSlug)}\n` +
237
251
  ` ${pc.dim(config.instanceUrl)}\n`);
@@ -256,6 +270,154 @@ async function runCloudStatus(context) {
256
270
  context.stdout.write(`Token expired. Run ${pc.cyan("thingd cloud login")}\n`);
257
271
  }
258
272
  }
273
+ function formatTimeAgo(iso) {
274
+ const ms = Date.now() - new Date(iso).getTime();
275
+ const s = Math.floor(ms / 1000);
276
+ if (s < 5)
277
+ return "just now";
278
+ if (s < 60)
279
+ return `${s}s ago`;
280
+ const m = Math.floor(s / 60);
281
+ if (m < 60)
282
+ return `${m}m ago`;
283
+ const h = Math.floor(m / 60);
284
+ if (h < 24)
285
+ return `${h}h ago`;
286
+ const d = Math.floor(h / 24);
287
+ return `${d}d ago`;
288
+ }
289
+ // ── Token subcommands ────────────────────────────────────────────────
290
+ function requireLoggedInConfig(context) {
291
+ const config = readCloudConfig();
292
+ if (!config?.userToken && !config?.token) {
293
+ context.stderr.write(pc.yellow("Not logged in. Run thingd cloud login first.\n"));
294
+ throw new Error("not_logged_in");
295
+ }
296
+ return config;
297
+ }
298
+ async function runToken(context) {
299
+ const sub = context.parsed.tokens[2];
300
+ if (!sub) {
301
+ context.stderr.write("Usage: thingd cloud token <list|create|revoke|restrict|unrestricted>\n");
302
+ return;
303
+ }
304
+ switch (sub) {
305
+ case "list":
306
+ await runTokenList(context);
307
+ return;
308
+ case "create":
309
+ await runTokenCreate(context);
310
+ return;
311
+ case "revoke":
312
+ await runTokenRevoke(context);
313
+ return;
314
+ case "restrict":
315
+ await runTokenRestrict(context);
316
+ return;
317
+ case "unrestricted":
318
+ await runTokenUnrestricted(context);
319
+ return;
320
+ default:
321
+ context.stderr.write(`Unknown token action: ${sub}. Available: list, create, revoke, restrict, unrestricted\n`);
322
+ }
323
+ }
324
+ async function runTokenList(context) {
325
+ const config = requireLoggedInConfig(context);
326
+ try {
327
+ const { userTokens } = await listUserTokens(config);
328
+ if (userTokens.length === 0) {
329
+ context.stdout.write("No CLI tokens found. Create one with `thingd cloud token create <name>`\n");
330
+ return;
331
+ }
332
+ // Header
333
+ const header = `${pc.bold("Name".padEnd(18))} ${pc.bold("Prefix".padEnd(22))} ${pc.bold("Created".padEnd(14))} ${pc.bold("Last Used".padEnd(14))} ${pc.bold("Access")}`;
334
+ context.stdout.write(`${header}\n`);
335
+ context.stdout.write(`${pc.dim("─".repeat(header.length))}\n`);
336
+ for (const t of userTokens) {
337
+ if (t.revokedAt)
338
+ continue; // Skip revoked tokens
339
+ const access = t.projectAccess === "all" ? "All" : t.projectAccess;
340
+ context.stdout.write(`${t.name.padEnd(18)} ${pc.dim(t.prefix.padEnd(22))} ${formatTimeAgo(t.createdAt).padEnd(14)} ${(t.lastUsedAt ? formatTimeAgo(t.lastUsedAt) : "never").padEnd(14)} ${pc.cyan(access)}\n`);
341
+ }
342
+ }
343
+ catch (err) {
344
+ context.stderr.write(pc.red(`Failed to list tokens: ${err}\n`));
345
+ }
346
+ }
347
+ async function runTokenCreate(context) {
348
+ const config = requireLoggedInConfig(context);
349
+ const name = context.parsed.tokens[3] ?? "cli-token";
350
+ try {
351
+ const { token: userToken } = await createUserToken(config, name);
352
+ context.stdout.write(`\n${pc.green("✓ Token created")}\n\n` +
353
+ `${pc.bold(userToken)}\n\n` +
354
+ `${pc.yellow("⚠ This token will only be shown once. Copy it now.\n")}` +
355
+ `${pc.dim("Press Enter to continue...")}\n`);
356
+ // Wait for user to acknowledge
357
+ const rl = createInterface({
358
+ input: context.stdin,
359
+ output: context.stderr,
360
+ });
361
+ await rl.question("");
362
+ rl.close();
363
+ }
364
+ catch (err) {
365
+ context.stderr.write(pc.red(`Failed to create token: ${err}\n`));
366
+ }
367
+ }
368
+ async function runTokenRevoke(context) {
369
+ const config = requireLoggedInConfig(context);
370
+ const tokenId = requiredToken(context.parsed, 3, "token-id");
371
+ try {
372
+ await revokeUserToken(config, tokenId);
373
+ context.stdout.write(pc.green(`✓ Token ${tokenId} revoked\n`));
374
+ }
375
+ catch (err) {
376
+ if (err instanceof CloudApiError && err.status === 404) {
377
+ context.stderr.write(pc.red("Token not found.\n"));
378
+ }
379
+ else {
380
+ context.stderr.write(pc.red(`Failed to revoke token: ${err}\n`));
381
+ }
382
+ }
383
+ }
384
+ async function runTokenRestrict(context) {
385
+ const config = requireLoggedInConfig(context);
386
+ const tokenId = requiredToken(context.parsed, 3, "token-id");
387
+ const projectSlug = requiredToken(context.parsed, 4, "project");
388
+ try {
389
+ const { userToken } = await updateUserToken(config, tokenId, {
390
+ projectAccess: projectSlug,
391
+ });
392
+ context.stdout.write(pc.green(`✓ Token "${userToken.name}" restricted to: ${projectSlug}\n`));
393
+ }
394
+ catch (err) {
395
+ if (err instanceof CloudApiError && err.status === 404) {
396
+ context.stderr.write(pc.red("Token not found.\n"));
397
+ }
398
+ else {
399
+ context.stderr.write(pc.red(`Failed to update token: ${err}\n`));
400
+ }
401
+ }
402
+ }
403
+ async function runTokenUnrestricted(context) {
404
+ const config = requireLoggedInConfig(context);
405
+ const tokenId = requiredToken(context.parsed, 3, "token-id");
406
+ try {
407
+ const { userToken } = await updateUserToken(config, tokenId, {
408
+ projectAccess: "all",
409
+ });
410
+ context.stdout.write(pc.green(`✓ Token "${userToken.name}" now has access to all projects\n`));
411
+ }
412
+ catch (err) {
413
+ if (err instanceof CloudApiError && err.status === 404) {
414
+ context.stderr.write(pc.red("Token not found.\n"));
415
+ }
416
+ else {
417
+ context.stderr.write(pc.red(`Failed to update token: ${err}\n`));
418
+ }
419
+ }
420
+ }
259
421
  // ── Org helpers ──────────────────────────────────────────────────────
260
422
  async function resolveOrg(config, slugOrId) {
261
423
  // Try direct ID lookup first, then assume it's a slug and list
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAUL,MAAM,EACN,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AASrB,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEjD,KAAK,YAAY,GAAG;IAClB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF,KAAK,YAAY,GAAG,MAAM,CAAC,cAAc,CAAC;AAE1C,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC7B,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,UAAU,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAsCF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;IACf,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAwFF,wBAAsB,MAAM,CAC1B,IAAI,WAAwB,EAC5B,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,MAAM,CAAC,CA8EjB;AA08BD,wBAAsB,MAAM,CAC1B,OAAO,EAAE,UAAU,EACnB,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GACtC,OAAO,CAAC,IAAI,CAAC,CAef;AA4BD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,UAAU,GAAG,iBAAiB,CAqDxE;AAiDD,wBAAgB,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjE;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE/E;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAGlF;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMrE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEnF;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMrF;AAyFD,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAEpF;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAElE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAUL,MAAM,EACN,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AASrB,KAAK,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAEjD,KAAK,YAAY,GAAG;IAClB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF,KAAK,YAAY,GAAG,MAAM,CAAC,cAAc,CAAC;AAE1C,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC7B,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,UAAU,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;IACrB,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;CACjB,CAAC;AAsCF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;IACf,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAwFF,wBAAsB,MAAM,CAC1B,IAAI,WAAwB,EAC5B,OAAO,GAAE,aAAkB,GAC1B,OAAO,CAAC,MAAM,CAAC,CA8EjB;AA08BD,wBAAsB,MAAM,CAC1B,OAAO,EAAE,UAAU,EACnB,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GACtC,OAAO,CAAC,IAAI,CAAC,CAef;AA4BD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,UAAU,GAAG,iBAAiB,CAsDxE;AAiDD,wBAAgB,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjE;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE/E;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAGlF;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMrE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEnF;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMrF;AAyFD,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAEpF;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAElE"}
package/dist/index.js CHANGED
@@ -1002,6 +1002,7 @@ export function resolveConnection(context) {
1002
1002
  }
1003
1003
  const resolvedAuthToken = stringFlag(context.parsed, "auth-token") ??
1004
1004
  context.env.THINGD_AUTH_TOKEN ??
1005
+ cloudCfg?.userToken ??
1005
1006
  cloudCfg?.apiKey ??
1006
1007
  cloudCfg?.token;
1007
1008
  // Prefer instanceUrl (resolved MCP endpoint) over raw cloudCfg.url (API base)
@@ -1 +1 @@
1
- {"version":3,"file":"interactive.d.ts","sourceRoot":"","sources":["../src/interactive.ts"],"names":[],"mappings":"AAitGA,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CA0FvD"}
1
+ {"version":3,"file":"interactive.d.ts","sourceRoot":"","sources":["../src/interactive.ts"],"names":[],"mappings":"AAqxGA,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 collectionCounts = new Map();
72
73
  const collectionOptions = new Map();
73
74
  const expandedSet = new Set(["cat:collections", "cat:streams", "cat:queues"]);
74
75
  let cursorIndex = 0;
@@ -217,6 +218,35 @@ function formatUptime(ms) {
217
218
  const h = Math.floor(m / 60);
218
219
  return `${h}h ${m % 60}m`;
219
220
  }
221
+ function formatRelativeTime(iso) {
222
+ const ms = Date.now() - new Date(iso).getTime();
223
+ const s = Math.floor(ms / 1000);
224
+ if (s < 0) {
225
+ return "now";
226
+ }
227
+ if (s < 5) {
228
+ return "now";
229
+ }
230
+ if (s < 60) {
231
+ return `${s}s ago`;
232
+ }
233
+ const m = Math.floor(s / 60);
234
+ if (m < 60) {
235
+ return `${m}m ago`;
236
+ }
237
+ const h = Math.floor(m / 60);
238
+ if (h < 24) {
239
+ return `${h}h ago`;
240
+ }
241
+ const d = Math.floor(h / 24);
242
+ return `${d}d ago`;
243
+ }
244
+ function formatCount(n) {
245
+ if (n >= 1000) {
246
+ return `${(n / 1000).toFixed(1).replace(/\.0$/, "")}k`;
247
+ }
248
+ return String(n);
249
+ }
220
250
  async function fetchResourcesFallback() {
221
251
  cloudError = null;
222
252
  // Collections and streams — parallel fetch
@@ -276,7 +306,7 @@ async function fetchResourcesFallback() {
276
306
  listOpts.filter = opts.filter;
277
307
  }
278
308
  const list = await db.listObjects(col, Object.keys(listOpts).length > 0 ? listOpts : undefined);
279
- objectsByCollection.set(col, list.map((o) => o.id));
309
+ objectsByCollection.set(col, list.map((o) => ({ id: o.id, createdAt: o.createdAt })));
280
310
  }
281
311
  catch {
282
312
  objectsByCollection.set(col, []);
@@ -335,7 +365,7 @@ async function fetchResources() {
335
365
  listOpts.filter = opts.filter;
336
366
  }
337
367
  const list = await db.listObjects(col, Object.keys(listOpts).length > 0 ? listOpts : undefined);
338
- objectsByCollection.set(col, list.map((o) => o.id));
368
+ objectsByCollection.set(col, list.map((o) => ({ id: o.id, createdAt: o.createdAt })));
339
369
  }
340
370
  catch {
341
371
  objectsByCollection.set(col, []);
@@ -364,6 +394,20 @@ async function fetchResources() {
364
394
  jobsByQueue.set(q, { active: [], dead: [] });
365
395
  }
366
396
  }));
397
+ // Fetch per-collection counts from schema
398
+ collectionCounts.clear();
399
+ try {
400
+ const schemas = await db.schema();
401
+ for (const s of schemas) {
402
+ collectionCounts.set(s.name, s.objectCount);
403
+ }
404
+ }
405
+ catch {
406
+ // Fallback — count from objectsByCollection
407
+ for (const col of collections) {
408
+ collectionCounts.set(col, objectsByCollection.get(col)?.length ?? 0);
409
+ }
410
+ }
367
411
  // Calculate Deltas for Operations Throughput Rates
368
412
  const prevObjects = objectsHistory.length > 0
369
413
  ? (objectsHistory[objectsHistory.length - 1] ?? totalObjects)
@@ -525,11 +569,13 @@ function buildTree() {
525
569
  for (const col of collections) {
526
570
  const colId = `col:${col}`;
527
571
  const colOpen = expandedSet.has(colId);
572
+ const colCount = collectionCounts.get(col);
573
+ const colSuffix = colCount !== undefined ? pc.dim(` ${formatCount(colCount)}`) : "";
528
574
  nodes.push({
529
575
  id: colId,
530
576
  parentId: "cat:collections",
531
577
  type: "collection",
532
- label: `${colOpen ? pc.cyan("▾") : pc.dim("▸")} ${pc.cyan(col)}`,
578
+ label: `${colOpen ? pc.cyan("▾") : pc.dim("▸")} ${pc.cyan(col)}${colSuffix}`,
533
579
  depth: 1,
534
580
  expandable: true,
535
581
  ref: { name: col },
@@ -546,15 +592,16 @@ function buildTree() {
546
592
  expandable: false,
547
593
  });
548
594
  }
549
- for (const objId of objs) {
595
+ for (const objData of objs) {
596
+ const objAge = objData.createdAt ? pc.dim(formatRelativeTime(objData.createdAt)) : "";
550
597
  nodes.push({
551
- id: `obj:${col}:${objId}`,
598
+ id: `obj:${col}:${objData.id}`,
552
599
  parentId: colId,
553
600
  type: "object",
554
- label: `${pc.cyan("○")} ${objId}`,
601
+ label: `${pc.cyan("○")} ${objData.id} ${objAge}`,
555
602
  depth: 2,
556
603
  expandable: false,
557
- ref: { collection: col, id: objId },
604
+ ref: { collection: col, id: objData.id },
558
605
  });
559
606
  }
560
607
  }
@@ -581,11 +628,13 @@ function buildTree() {
581
628
  }
582
629
  for (const stream of streams) {
583
630
  const sOpen = expandedSet.has(`stream:${stream}`);
631
+ const evtCount = eventsByStream.get(stream)?.length ?? 0;
632
+ const evtSuffix = evtCount > 0 ? pc.dim(` ${formatCount(evtCount)}`) : "";
584
633
  nodes.push({
585
634
  id: `stream:${stream}`,
586
635
  parentId: "cat:streams",
587
636
  type: "stream",
588
- label: `${sOpen ? pc.cyan("▾") : pc.dim("▸")} ${pc.green(stream)}`,
637
+ label: `${sOpen ? pc.cyan("▾") : pc.dim("▸")} ${pc.green(stream)}${evtSuffix}`,
589
638
  depth: 1,
590
639
  expandable: true,
591
640
  ref: { name: stream },
@@ -607,7 +656,7 @@ function buildTree() {
607
656
  id: `evt:${stream}:${evt.id}`,
608
657
  parentId: `stream:${stream}`,
609
658
  type: "event",
610
- label: `${pc.dim("·")} ${pc.dim(evt.type || "unknown")}`,
659
+ label: `${pc.dim("·")} ${pc.dim(evt.type || "unknown")} ${pc.dim(formatRelativeTime(evt.createdAt))}`,
611
660
  depth: 2,
612
661
  expandable: false,
613
662
  ref: { stream: stream, eventId: evt.id, eventData: evt },
@@ -637,11 +686,17 @@ function buildTree() {
637
686
  }
638
687
  for (const q of queues) {
639
688
  const qOpen = expandedSet.has(`queue:${q}`);
689
+ const qJobData = jobsByQueue.get(q);
690
+ const qActive = qJobData?.active.length ?? 0;
691
+ const qDead = qJobData?.dead.length ?? 0;
692
+ const qSuffix = qActive > 0 || qDead > 0
693
+ ? ` ${qActive > 0 ? pc.cyan(String(qActive)) : pc.dim("0")}${pc.cyan(" ●")} ${qDead > 0 ? pc.red(String(qDead)) : pc.dim("0")}${pc.red(" ○")}`
694
+ : "";
640
695
  nodes.push({
641
696
  id: `queue:${q}`,
642
697
  parentId: "cat:queues",
643
698
  type: "queue",
644
- label: `${qOpen ? pc.cyan("▾") : pc.dim("▸")} ${pc.magenta(q)}`,
699
+ label: `${qOpen ? pc.cyan("▾") : pc.dim("▸")} ${pc.magenta(q)}${qSuffix}`,
645
700
  depth: 1,
646
701
  expandable: true,
647
702
  ref: { name: q },
@@ -672,11 +727,13 @@ function buildTree() {
672
727
  });
673
728
  }
674
729
  for (const job of activeJobs) {
730
+ const jobStatus = pc.cyan(job.status ?? "ready");
731
+ const jobAttempts = pc.dim(`${job.attempts}/${job.maxAttempts}`);
675
732
  nodes.push({
676
733
  id: `job:${q}:active:${job.id}`,
677
734
  parentId: `queue:${q}:active`,
678
735
  type: "job",
679
- label: `${pc.cyan("●")} ${pc.dim(job.id.slice(0, 12))}`,
736
+ label: `${pc.cyan("●")} ${pc.dim(job.id.slice(0, 12))} ${jobAttempts} ${jobStatus}`,
680
737
  depth: 3,
681
738
  expandable: false,
682
739
  ref: { queue: q, jobId: job.id, status: "active", jobData: job },
@@ -705,11 +762,13 @@ function buildTree() {
705
762
  });
706
763
  }
707
764
  for (const job of deadJobs) {
765
+ const jobAttempts = pc.dim(`${job.attempts}/${job.maxAttempts}`);
766
+ const lastErr = job.lastError ? pc.dim(job.lastError.slice(0, 20)) : "";
708
767
  nodes.push({
709
768
  id: `job:${q}:dead:${job.id}`,
710
769
  parentId: `queue:${q}:dead`,
711
770
  type: "job",
712
- label: `${pc.red("○")} ${pc.dim(job.id.slice(0, 12))}`,
771
+ label: `${pc.red("○")} ${pc.dim(job.id.slice(0, 12))} ${jobAttempts}${lastErr ? ` ${pc.red("⚠")} ${lastErr}` : ""}`,
713
772
  depth: 3,
714
773
  expandable: false,
715
774
  ref: { queue: q, jobId: job.id, status: "dead", jobData: job },
@@ -765,7 +824,8 @@ function scheduleLoad(node) {
765
824
  : d === "native"
766
825
  ? ` Persistent SQLite database.\n Data is stored on disk.\n\n ${pc.dim("Best for: local development, single-node")}\n\n ${pc.dim("Press")} ${pc.bold("Enter")} ${pc.dim("to connect.")}`
767
826
  : (() => {
768
- const hasCfg = !!readCloudConfig()?.token;
827
+ const cfg = readCloudConfig();
828
+ const hasCfg = !!(cfg?.userToken ?? cfg?.token);
769
829
  return hasCfg
770
830
  ? ` Connect to a remote thingd instance.\n Requires a URL and optional auth token.\n\n ${pc.dim("Best for: production, multi-node")}\n\n ${pc.dim("Press")} ${pc.bold("Enter")} ${pc.dim("to connect.")}`
771
831
  : ` ${pc.yellow("Not logged in to thingd Cloud.")}\n\n Run ${pc.cyan("thingd cloud login")} to authenticate, or\n press ${pc.bold("Enter")} to connect with a URL and token manually.`;
@@ -828,7 +888,7 @@ async function loadContent(node) {
828
888
  res += pc.dim("No objects in this collection.");
829
889
  }
830
890
  else {
831
- const lines = objs.map((id) => ` ${pc.cyan("○")} ${id}`);
891
+ const lines = objs.map((o) => ` ${pc.cyan("○")} ${o.id}${o.createdAt ? ` ${pc.dim(formatRelativeTime(o.createdAt))}` : ""}`);
832
892
  res += lines.join("\n");
833
893
  }
834
894
  content = res;
@@ -2672,14 +2732,14 @@ async function handleConnect(node) {
2672
2732
  cloudCfg.projectSlug = selected.projectSlug;
2673
2733
  cloudCfg.instanceSlug = selected.instanceSlug;
2674
2734
  writeCloudConfig(cloudCfg);
2675
- await connectToDriver("cloud", deriveRestUrl(selected.mcpUrl), deriveRestUrl(selected.mcpUrl), cloudCfg.apiKey ?? cloudCfg.token, selected.instanceSlug);
2735
+ await connectToDriver("cloud", deriveRestUrl(selected.mcpUrl), deriveRestUrl(selected.mcpUrl), cloudCfg.userToken ?? cloudCfg.apiKey ?? cloudCfg.token, selected.instanceSlug);
2676
2736
  });
2677
2737
  return;
2678
2738
  }
2679
2739
  // No instances found — try saved URL or show error
2680
2740
  if (cloudCfg.instanceUrl) {
2681
2741
  const restUrl = deriveRestUrl(cloudCfg.instanceUrl);
2682
- await connectToDriver("cloud", restUrl, restUrl, cloudCfg.apiKey ?? cloudCfg.token, cloudCfg.instanceSlug);
2742
+ await connectToDriver("cloud", restUrl, restUrl, cloudCfg.userToken ?? cloudCfg.apiKey ?? cloudCfg.token, cloudCfg.instanceSlug);
2683
2743
  return;
2684
2744
  }
2685
2745
  viewerLines = [
@@ -2692,7 +2752,7 @@ async function handleConnect(node) {
2692
2752
  catch {
2693
2753
  if (cloudCfg.instanceUrl) {
2694
2754
  const restUrl = deriveRestUrl(cloudCfg.instanceUrl);
2695
- await connectToDriver("cloud", restUrl, restUrl, cloudCfg.apiKey ?? cloudCfg.token, cloudCfg.instanceSlug);
2755
+ await connectToDriver("cloud", restUrl, restUrl, cloudCfg.userToken ?? cloudCfg.apiKey ?? cloudCfg.token, cloudCfg.instanceSlug);
2696
2756
  return;
2697
2757
  }
2698
2758
  viewerLines = [
@@ -2843,6 +2903,7 @@ async function handleSwitch() {
2843
2903
  streams = [];
2844
2904
  queues = [];
2845
2905
  objectsByCollection = new Map();
2906
+ collectionCounts.clear();
2846
2907
  cursorIndex = 0;
2847
2908
  scrollOffset = 0;
2848
2909
  loadedItemId = "";
@@ -2876,6 +2937,7 @@ async function handleLogout() {
2876
2937
  streams = [];
2877
2938
  queues = [];
2878
2939
  objectsByCollection = new Map();
2940
+ collectionCounts.clear();
2879
2941
  cursorIndex = 0;
2880
2942
  scrollOffset = 0;
2881
2943
  loadedItemId = "";
@@ -2906,12 +2968,12 @@ export async function runInteractiveCli() {
2906
2968
  }
2907
2969
  // Auto-connect to cloud if credentials exist
2908
2970
  const cloudCfg = readCloudConfig();
2909
- if (cloudCfg?.token) {
2971
+ if (cloudCfg?.userToken ?? cloudCfg?.token) {
2910
2972
  const cloudUrl = resolveCloudUrl(cloudCfg);
2911
2973
  if (cloudUrl) {
2912
2974
  try {
2913
2975
  const restUrl = deriveRestUrl(cloudUrl);
2914
- await connectToDriver("cloud", restUrl, restUrl, cloudCfg.apiKey ?? cloudCfg.token, cloudCfg.instanceSlug);
2976
+ await connectToDriver("cloud", restUrl, restUrl, cloudCfg.userToken ?? cloudCfg.apiKey ?? cloudCfg.token, cloudCfg.instanceSlug);
2915
2977
  }
2916
2978
  catch (err) {
2917
2979
  const msg = err instanceof Error ? err.message : String(err);
@@ -19,6 +19,15 @@ export type CloudApiKey = {
19
19
  token?: string;
20
20
  createdAt: string;
21
21
  };
22
+ export type UserTokenDto = {
23
+ id: string;
24
+ name: string;
25
+ prefix: string;
26
+ projectAccess: string;
27
+ createdAt: string;
28
+ lastUsedAt: string | null;
29
+ revokedAt: string | null;
30
+ };
22
31
  export type CloudOrganization = {
23
32
  id: string;
24
33
  name: string;
@@ -105,6 +114,24 @@ export declare function deriveRestUrl(mcpUrl: string): string;
105
114
  * Returns null if no projects or instances exist.
106
115
  */
107
116
  export declare function resolveFirstInstance(config: CloudConfig): Promise<ResolvedInstance | null>;
117
+ export declare function createUserToken(config: CloudConfig, name: string, projectAccess?: string): Promise<{
118
+ token: string;
119
+ userToken: UserTokenDto;
120
+ }>;
121
+ export declare function listUserTokens(config: CloudConfig): Promise<{
122
+ userTokens: UserTokenDto[];
123
+ }>;
124
+ export declare function revokeUserToken(config: CloudConfig, tokenId: string): Promise<void>;
125
+ export declare function updateUserToken(config: CloudConfig, tokenId: string, updates: {
126
+ name?: string;
127
+ projectAccess?: string;
128
+ }): Promise<{
129
+ userToken: UserTokenDto;
130
+ }>;
131
+ /**
132
+ * Extract the token ID from a full user token string (md_user_<hexId>_<secret> → utk_<hexId>).
133
+ */
134
+ export declare function parseUserTokenId(userToken: string): string | null;
108
135
  /**
109
136
  * Fetch all cloud instances across all projects for the logged-in user.
110
137
  * Returns an empty array if no projects or instances exist.
@@ -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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"}
@@ -7,10 +7,14 @@ export class CloudApiError extends Error {
7
7
  this.name = "CloudApiError";
8
8
  }
9
9
  }
10
+ function resolveAuthToken(config) {
11
+ return config.userToken ?? config.token ?? config.apiKey ?? "";
12
+ }
10
13
  async function request(config, path, opts = {}) {
11
14
  const url = `${config.url ?? DEFAULT_API_URL}${path}`;
15
+ const authToken = resolveAuthToken(config);
12
16
  const headers = {
13
- authorization: `Bearer ${config.token}`,
17
+ authorization: `Bearer ${authToken}`,
14
18
  "content-type": "application/json",
15
19
  };
16
20
  const res = await fetch(url, {
@@ -137,6 +141,35 @@ export async function resolveFirstInstance(config) {
137
141
  }
138
142
  return null;
139
143
  }
144
+ // ── User Token API ──────────────────────────────────────────────────
145
+ export async function createUserToken(config, name, projectAccess) {
146
+ return request(config, "/auth/user-tokens", {
147
+ method: "POST",
148
+ body: { name, projectAccess },
149
+ });
150
+ }
151
+ export async function listUserTokens(config) {
152
+ return request(config, "/auth/user-tokens");
153
+ }
154
+ export async function revokeUserToken(config, tokenId) {
155
+ await request(config, `/auth/user-tokens/${tokenId}`, { method: "DELETE" });
156
+ }
157
+ export async function updateUserToken(config, tokenId, updates) {
158
+ return request(config, `/auth/user-tokens/${tokenId}`, {
159
+ method: "PATCH",
160
+ body: updates,
161
+ });
162
+ }
163
+ /**
164
+ * Extract the token ID from a full user token string (md_user_<hexId>_<secret> → utk_<hexId>).
165
+ */
166
+ export function parseUserTokenId(userToken) {
167
+ const match = /^md_user_([a-f0-9]{20})_/.exec(userToken);
168
+ if (!match?.[1]) {
169
+ return null;
170
+ }
171
+ return `utk_${match[1]}`;
172
+ }
140
173
  /**
141
174
  * Fetch all cloud instances across all projects for the logged-in user.
142
175
  * Returns an empty array if no projects or instances exist.
@@ -1,8 +1,9 @@
1
1
  export type CloudConfig = {
2
- token: string;
3
- /** API key for data plane operations (REST/MCP) — created after login. */
4
- apiKey?: string;
2
+ /** Primary credential — user token (md_user_*) created on login. */
3
+ userToken?: string;
4
+ /** Email of the logged-in user. */
5
5
  email?: string;
6
+ /** API base URL (defaults to https://api.thingd.cloud). */
6
7
  url?: string;
7
8
  /** Currently active organization context (set by `thingd cloud org use`). */
8
9
  organizationId?: string;
@@ -14,6 +15,10 @@ export type CloudConfig = {
14
15
  projectSlug?: string;
15
16
  /** Active instance slug (set when instanceUrl is resolved). */
16
17
  instanceSlug?: string;
18
+ /** @deprecated Old JWT — still read as fallback if no userToken. */
19
+ token?: string;
20
+ /** @deprecated Old project API key — still read as fallback if no userToken or token. */
21
+ apiKey?: string;
17
22
  };
18
23
  export declare function cloudConfigPath(): string;
19
24
  export declare function readCloudConfig(): CloudConfig | null;
@@ -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,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,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,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,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;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAKvE;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,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8GAA8G;IAC9G,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+DAA+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yFAAyF;IACzF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,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;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAKvE;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAMxC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thingd/cli",
3
- "version": "0.58.2",
3
+ "version": "0.60.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.58.2"
48
+ "@thingd/sdk": "0.60.0"
49
49
  },
50
50
  "engines": {
51
51
  "node": ">=24.0.0"