@pmate/cli 0.33.0 → 0.34.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.
package/README.md CHANGED
@@ -236,18 +236,36 @@ Notes:
236
236
  - `pmate vercel deploys` requires `--project <name>`.
237
237
  # Service logs
238
238
 
239
- Service logs use the same authenticated API as `dev.pmate.chat`. Each registered Service has `app` (application/deployment) and `runtime` (PM2/Nginx) Table topics on the dedicated `log` chain.
239
+ Service logs use the same authenticated API as `dev.pmate.chat`. Sources are registered Service/topic pairs on the dedicated `log` chain.
240
240
 
241
241
  ```sh
242
242
  pmate service log pmate:issues-api --topic application --page 0 --json
243
- pmate service log pmate:issues-api --topic nginx --field status=500 --env prod
244
- pmate service log pmate:issues-api --field requestId=<id> --follow
243
+ pmate service log pmate:issues-api --topic nginx --field status=500
244
+ pmate service log pmate:issues-api --field requestId=<id>
245
245
  ```
246
246
 
247
247
  Pages are zero-based. Lists and `--search` cover the chain's configured retention window (30 days in production). `--since-ms` adds an epoch-millisecond lower bound. `--field name=value` (repeatable, scalar JSON values) and `--env` filter on the server. Topics are independent sources such as `application`, `nginx`, `pm2`, `deployment`, `pv`, and `vitals`; custom fields live in `data`. Old `--type`, `--level`, `--source`, and `--request-id` flags are removed. The legacy `--app` argument is optional and does not select a different source.
248
248
 
249
249
  A current CLI session is exchanged in memory for `pmate:developers`; the active App and session file are unchanged. If needed, run `pmate login --app pmate:developers`. Permission denial never falls back to anonymous Indexer reads. Credentials are never printed.
250
250
 
251
+ ### Log analysis tools
252
+
253
+ `pmate logs` exposes the same read-only tools used by Meili. The server calculates results over a bounded retained window, with query IDs and coverage metadata; the model does not count sampled pages.
254
+
255
+ ```sh
256
+ pmate logs schema --json
257
+ pmate logs query pmate:apps-api --topic application --field level=error --json
258
+ pmate logs aggregate pmate:hello --topic pv --from 2026-10-08T00:00:00+08:00 --to 2026-10-08T10:00:00+08:00 --group-by path --interval hour --timezone Asia/Shanghai --json
259
+ pmate logs trace pmate:developers-api --request-id REQUEST_ID --json
260
+ pmate logs compare pmate:hello --topic pv --from 2026-10-08T00:00:00+08:00 --to 2026-10-08T10:00:00+08:00 --compare-from 2026-10-07T00:00:00+08:00 --compare-to 2026-10-07T10:00:00+08:00 --json
261
+ ```
262
+
263
+ PV counts page-view events; sessions deduplicate appId/accessId; Profile UV deduplicates appId/profileId. Missing IDs and anonymous events remain separate. Query/trace default to application; aggregate/compare default to pv. Vitals percentiles require `--field name=LCP` (or another specific metric). Current user classification is visit frequency within the query window; first-ever/new users cannot be inferred from retention-limited history.
264
+
265
+ Time windows are `[from,to)` and at most 31 days, inside the configured retention. Times accept Unix milliseconds or ISO with explicit timezone. Comparisons require equal duration. More than 10,000 records/100 pages/100 groups is rejected instead of reporting partial totals. Indexing and browser telemetry remain best effort; `completeRead` is not proof of complete real-world activity. Query pages are zero-based.
266
+
267
+ Meili's Host may pass `--actor-profile-id` with `PMATE_LOG_DELEGATE_TOKEN` in the child environment. This mode never reads or exchanges the host's personal CLI session. Auth API verifies the allowlisted Bot and the actual sender's pmate:dev permission on every call. Models cannot supply the actor or token.
268
+
251
269
  ### Package a desktop app
252
270
 
253
271
  `pmate app build <ns:app> --target electron --config <file>` builds the configured web app, embeds its static output in an Electron ASAR, and produces a macOS DMG and ZIP. It does not require registry login. The first build downloads pinned Electron and electron-builder versions. Current targets: macOS `arm64` and `x64`, built on macOS.
package/dist/index.d.ts CHANGED
@@ -31,6 +31,7 @@ declare enum Command {
31
31
  Store = "store",
32
32
  App = "app",
33
33
  Service = "service",
34
+ Logs = "logs",
34
35
  Vercel = "vercel",
35
36
  Host = "host",
36
37
  Quiz = "quiz"
@@ -1143,7 +1144,14 @@ type VercelDeploysCommandArgs = {
1143
1144
  lastWeek: boolean;
1144
1145
  };
1145
1146
  type VercelCommandArgs = VercelProjectsCommandArgs | VercelDeploysCommandArgs;
1146
- type CommandArgs = NoneCommandArgs | ListCommandArgs | LoginCommandArgs | AddDnsCommandArgs | StsCommandArgs | BucketCommandArgs | DeployCommandArgs | LogCommandArgs | InitCommandArgs | DeleteCommandArgs | DevCommandArgs | EnvCommandArgs | CommitCommandArgs | SummaryCommandArgs | QuizCommandArgs | ServiceCommandArgs | LinearCommandArgs | NotionCommandArgs | OssCommandArgs | QrcodeCommandArgs | RandomCommandArgs | CollectCommandArgs | ChatRoomCommandArgs | FunctionCommandArgs | WorkerCommandArgs | CallCommandArgs | NsCommandArgs | RoleCommandArgs | AccountCommandArgs | OrgCommandArgs | StoreCommandArgs | AppCommandArgs | VercelCommandArgs | HostCommandArgs;
1147
+ type LogsCommandArgs = {
1148
+ type: Command.Logs;
1149
+ tool: "schema" | "query" | "aggregate" | "trace" | "compare";
1150
+ params: Record<string, string>;
1151
+ json: boolean;
1152
+ actorProfileId?: string;
1153
+ };
1154
+ type CommandArgs = LogsCommandArgs | NoneCommandArgs | ListCommandArgs | LoginCommandArgs | AddDnsCommandArgs | StsCommandArgs | BucketCommandArgs | DeployCommandArgs | LogCommandArgs | InitCommandArgs | DeleteCommandArgs | DevCommandArgs | EnvCommandArgs | CommitCommandArgs | SummaryCommandArgs | QuizCommandArgs | ServiceCommandArgs | LinearCommandArgs | NotionCommandArgs | OssCommandArgs | QrcodeCommandArgs | RandomCommandArgs | CollectCommandArgs | ChatRoomCommandArgs | FunctionCommandArgs | WorkerCommandArgs | CallCommandArgs | NsCommandArgs | RoleCommandArgs | AccountCommandArgs | OrgCommandArgs | StoreCommandArgs | AppCommandArgs | VercelCommandArgs | HostCommandArgs;
1147
1155
 
1148
1156
  interface ParsedArgs {
1149
1157
  help: boolean;
package/dist/index.js CHANGED
@@ -18750,13 +18750,19 @@ async function queryDeveloperLogs(input, fetcher = fetch) {
18750
18750
  if (!Number.isSafeInteger(input.page) || input.page < 0) throw new Error("Invalid log page");
18751
18751
  if (input.sinceMs !== void 0 && (!Number.isSafeInteger(input.sinceMs) || input.sinceMs < 0)) throw new Error("Invalid log time");
18752
18752
  if (input.keyword && input.keyword.length > 256) throw new Error("Log search must be at most 256 characters");
18753
- const session = readRequiredSession({ preferredApp: appRef, fallbackToActiveApp: true });
18754
- if (Date.parse(session.expiresAt) <= Date.now() || !Number.isFinite(Date.parse(session.expiresAt)))
18755
- throw new Error("Session expired. Run pmate login --app pmate:developers");
18756
- async function request2(url, init) {
18753
+ const params = new URLSearchParams({ service: input.service, page: String(input.page), topic: input.topic ?? "application" });
18754
+ if (input.environment) params.set("environment", input.environment);
18755
+ if (input.fields) params.set("fields", JSON.stringify(input.fields));
18756
+ if (input.keyword) params.set("keyword", input.keyword);
18757
+ if (input.sinceMs !== void 0) params.set("sinceMs", String(input.sinceMs));
18758
+ return requestDeveloperLogTool("query", params, fetcher);
18759
+ }
18760
+ async function requestDeveloperLogTool(tool, params, fetcher = fetch, delegate) {
18761
+ if (!["schema", "query", "aggregate", "trace", "compare"].includes(tool)) throw new Error("Unknown log tool");
18762
+ async function request2(url2, init) {
18757
18763
  let response;
18758
18764
  try {
18759
- response = await fetcher(url, { ...init, redirect: "error", signal: AbortSignal.timeout(3e4) });
18765
+ response = await fetcher(url2, { ...init, redirect: "error", signal: AbortSignal.timeout(3e4) });
18760
18766
  } catch {
18761
18767
  throw new Error("Developers log request failed; no anonymous fallback was attempted");
18762
18768
  }
@@ -18770,6 +18776,15 @@ async function queryDeveloperLogs(input, fetcher = fetch) {
18770
18776
  if (!body?.success) throw new Error("Developers API rejected the request");
18771
18777
  return body.data;
18772
18778
  }
18779
+ const suffix = tool === "query" ? "" : `/${tool}`;
18780
+ const url = `https://dev-api.pmate.chat/api/logs${suffix}?${params}`;
18781
+ if (delegate) {
18782
+ if (!delegate.token || !/^[a-zA-Z0-9_.:@-]{1,128}$/.test(delegate.profileId)) throw new Error("Invalid log delegation");
18783
+ return request2(url, { headers: { Authorization: `Bearer ${delegate.token}`, "x-pmate-actor-profile-id": delegate.profileId } });
18784
+ }
18785
+ const session = readRequiredSession({ preferredApp: appRef, fallbackToActiveApp: true });
18786
+ if (Date.parse(session.expiresAt) <= Date.now() || !Number.isFinite(Date.parse(session.expiresAt)))
18787
+ throw new Error("Session expired. Run pmate login --app pmate:developers");
18773
18788
  const exchanged = await request2("https://auth-api-v2.pmate.chat/session/exchange", {
18774
18789
  method: "POST",
18775
18790
  headers: { Authorization: `Bearer ${session.token}`, "Content-Type": "application/json" },
@@ -18777,12 +18792,7 @@ async function queryDeveloperLogs(input, fetcher = fetch) {
18777
18792
  });
18778
18793
  if (exchanged?.session?.identity?.app !== appRef || exchanged.session.identity.accountId !== session.accountId || typeof exchanged.token !== "string" || !exchanged.token)
18779
18794
  throw new Error("Developers session identity mismatch");
18780
- const params = new URLSearchParams({ service: input.service, page: String(input.page), topic: input.topic ?? "application" });
18781
- if (input.environment) params.set("environment", input.environment);
18782
- if (input.fields) params.set("fields", JSON.stringify(input.fields));
18783
- if (input.keyword) params.set("keyword", input.keyword);
18784
- if (input.sinceMs !== void 0) params.set("sinceMs", String(input.sinceMs));
18785
- return request2(`https://dev-api.pmate.chat/api/logs?${params}`, { headers: { Authorization: `Bearer ${exchanged.token}` } });
18795
+ return request2(url, { headers: { Authorization: `Bearer ${exchanged.token}` } });
18786
18796
  }
18787
18797
 
18788
18798
  // src/services/ServiceRuntimeRunner.ts
@@ -19527,7 +19537,7 @@ var ServiceRuntimeRunner = class {
19527
19537
  `git pull --rebase`,
19528
19538
  `pnpm install`,
19529
19539
  ...envSyncCommand ? [envSyncCommand] : [],
19530
- buildIfNeeded,
19540
+ `{ ${buildIfNeeded}; }`,
19531
19541
  `if [ -f "${appDir}/ecosystem.config.cjs" ]; then eco="${appDir}/ecosystem.config.cjs"; elif [ -f "${appDir}/ecosystem.config.js" ]; then eco="${appDir}/ecosystem.config.js"; else echo "Missing pm2 config: ${appDir}/ecosystem.config.cjs|.js" 1>&2; exit 2; fi`,
19532
19542
  pm2Start,
19533
19543
  pm2Reload
@@ -24744,6 +24754,74 @@ function parseServiceEnvArgs(appId, tokens) {
24744
24754
  throw new Error(`Unknown service env action: ${action}`);
24745
24755
  }
24746
24756
 
24757
+ // src/logsCommandParser.ts
24758
+ init_namespacedRef();
24759
+ function parseLogsArgs(input) {
24760
+ const rest = [...input], tool = rest.shift();
24761
+ if (!["schema", "query", "aggregate", "trace", "compare"].includes(tool)) throw Error("Usage: pmate logs schema|query|aggregate|trace|compare [ns:service] [options]");
24762
+ const params = {}, fields = {};
24763
+ let json = false, actorProfileId;
24764
+ if (rest[0] && !rest[0].startsWith("--")) params.service = parseNamespacedRef(rest.shift(), { entityLabel: "service" }).fullId;
24765
+ const keys = { "--topic": "topic", "--env": "environment", "--search": "keyword", "--group-by": "groupBy", "--interval": "interval", "--timezone": "timezone", "--request-id": "requestId", "--page": "page" };
24766
+ const times = { "--from": "from", "--to": "to", "--compare-from": "compareFrom", "--compare-to": "compareTo" };
24767
+ while (rest.length) {
24768
+ const flag = rest.shift();
24769
+ if (flag === "--json") {
24770
+ json = true;
24771
+ continue;
24772
+ }
24773
+ const value = rest.shift();
24774
+ if (value === void 0 || value.startsWith("--")) throw Error(`Missing value for ${flag}`);
24775
+ if (flag === "--actor-profile-id") {
24776
+ if (!/^[a-zA-Z0-9_.:@-]{1,128}$/.test(value)) throw Error("Invalid actor profile");
24777
+ actorProfileId = value;
24778
+ } else if (flag === "--field") {
24779
+ const match = /^([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)=(.*)$/.exec(value);
24780
+ if (!match || match[1].length > 128 || match[2].length > 256) throw Error("Use --field name=value");
24781
+ let parsed = match[2];
24782
+ try {
24783
+ parsed = JSON.parse(match[2]);
24784
+ } catch {
24785
+ }
24786
+ if (!["string", "number", "boolean"].includes(typeof parsed)) throw Error("Field must be a scalar");
24787
+ fields[match[1]] = parsed;
24788
+ if (Object.keys(fields).length > 20) throw Error("At most 20 field filters");
24789
+ } else if (times[flag]) {
24790
+ if (!/^\d+$/.test(value) && !/^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(value)) throw Error("Use Unix milliseconds or ISO time with an explicit timezone");
24791
+ const milliseconds = /^\d+$/.test(value) ? Number(value) : Date.parse(value);
24792
+ if (!Number.isSafeInteger(milliseconds) || milliseconds < 0) throw Error(`Invalid ${flag}`);
24793
+ params[times[flag]] = String(milliseconds);
24794
+ } else if (keys[flag]) params[keys[flag]] = value;
24795
+ else throw Error(`Unknown log option: ${flag}`);
24796
+ }
24797
+ if (tool !== "schema" && !params.service) throw Error("Service reference required");
24798
+ if (tool === "schema" && (Object.keys(params).length || Object.keys(fields).length)) throw Error("logs schema only accepts --json");
24799
+ if (tool === "aggregate" || tool === "compare") {
24800
+ if (!params.from || !params.to || Number(params.from) >= Number(params.to)) throw Error("An increasing --from/--to window is required");
24801
+ if (params.page || params.requestId) throw Error("Use --field requestId=value for analysis; --page only applies to query/trace");
24802
+ } else {
24803
+ if (params.groupBy || params.interval || params.timezone || params.compareFrom || params.compareTo) throw Error("Grouping and comparison options require aggregate/compare");
24804
+ if (params.from) {
24805
+ params.sinceMs = params.from;
24806
+ delete params.from;
24807
+ }
24808
+ if (params.to) {
24809
+ params.untilMs = params.to;
24810
+ delete params.to;
24811
+ }
24812
+ }
24813
+ if (tool !== "compare" && (params.compareFrom || params.compareTo)) throw Error("Comparison bounds require compare");
24814
+ if (tool === "compare" && (!params.compareFrom || !params.compareTo)) throw Error("compare requires --compare-from and --compare-to");
24815
+ if (tool === "trace" && !params.requestId) throw Error("trace requires --request-id");
24816
+ if (tool === "query" && params.requestId) {
24817
+ fields.requestId = params.requestId;
24818
+ delete params.requestId;
24819
+ }
24820
+ if (params.page !== void 0 && (!/^\d+$/.test(params.page) || !Number.isSafeInteger(Number(params.page)))) throw Error("Invalid --page");
24821
+ if (Object.keys(fields).length) params.fields = JSON.stringify(fields);
24822
+ return { type: "logs" /* Logs */, tool, params, json, ...actorProfileId ? { actorProfileId } : {} };
24823
+ }
24824
+
24747
24825
  // src/roleCommandParser.ts
24748
24826
  function parseRoleArgs(tokens) {
24749
24827
  const rest = [...tokens];
@@ -26147,6 +26225,7 @@ var HELP_TEXT = [
26147
26225
  import_chalk4.default.bold(`pmate v${pkg.version}`),
26148
26226
  "",
26149
26227
  import_chalk4.default.bold("Usage:"),
26228
+ " logs schema|query|aggregate|trace|compare [service] --from <ISO|ms> --to <ISO|ms> --topic <topic> --field name=value --json",
26150
26229
  ` ${import_chalk4.default.cyan("pmate")} ${import_chalk4.default.gray("[options]")}`,
26151
26230
  "",
26152
26231
  import_chalk4.default.bold("Options:"),
@@ -26501,6 +26580,7 @@ function parseCommandArgs(args) {
26501
26580
  if (args.rest[0] === "app") {
26502
26581
  return parseAppArgs(args.rest.slice(1));
26503
26582
  }
26583
+ if (args.rest[0] === "logs") return parseLogsArgs(args.rest.slice(1));
26504
26584
  if (args.rest[0] === "service") {
26505
26585
  return parseServiceArgs(args.rest.slice(1));
26506
26586
  }
@@ -28196,6 +28276,14 @@ async function main() {
28196
28276
  await handler.handle(commandArgs);
28197
28277
  return;
28198
28278
  }
28279
+ case "logs" /* Logs */: {
28280
+ const delegate = commandArgs.actorProfileId ? { profileId: commandArgs.actorProfileId, token: process.env.PMATE_LOG_DELEGATE_TOKEN ?? "" } : void 0;
28281
+ if (delegate && !delegate.token) throw new Error("PMATE_LOG_DELEGATE_TOKEN is required for delegated log tools");
28282
+ if (!delegate && process.env.PMATE_LOG_DELEGATE_TOKEN) throw new Error("Delegated log tools require --actor-profile-id");
28283
+ const result = await requestDeveloperLogTool(commandArgs.tool, new URLSearchParams(commandArgs.params), fetch, delegate);
28284
+ console.log(JSON.stringify(result, null, commandArgs.json ? void 0 : 2));
28285
+ return;
28286
+ }
28199
28287
  case "service" /* Service */: {
28200
28288
  const handler = new ServiceCommandHandler();
28201
28289
  await handler.handle(commandArgs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/cli",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "description": "CLI utilities for the pMate project",
5
5
  "license": "MIT",
6
6
  "bin": {