@moxt-ai/cli 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7,6 +7,118 @@ import updateNotifier from "update-notifier";
7
7
  // src/commands/file.ts
8
8
  import * as fs from "fs";
9
9
 
10
+ // src/utils/file-selector.ts
11
+ var SpaceOptionsError = class extends Error {
12
+ constructor(message) {
13
+ super(message);
14
+ this.name = "SpaceOptionsError";
15
+ }
16
+ };
17
+ function resolveSpaceSelector(options) {
18
+ const { space, teamSpaceId, teammateId } = options;
19
+ const personalIsSet = options.personal === true;
20
+ const spaceIsSet = space != null && space !== "";
21
+ const teamSpaceIdIsSet = teamSpaceId != null && teamSpaceId !== "";
22
+ const teammateIdIsSet = teammateId != null && teammateId !== "";
23
+ const selectorCount = (spaceIsSet ? 1 : 0) + (personalIsSet ? 1 : 0) + (teamSpaceIdIsSet ? 1 : 0) + (teammateIdIsSet ? 1 : 0);
24
+ if (selectorCount > 1) {
25
+ throw new SpaceOptionsError(
26
+ "Error: -s, --personal, --team-space-id, and --teammate-id are mutually exclusive."
27
+ );
28
+ }
29
+ if (teamSpaceId != null && teamSpaceId !== "") {
30
+ return { teamSpaceId };
31
+ }
32
+ if (teammateId != null && teammateId !== "") {
33
+ const parsed = Number.parseInt(teammateId, 10);
34
+ if (!Number.isInteger(parsed) || parsed <= 0 || String(parsed) !== teammateId) {
35
+ throw new SpaceOptionsError(
36
+ `Error: --teammate-id must be a positive integer, got "${teammateId}".`
37
+ );
38
+ }
39
+ return { agentId: parsed };
40
+ }
41
+ if (personalIsSet) {
42
+ return { space: "personal" };
43
+ }
44
+ if (space != null && space !== "") {
45
+ return { space };
46
+ }
47
+ return {};
48
+ }
49
+ function buildFileQueryString(path2, spaceParams) {
50
+ const params = new URLSearchParams();
51
+ if (path2 != null && path2 !== "") {
52
+ params.set("path", path2);
53
+ }
54
+ if (spaceParams.space) {
55
+ params.set("space", spaceParams.space);
56
+ }
57
+ if (spaceParams.teamSpaceId) {
58
+ params.set("teamSpaceId", spaceParams.teamSpaceId);
59
+ }
60
+ if (spaceParams.agentId != null) {
61
+ params.set("agentId", String(spaceParams.agentId));
62
+ }
63
+ const qs = params.toString();
64
+ return qs ? `?${qs}` : "";
65
+ }
66
+ function isBinaryContent(buffer2) {
67
+ const sampleSize = Math.min(buffer2.length, 8192);
68
+ for (let i = 0; i < sampleSize; i++) {
69
+ const byte = buffer2[i];
70
+ if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) {
71
+ return true;
72
+ }
73
+ }
74
+ return false;
75
+ }
76
+ function resolveReadMode(options) {
77
+ if (options.url && options.workspace) {
78
+ throw new SpaceOptionsError("Error: --url cannot be used with --workspace");
79
+ }
80
+ if (options.url && options.path) {
81
+ throw new SpaceOptionsError("Error: --url cannot be used with --path");
82
+ }
83
+ if (!options.url && !options.workspace) {
84
+ throw new SpaceOptionsError("Error: Either --url or --workspace is required");
85
+ }
86
+ if (!options.url && !options.path) {
87
+ throw new SpaceOptionsError("Error: --path is required when not using --url");
88
+ }
89
+ if (options.url) {
90
+ return { kind: "by-url", url: options.url };
91
+ }
92
+ if (!options.workspace) {
93
+ throw new SpaceOptionsError("Error: Either --url or --workspace is required");
94
+ }
95
+ if (!options.path) {
96
+ throw new SpaceOptionsError("Error: --path is required when not using --url");
97
+ }
98
+ return { kind: "by-path", workspace: options.workspace, path: options.path };
99
+ }
100
+ function resolveWriteMode(options) {
101
+ if (options.url && options.workspace) {
102
+ throw new SpaceOptionsError("Error: --url cannot be used with --workspace");
103
+ }
104
+ if (options.url && options.path) {
105
+ throw new SpaceOptionsError("Error: --url cannot be used with --path");
106
+ }
107
+ if (options.url && options.recursive) {
108
+ throw new SpaceOptionsError("Error: --recursive cannot be used with --url (the file must already exist)");
109
+ }
110
+ if (!options.url && !options.workspace) {
111
+ throw new SpaceOptionsError("Error: Either --url or --workspace is required");
112
+ }
113
+ if (!options.url && !options.path) {
114
+ throw new SpaceOptionsError("Error: --path is required when not using --url");
115
+ }
116
+ if (options.url) {
117
+ return { kind: "by-url", url: options.url };
118
+ }
119
+ return { kind: "by-path", workspace: options.workspace, path: options.path };
120
+ }
121
+
10
122
  // src/utils/http.ts
11
123
  import { arch, platform } from "os";
12
124
 
@@ -29,14 +141,17 @@ function getApiKey() {
29
141
  }
30
142
  return apiKey;
31
143
  }
144
+ function getApiKeyOrUndefined() {
145
+ return process.env.MOXT_API_KEY;
146
+ }
32
147
 
33
148
  // src/utils/http.ts
34
149
  function getUserAgent() {
35
- return `moxt-cli/${"0.2.0"} (${platform()}/${arch()}) node/${process.versions.node}`;
150
+ return `moxt-cli/${"0.3.1"} (${platform()}/${arch()}) node/${process.versions.node}`;
36
151
  }
37
- async function request(method, path, body) {
152
+ async function fetchApi(method, path2, body) {
38
153
  const apiKey = getApiKey();
39
- const url = `${getApiBaseUrl()}${path}`;
154
+ const url = `${getApiBaseUrl()}${path2}`;
40
155
  const response = await fetch(url, {
41
156
  method,
42
157
  headers: {
@@ -46,35 +161,43 @@ async function request(method, path, body) {
46
161
  },
47
162
  body: body ? JSON.stringify(body) : void 0
48
163
  });
49
- let data;
50
- const contentType = response.headers.get("content-type");
51
- if (contentType?.includes("application/json")) {
52
- data = await response.json();
53
- } else {
54
- data = await response.text();
55
- }
56
164
  if (response.status === 401) {
57
165
  console.error("Authentication failed. Check your MOXT_API_KEY.");
58
166
  console.error("Get a new key at: https://moxt.ai");
59
167
  process.exit(1);
60
168
  }
169
+ return response;
170
+ }
171
+ async function request(method, path2, body) {
172
+ const response = await fetchApi(method, path2, body);
173
+ const contentType = response.headers.get("content-type");
174
+ const data = contentType?.includes("application/json") ? await response.json() : await response.text();
61
175
  return {
62
176
  ok: response.ok,
63
177
  status: response.status,
64
178
  data
65
179
  };
66
180
  }
67
- async function httpGet(path) {
68
- return request("GET", path);
181
+ async function httpGet(path2) {
182
+ return request("GET", path2);
69
183
  }
70
- async function httpPut(path, body) {
71
- return request("PUT", path, body);
184
+ async function httpPut(path2, body) {
185
+ return request("PUT", path2, body);
72
186
  }
73
- async function httpPost(path, body) {
74
- return request("POST", path, body);
187
+ async function httpPost(path2, body) {
188
+ return request("POST", path2, body);
75
189
  }
76
- async function httpDelete(path, body) {
77
- return request("DELETE", path, body);
190
+ async function httpDelete(path2, body) {
191
+ return request("DELETE", path2, body);
192
+ }
193
+ async function httpRaw(method, path2, body) {
194
+ const response = await fetchApi(method, path2, body);
195
+ return {
196
+ ok: response.ok,
197
+ status: response.status,
198
+ text: await response.text(),
199
+ contentType: response.headers.get("content-type")
200
+ };
78
201
  }
79
202
 
80
203
  // src/utils/output.ts
@@ -117,59 +240,16 @@ function stopSpinner(success, message) {
117
240
  }
118
241
 
119
242
  // src/commands/file.ts
120
- function isBinaryContent(buffer) {
121
- const sampleSize = Math.min(buffer.length, 8192);
122
- for (let i = 0; i < sampleSize; i++) {
123
- const byte = buffer[i];
124
- if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13) {
125
- return true;
126
- }
127
- }
128
- return false;
129
- }
130
- function validateSpaceOptions(options) {
131
- const mutuallyExclusive = [options.space, options.personal, options.teamSpaceId, options.teammateId].filter(
132
- Boolean
133
- ).length;
134
- if (mutuallyExclusive > 1) {
135
- printError("Error: -s, --personal, --team-space-id, and --teammate-id are mutually exclusive.");
136
- process.exit(1);
137
- }
138
- if (options.teamSpaceId) {
139
- return { teamSpaceId: options.teamSpaceId };
140
- }
141
- if (options.teammateId) {
142
- const parsed = Number.parseInt(options.teammateId, 10);
143
- if (!Number.isInteger(parsed) || parsed <= 0) {
144
- printError(`Error: --teammate-id must be a positive integer, got "${options.teammateId}".`);
243
+ function runOrExit(fn) {
244
+ try {
245
+ return fn();
246
+ } catch (err) {
247
+ if (err instanceof SpaceOptionsError) {
248
+ printError(err.message);
145
249
  process.exit(1);
146
250
  }
147
- return { agentId: parsed };
251
+ throw err;
148
252
  }
149
- if (options.personal) {
150
- return { space: "personal" };
151
- }
152
- if (options.space) {
153
- return { space: options.space };
154
- }
155
- return {};
156
- }
157
- function buildFileQueryString(path, spaceParams) {
158
- const params = new URLSearchParams();
159
- if (path) {
160
- params.set("path", path);
161
- }
162
- if (spaceParams.space) {
163
- params.set("space", spaceParams.space);
164
- }
165
- if (spaceParams.teamSpaceId) {
166
- params.set("teamSpaceId", spaceParams.teamSpaceId);
167
- }
168
- if (spaceParams.agentId != null) {
169
- params.set("agentId", String(spaceParams.agentId));
170
- }
171
- const qs = params.toString();
172
- return qs ? `?${qs}` : "";
173
253
  }
174
254
  function addSpaceOptions(cmd) {
175
255
  return cmd.requiredOption("-w, --workspace <workspaceId>", "Workspace ID").option("-s, --space <name>", "Space name (personal or teamspace name)").option("--personal", "Use personal space (default)").option("--team-space-id <teamSpaceId>", "Team space ID").option("--teammate-id <teammateId>", "Teammate ID (teammate space)");
@@ -179,7 +259,7 @@ function registerFileCommand(program2) {
179
259
  addSpaceOptions(
180
260
  file.command("get-url").description("Resolve the shareable browser URL for a file").requiredOption("-p, --path <path>", "File path")
181
261
  ).action(async (options) => {
182
- const spaceParams = validateSpaceOptions(options);
262
+ const spaceParams = runOrExit(() => resolveSpaceSelector(options));
183
263
  const qs = buildFileQueryString(options.path, spaceParams);
184
264
  startSpinner("Resolving URL...");
185
265
  const response = await httpGet(
@@ -202,7 +282,7 @@ function registerFileCommand(program2) {
202
282
  addSpaceOptions(
203
283
  file.command("list").description("List directory contents").option("-p, --path <path>", "Directory path", "/")
204
284
  ).action(async (options) => {
205
- const spaceParams = validateSpaceOptions(options);
285
+ const spaceParams = runOrExit(() => resolveSpaceSelector(options));
206
286
  const qs = buildFileQueryString(options.path, spaceParams);
207
287
  startSpinner("Listing files...");
208
288
  const response = await httpGet(
@@ -217,12 +297,12 @@ function registerFileCommand(program2) {
217
297
  }
218
298
  process.exit(1);
219
299
  }
220
- const { path, entries } = response.data;
300
+ const { path: path2, entries } = response.data;
221
301
  if (!entries || entries.length === 0) {
222
- stopSpinner(true, `${path}: empty directory`);
302
+ stopSpinner(true, `${path2}: empty directory`);
223
303
  return;
224
304
  }
225
- stopSpinner(true, path);
305
+ stopSpinner(true, path2);
226
306
  for (const entry of entries) {
227
307
  if (entry.type === "directory") {
228
308
  print(`${colors.blue}${entry.name}/${colors.reset}`);
@@ -232,36 +312,21 @@ function registerFileCommand(program2) {
232
312
  }
233
313
  });
234
314
  file.command("read").description("Read file content").option("-w, --workspace <workspaceId>", "Workspace ID").option("-s, --space <name>", "Space name (personal or teamspace name)").option("--personal", "Use personal space (default)").option("--team-space-id <teamSpaceId>", "Team space ID").option("--teammate-id <teammateId>", "Teammate ID (teammate space)").option("-p, --path <path>", "File path").option("-u, --url <url>", "Moxt file URL (e.g., https://moxt.ai/w/{workspaceId}/{fileId})").action(async (options) => {
235
- if (options.url && options.workspace) {
236
- printError("Error: --url cannot be used with --workspace");
237
- process.exit(1);
238
- }
239
- if (options.url && options.path) {
240
- printError("Error: --url cannot be used with --path");
241
- process.exit(1);
242
- }
243
- if (!options.url && !options.workspace) {
244
- printError("Error: Either --url or --workspace is required");
245
- process.exit(1);
246
- }
247
- if (!options.url && !options.path) {
248
- printError("Error: --path is required when not using --url");
249
- process.exit(1);
250
- }
315
+ const mode = runOrExit(() => resolveReadMode(options));
251
316
  startSpinner("Reading file...");
252
317
  let response;
253
318
  let displayPath;
254
- if (options.url) {
255
- displayPath = options.url;
319
+ if (mode.kind === "by-url") {
320
+ displayPath = mode.url;
256
321
  response = await httpGet(
257
- `/files/read-by-url?url=${encodeURIComponent(options.url)}`
322
+ `/files/read-by-url?url=${encodeURIComponent(mode.url)}`
258
323
  );
259
324
  } else {
260
- displayPath = options.path;
261
- const spaceParams = validateSpaceOptions(options);
262
- const qs = buildFileQueryString(options.path, spaceParams);
325
+ displayPath = mode.path;
326
+ const spaceParams = runOrExit(() => resolveSpaceSelector(options));
327
+ const qs = buildFileQueryString(mode.path, spaceParams);
263
328
  response = await httpGet(
264
- `/workspaces/${options.workspace}/files/read${qs}`
329
+ `/workspaces/${mode.workspace}/files/read${qs}`
265
330
  );
266
331
  }
267
332
  if (!response.ok) {
@@ -282,11 +347,9 @@ function registerFileCommand(program2) {
282
347
  stopSpinner(true, "Done");
283
348
  print(content ?? "");
284
349
  });
285
- addSpaceOptions(
286
- file.command("put").description("Upload a file").requiredOption("-p, --path <path>", "Remote file path").requiredOption("-l, --local-path <localPath>", "Local file path").option("-r, --recursive", "Create parent directories if needed")
287
- ).action(
350
+ file.command("put").description("Upload a file").option("-w, --workspace <workspaceId>", "Workspace ID").option("-s, --space <name>", "Space name (personal or teamspace name)").option("--personal", "Use personal space (default)").option("--team-space-id <teamSpaceId>", "Team space ID").option("--teammate-id <teammateId>", "Teammate ID (teammate space)").option("-p, --path <path>", "Remote file path").option("-u, --url <url>", "Moxt file URL (e.g., https://moxt.ai/w/{workspaceId}/{fileId})").requiredOption("-l, --local-path <localPath>", "Local file path").option("-r, --recursive", "Create parent directories if needed").action(
288
351
  async (options) => {
289
- const spaceParams = validateSpaceOptions(options);
352
+ const mode = runOrExit(() => resolveWriteMode(options));
290
353
  if (!fs.existsSync(options.localPath)) {
291
354
  print(`${colors.red}\u2620 Local file not found: ${options.localPath}${colors.reset}`);
292
355
  process.exit(1);
@@ -301,26 +364,42 @@ function registerFileCommand(program2) {
301
364
  print(`${colors.red}\u2620 File too large (max 10MB): ${options.localPath}${colors.reset}`);
302
365
  process.exit(1);
303
366
  }
304
- const buffer = fs.readFileSync(options.localPath);
305
- if (isBinaryContent(buffer)) {
367
+ const buffer2 = fs.readFileSync(options.localPath);
368
+ if (isBinaryContent(buffer2)) {
306
369
  print(`${colors.red}\u2620 Binary files are not allowed. Only text files can be uploaded.${colors.reset}`);
307
370
  process.exit(1);
308
371
  }
309
- const content = buffer.toString("utf8");
372
+ const content = buffer2.toString("utf8");
310
373
  startSpinner("Uploading...");
311
- const response = await httpPut(
312
- `/workspaces/${options.workspace}/files/write`,
313
- {
314
- path: options.path,
315
- content,
316
- recursive: options.recursive ?? false,
317
- ...spaceParams
318
- }
319
- );
374
+ let response;
375
+ let displayPath;
376
+ if (mode.kind === "by-url") {
377
+ displayPath = mode.url;
378
+ response = await httpPut("/files/write-by-url", {
379
+ url: mode.url,
380
+ content
381
+ });
382
+ } else {
383
+ displayPath = mode.path;
384
+ const spaceParams = runOrExit(() => resolveSpaceSelector(options));
385
+ response = await httpPut(
386
+ `/workspaces/${mode.workspace}/files/write`,
387
+ {
388
+ path: mode.path,
389
+ content,
390
+ recursive: options.recursive ?? false,
391
+ ...spaceParams
392
+ }
393
+ );
394
+ }
320
395
  if (!response.ok) {
321
396
  stopSpinner(false, "Failed");
322
397
  if (response.status === 404) {
323
- print(`${colors.red}\u2620 Parent directory does not exist${colors.reset}`);
398
+ if (mode.kind === "by-url") {
399
+ print(`${colors.red}\u2620 ${displayPath} does not exist${colors.reset}`);
400
+ } else {
401
+ print(`${colors.red}\u2620 Parent directory does not exist${colors.reset}`);
402
+ }
324
403
  } else if (response.status === 400) {
325
404
  printError(`Error: ${JSON.stringify(response.data)}`);
326
405
  } else {
@@ -336,7 +415,7 @@ function registerFileCommand(program2) {
336
415
  file.command("mkdir").description("Create a directory").requiredOption("-p, --path <path>", "Directory path").option("-r, --recursive", "Create parent directories if needed")
337
416
  ).action(
338
417
  async (options) => {
339
- const spaceParams = validateSpaceOptions(options);
418
+ const spaceParams = runOrExit(() => resolveSpaceSelector(options));
340
419
  startSpinner("Creating directory...");
341
420
  const response = await httpPost(
342
421
  `/workspaces/${options.workspace}/files/mkdir`,
@@ -365,7 +444,7 @@ function registerFileCommand(program2) {
365
444
  file.command("del").description("Delete a file or empty directory").requiredOption("-p, --path <path>", "File or directory path")
366
445
  ).action(
367
446
  async (options) => {
368
- const spaceParams = validateSpaceOptions(options);
447
+ const spaceParams = runOrExit(() => resolveSpaceSelector(options));
369
448
  startSpinner("Deleting...");
370
449
  const response = await httpDelete(
371
450
  `/workspaces/${options.workspace}/files/delete`,
@@ -390,6 +469,231 @@ function registerFileCommand(program2) {
390
469
  );
391
470
  }
392
471
 
472
+ // src/utils/miniapp-db.ts
473
+ var MiniappDbOptionsError = class extends Error {
474
+ constructor(message) {
475
+ super(message);
476
+ this.name = "MiniappDbOptionsError";
477
+ }
478
+ };
479
+ var NON_FILTER_QUERY_PARAMS = /* @__PURE__ */ new Set([
480
+ "select",
481
+ "order",
482
+ "limit",
483
+ "offset",
484
+ "range",
485
+ "range-unit"
486
+ ]);
487
+ var ALLOWED_MINIAPP_DB_EMAIL_DOMAINS = ["@paraflow.com", "@moxt.ai", "@kanyun.com"];
488
+ function normalizePostgrestPath(path2) {
489
+ const trimmed = path2.trim();
490
+ if (!trimmed) {
491
+ throw new MiniappDbOptionsError("Error: PostgREST path is required.");
492
+ }
493
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
494
+ }
495
+ function buildMiniappDbDataPath(workspaceId, appId, postgrestPath) {
496
+ return `/workspaces/${encodeURIComponent(workspaceId)}/miniapps/${encodeURIComponent(appId)}/db/data${normalizePostgrestPath(postgrestPath)}`;
497
+ }
498
+ function buildMiniappDbSchemaPath(workspaceId, appId) {
499
+ return `/workspaces/${encodeURIComponent(workspaceId)}/miniapps/${encodeURIComponent(appId)}/db/schema`;
500
+ }
501
+ function parseJsonBody(raw) {
502
+ let parsed;
503
+ try {
504
+ parsed = JSON.parse(raw);
505
+ } catch (error) {
506
+ const detail = error instanceof Error ? error.message : String(error);
507
+ throw new MiniappDbOptionsError(`Error: --body must be valid JSON. ${detail}`);
508
+ }
509
+ if (parsed === null || typeof parsed !== "object" && !Array.isArray(parsed)) {
510
+ throw new MiniappDbOptionsError("Error: --body must be a JSON object or array.");
511
+ }
512
+ return parsed;
513
+ }
514
+ function hasPostgrestFilter(postgrestPath) {
515
+ const normalizedPath = normalizePostgrestPath(postgrestPath);
516
+ const parsed = new URL(normalizedPath, "https://postgrest.local");
517
+ for (const [name] of parsed.searchParams.entries()) {
518
+ if (!NON_FILTER_QUERY_PARAMS.has(name.toLowerCase())) {
519
+ return true;
520
+ }
521
+ }
522
+ return false;
523
+ }
524
+ function assertPostgrestFilter(postgrestPath, method) {
525
+ if (!hasPostgrestFilter(postgrestPath)) {
526
+ throw new MiniappDbOptionsError(
527
+ `Error: ${method.toLowerCase()} requires at least one PostgREST filter query parameter.`
528
+ );
529
+ }
530
+ }
531
+ function formatRawResponse(text, pretty) {
532
+ if (!pretty || !text) {
533
+ return text;
534
+ }
535
+ try {
536
+ return JSON.stringify(JSON.parse(text), null, 2);
537
+ } catch {
538
+ return text;
539
+ }
540
+ }
541
+ function isAllowedMiniappDbUserEmail(email) {
542
+ const normalized = email.trim().toLowerCase();
543
+ return ALLOWED_MINIAPP_DB_EMAIL_DOMAINS.some((domain) => normalized.endsWith(domain));
544
+ }
545
+
546
+ // src/commands/miniapp.ts
547
+ function runOrExit2(fn) {
548
+ try {
549
+ return fn();
550
+ } catch (err) {
551
+ if (err instanceof MiniappDbOptionsError) {
552
+ printError(err.message);
553
+ process.exit(1);
554
+ }
555
+ throw err;
556
+ }
557
+ }
558
+ function addMiniappDbTargetOptions(command) {
559
+ return command.requiredOption("-w, --workspace <workspaceId>", "Workspace ID").requiredOption("--app-id <appId>", "Miniapp app ID");
560
+ }
561
+ function handleRawResponse(response, pretty) {
562
+ if (!response.ok) {
563
+ printError(`Error [${response.status}]: ${response.text}`);
564
+ process.exit(1);
565
+ }
566
+ print(formatRawResponse(response.text, pretty));
567
+ }
568
+ async function ensureAllowedMiniappDbUser() {
569
+ const response = await httpGet("/users/whoami");
570
+ if (!response.ok) {
571
+ printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
572
+ process.exit(1);
573
+ }
574
+ if (!isAllowedMiniappDbUserEmail(response.data.email)) {
575
+ printError("Error: miniapp db commands are only available to @paraflow.com, @moxt.ai, or @kanyun.com users.");
576
+ process.exit(1);
577
+ }
578
+ }
579
+ async function requestDataApi(method, postgrestPath, options, body) {
580
+ await ensureAllowedMiniappDbUser();
581
+ const path2 = buildMiniappDbDataPath(options.workspace, options.appId, postgrestPath);
582
+ const response = await httpRaw(method, path2, body);
583
+ handleRawResponse(response, options.pretty);
584
+ }
585
+ function registerMiniappCommand(program2) {
586
+ const miniapp = program2.command("miniapp", { hidden: true }).description("Miniapp operations");
587
+ const db = miniapp.command("db").description("Miniapp database operations");
588
+ addMiniappDbTargetOptions(
589
+ db.command("schema").description("Print the miniapp database schema SQL")
590
+ ).action(async (options) => {
591
+ await ensureAllowedMiniappDbUser();
592
+ const response = await httpGet(buildMiniappDbSchemaPath(options.workspace, options.appId));
593
+ if (!response.ok) {
594
+ printError(`Error [${response.status}]: ${JSON.stringify(response.data)}`);
595
+ process.exit(1);
596
+ }
597
+ print(response.data.sql);
598
+ });
599
+ addMiniappDbTargetOptions(
600
+ db.command("get").description("Read miniapp data through PostgREST").argument("<path>", "PostgREST path, for example /todos?select=*").option("--pretty", "Pretty-print JSON responses")
601
+ ).action(async (postgrestPath, options) => {
602
+ await requestDataApi("GET", postgrestPath, options);
603
+ });
604
+ addMiniappDbTargetOptions(
605
+ db.command("post").description("Create miniapp data through PostgREST").argument("<path>", "PostgREST path, for example /todos").requiredOption("--body <json>", "JSON object or array request body").option("--pretty", "Pretty-print JSON responses")
606
+ ).action(async (postgrestPath, options) => {
607
+ const body = runOrExit2(() => parseJsonBody(options.body ?? ""));
608
+ await requestDataApi("POST", postgrestPath, options, body);
609
+ });
610
+ addMiniappDbTargetOptions(
611
+ db.command("patch").description("Update miniapp data through PostgREST").argument("<path>", "PostgREST path with a filter, for example /todos?id=eq.1").requiredOption("--body <json>", "JSON object or array request body").option("--pretty", "Pretty-print JSON responses")
612
+ ).action(async (postgrestPath, options) => {
613
+ runOrExit2(() => assertPostgrestFilter(postgrestPath, "PATCH"));
614
+ const body = runOrExit2(() => parseJsonBody(options.body ?? ""));
615
+ await requestDataApi("PATCH", postgrestPath, options, body);
616
+ });
617
+ addMiniappDbTargetOptions(
618
+ db.command("delete").description("Delete miniapp data through PostgREST").argument("<path>", "PostgREST path with a filter, for example /todos?id=eq.1").option("--yes", "Confirm the delete operation").option("--pretty", "Pretty-print JSON responses")
619
+ ).action(async (postgrestPath, options) => {
620
+ if (options.yes !== true) {
621
+ printError("Error: delete requires --yes.");
622
+ process.exit(1);
623
+ }
624
+ runOrExit2(() => assertPostgrestFilter(postgrestPath, "DELETE"));
625
+ await requestDataApi("DELETE", postgrestPath, options);
626
+ });
627
+ }
628
+
629
+ // src/telemetry/config.ts
630
+ import * as fs2 from "fs";
631
+ import * as os from "os";
632
+ import * as path from "path";
633
+ function getConfigPath() {
634
+ return path.join(os.homedir(), ".moxt", "config.json");
635
+ }
636
+ function readTelemetryConfig() {
637
+ try {
638
+ const raw = fs2.readFileSync(getConfigPath(), "utf8");
639
+ const parsed = JSON.parse(raw);
640
+ return parsed.telemetry ?? {};
641
+ } catch {
642
+ return {};
643
+ }
644
+ }
645
+ function writeTelemetryConfig(update) {
646
+ const configPath = getConfigPath();
647
+ let existing = {};
648
+ try {
649
+ existing = JSON.parse(fs2.readFileSync(configPath, "utf8"));
650
+ } catch {
651
+ existing = {};
652
+ }
653
+ const currentTelemetry = existing.telemetry ?? {};
654
+ const next = { ...existing, telemetry: { ...currentTelemetry, ...update } };
655
+ fs2.mkdirSync(path.dirname(configPath), { recursive: true });
656
+ fs2.writeFileSync(configPath, JSON.stringify(next, null, 2));
657
+ }
658
+
659
+ // src/telemetry/opt-out.ts
660
+ function isCiEnvironment() {
661
+ return process.env.CI === "true" || process.env.CI === "1";
662
+ }
663
+ function isTelemetryDisabled() {
664
+ if (process.env.MOXT_TELEMETRY_DISABLED === "1" || process.env.MOXT_TELEMETRY_DISABLED === "true") {
665
+ return true;
666
+ }
667
+ if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true") {
668
+ return true;
669
+ }
670
+ return readTelemetryConfig().disabled === true;
671
+ }
672
+
673
+ // src/commands/telemetry.ts
674
+ function registerTelemetryCommand(program2) {
675
+ const telemetry = program2.command("telemetry").description("Manage Moxt CLI telemetry");
676
+ telemetry.command("status").description("Show telemetry status").action(() => {
677
+ const cfg = readTelemetryConfig();
678
+ const envOverride = process.env.MOXT_TELEMETRY_DISABLED === "1" || process.env.MOXT_TELEMETRY_DISABLED === "true" || process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true";
679
+ const state = isTelemetryDisabled() ? "disabled" : "enabled";
680
+ print(`telemetry: ${state}`);
681
+ if (envOverride) {
682
+ print("(disabled via environment variable)");
683
+ } else if (cfg.disabled) {
684
+ print("(disabled via config file)");
685
+ }
686
+ });
687
+ telemetry.command("enable").description("Enable telemetry").action(() => {
688
+ writeTelemetryConfig({ disabled: false });
689
+ print("telemetry: enabled");
690
+ });
691
+ telemetry.command("disable").description("Disable telemetry").action(() => {
692
+ writeTelemetryConfig({ disabled: true });
693
+ print("telemetry: disabled");
694
+ });
695
+ }
696
+
393
697
  // src/commands/whoami.ts
394
698
  function registerWhoamiCommand(program2) {
395
699
  program2.command("whoami").description("Show current user information").action(async () => {
@@ -519,19 +823,218 @@ function registerWorkspaceCommand(program2) {
519
823
  });
520
824
  }
521
825
 
826
+ // src/telemetry/client.ts
827
+ import { arch as arch2, platform as platform2 } from "os";
828
+ var buffer = [];
829
+ var FLUSH_TIMEOUT_MS = 3e3;
830
+ var MAX_BATCH_SIZE = 100;
831
+ function commonLabels() {
832
+ return {
833
+ cli_version: "0.3.1",
834
+ node_version: process.versions.node,
835
+ os: platform2(),
836
+ arch: arch2(),
837
+ is_ci: isCiEnvironment() ? "true" : "false"
838
+ };
839
+ }
840
+ function record(metric) {
841
+ if (isTelemetryDisabled()) {
842
+ debugLog(`record: disabled, dropping ${metric.metric_id}`);
843
+ return;
844
+ }
845
+ if (!getApiKeyOrUndefined()) {
846
+ debugLog(`record: no API key, dropping ${metric.metric_id}`);
847
+ return;
848
+ }
849
+ buffer.push({
850
+ ...metric,
851
+ extra_label_name_2_value: {
852
+ ...commonLabels(),
853
+ ...metric.extra_label_name_2_value ?? {}
854
+ }
855
+ });
856
+ debugLog(`record: buffered ${metric.metric_id} (buffer=${buffer.length})`);
857
+ }
858
+ function isDebug() {
859
+ return process.env.MOXT_TELEMETRY_DEBUG === "1" || process.env.MOXT_TELEMETRY_DEBUG === "true";
860
+ }
861
+ function debugLog(msg, detail) {
862
+ if (!isDebug()) return;
863
+ process.stderr.write(`[telemetry] ${msg}${detail !== void 0 ? ` ${JSON.stringify(detail)}` : ""}
864
+ `);
865
+ }
866
+ async function flush() {
867
+ if (buffer.length === 0) {
868
+ debugLog("flush: buffer empty, skipping");
869
+ return;
870
+ }
871
+ const apiKey = getApiKeyOrUndefined();
872
+ if (!apiKey) {
873
+ debugLog("flush: no API key, dropping buffer");
874
+ buffer.length = 0;
875
+ return;
876
+ }
877
+ const batch = buffer.splice(0, MAX_BATCH_SIZE);
878
+ const payload = {
879
+ release_id: "0.3.1",
880
+ client_type: "cli",
881
+ metric_data_list: batch.map((m) => ({
882
+ profile: "cli",
883
+ metric_id: m.metric_id,
884
+ metric_type: m.metric_type,
885
+ value: m.value,
886
+ extra_label_name_2_value: m.extra_label_name_2_value
887
+ }))
888
+ };
889
+ const url = `${getApiBaseUrl()}/cli-metrics`;
890
+ debugLog(`flush: POST ${url} with ${batch.length} metrics`, payload);
891
+ try {
892
+ const controller = new AbortController();
893
+ const timeout = setTimeout(() => {
894
+ controller.abort();
895
+ }, FLUSH_TIMEOUT_MS);
896
+ try {
897
+ const res = await fetch(url, {
898
+ method: "POST",
899
+ headers: {
900
+ "Content-Type": "application/json",
901
+ Authorization: `Bearer ${apiKey}`
902
+ },
903
+ body: JSON.stringify(payload),
904
+ signal: controller.signal
905
+ });
906
+ debugLog(`flush: response status=${res.status}`);
907
+ } finally {
908
+ clearTimeout(timeout);
909
+ }
910
+ } catch (err) {
911
+ debugLog("flush: failed", err instanceof Error ? err.message : String(err));
912
+ }
913
+ }
914
+
915
+ // src/telemetry/notice.ts
916
+ var NOTICE = [
917
+ "",
918
+ "Attention: Moxt CLI now collects anonymous telemetry regarding usage.",
919
+ "This information is used to shape Moxt CLI's roadmap and prioritize features.",
920
+ "",
921
+ "You may opt out by setting MOXT_TELEMETRY_DISABLED=1 in your env",
922
+ "or by running `moxt telemetry disable`.",
923
+ "Details: https://www.npmjs.com/package/@moxt-ai/cli#telemetry",
924
+ ""
925
+ ];
926
+ function maybeShowTelemetryNotice() {
927
+ if (isTelemetryDisabled()) return;
928
+ if (isCiEnvironment()) return;
929
+ if (readTelemetryConfig().noticeShown) return;
930
+ for (const line of NOTICE) {
931
+ process.stderr.write(`${line}
932
+ `);
933
+ }
934
+ try {
935
+ writeTelemetryConfig({ noticeShown: true });
936
+ } catch {
937
+ }
938
+ }
939
+
940
+ // src/telemetry/instrument.ts
941
+ var current = null;
942
+ function commandPath(cmd) {
943
+ const names = [];
944
+ let node = cmd;
945
+ while (node && node.parent) {
946
+ names.unshift(node.name());
947
+ node = node.parent;
948
+ }
949
+ if (names.length === 0) return { command: "unknown" };
950
+ if (names.length === 1) return { command: names[0] };
951
+ return { command: names[0], subcommand: names.slice(1).join(" ") };
952
+ }
953
+ function instrumentProgram(program2) {
954
+ program2.hook("preAction", (_thisCommand, actionCommand) => {
955
+ const { command, subcommand } = commandPath(actionCommand);
956
+ if (command !== "telemetry") {
957
+ maybeShowTelemetryNotice();
958
+ }
959
+ current = { name: command, subcommand, startedAt: Date.now() };
960
+ record({
961
+ metric_id: "client.cli.command_invocation.count",
962
+ metric_type: "counter",
963
+ value: 1,
964
+ extra_label_name_2_value: {
965
+ command,
966
+ ...subcommand ? { subcommand } : {},
967
+ status: "started"
968
+ }
969
+ });
970
+ });
971
+ program2.hook("postAction", async () => {
972
+ await reportOutcome("success");
973
+ });
974
+ }
975
+ async function reportOutcome(status, errorCode) {
976
+ if (!current) return;
977
+ const { name, subcommand, startedAt } = current;
978
+ const durationSeconds = (Date.now() - startedAt) / 1e3;
979
+ record({
980
+ metric_id: "client.cli.command_duration_seconds",
981
+ metric_type: "histogram",
982
+ value: durationSeconds,
983
+ extra_label_name_2_value: {
984
+ command: name,
985
+ ...subcommand ? { subcommand } : {},
986
+ status
987
+ }
988
+ });
989
+ if (status === "error") {
990
+ record({
991
+ metric_id: "client.cli.command_error.count",
992
+ metric_type: "counter",
993
+ value: 1,
994
+ extra_label_name_2_value: {
995
+ command: name,
996
+ ...subcommand ? { subcommand } : {},
997
+ ...errorCode ? { error_code: errorCode } : {}
998
+ }
999
+ });
1000
+ }
1001
+ current = null;
1002
+ await flush();
1003
+ }
1004
+ function installExitHandler() {
1005
+ const handleError = (errorCode) => {
1006
+ void reportOutcome("error", errorCode);
1007
+ };
1008
+ process.on("uncaughtException", () => {
1009
+ handleError("uncaught_exception");
1010
+ process.exit(1);
1011
+ });
1012
+ process.on("unhandledRejection", () => {
1013
+ handleError("unhandled_rejection");
1014
+ process.exit(1);
1015
+ });
1016
+ }
1017
+
522
1018
  // src/index.ts
523
1019
  updateNotifier({
524
- pkg: { name: "@moxt-ai/cli", version: "0.2.0" }
1020
+ pkg: { name: "@moxt-ai/cli", version: "0.3.1" }
525
1021
  }).notify();
526
1022
  var program = new Command();
527
- program.name("moxt").description("Moxt CLI - AI Workspace").version("0.2.0").action(() => {
1023
+ program.name("moxt").description("Moxt CLI - AI Workspace").version("0.3.1").action(() => {
528
1024
  program.help();
529
1025
  });
1026
+ instrumentProgram(program);
1027
+ installExitHandler();
530
1028
  registerWhoamiCommand(program);
531
1029
  registerWorkspaceCommand(program);
532
1030
  registerFileCommand(program);
533
- program.parseAsync(process.argv).catch((err) => {
1031
+ registerMiniappCommand(program);
1032
+ registerTelemetryCommand(program);
1033
+ program.parseAsync(process.argv).then(async () => {
1034
+ await flush();
1035
+ }).catch(async (err) => {
534
1036
  console.error("CLI Error:", err);
1037
+ await flush();
535
1038
  process.exit(1);
536
1039
  });
537
1040
  //# sourceMappingURL=index.js.map