@pmate/cli 0.33.1 → 0.34.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/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.
@@ -275,3 +293,7 @@ Config (paths relative to the config file, `web.dist` relative to `web.cwd`):
275
293
  Use `--dry-run` to inspect the plan, `--dir` for an unpacked `.app`, or `--signed` to require a configured macOS signing identity. Default builds are ad-hoc signed for local use; they are not notarized distribution releases. Signed builds use electron-builder's standard signing/notarization environment variables; verify notarization separately before distributing.
276
294
 
277
295
  The shell serves the bundled renderer under its existing exact HTTPS origin, preserving authentication callbacks and API CORS. Other origins remain normal network requests. Static resources are hashed and checked at launch; missing assets return 404. The renderer is sandboxed with Node disabled and no privileged preload bridge. Sign-in, chat and inference still require internet. Updates require a new package. Each build prints its artifact directory and a resource/signing report; no build uploads or auto-updates occur.
296
+
297
+ ### 日志统计默认口径
298
+
299
+ 请求次数与日均可使用 `pmate logs aggregate pmate:issues-api --metric count --topic application --field event=http.access --interval day --timezone Asia/Shanghai --from <ISO> --to <ISO> --json`。计数由数据库完成,返回 `count`、`durationDays`、`averagePerDay`,时间桶包含零值;不返回 UV 或延迟百分位。JSON 模式失败以非零状态退出并返回固定 `error.code`,区分权限、保留期、查询预算和上游故障,避免泄露上游原始错误。
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
@@ -18743,6 +18743,30 @@ function capitalize(value) {
18743
18743
  // src/services/DeveloperLogsClient.ts
18744
18744
  init_sessionStore();
18745
18745
  init_namespacedRef();
18746
+ var DeveloperLogError = class extends Error {
18747
+ constructor(code, status, message) {
18748
+ super(message);
18749
+ this.code = code;
18750
+ this.status = status;
18751
+ }
18752
+ };
18753
+ function logResponseError(status, message) {
18754
+ const errors = {
18755
+ 400: ["logs_invalid_query", "Invalid log query; correct parameters before retrying"],
18756
+ 401: ["logs_unauthenticated", "Log session expired or invalid"],
18757
+ 403: ["logs_forbidden", "Log access denied for this sender"],
18758
+ 404: ["logs_not_found", "Requested log service or resource was not found"],
18759
+ 409: ["logs_index_changed", "Log index changed; retry the same query once"],
18760
+ 422: ["logs_query_limit", "Log query exceeded a retention or analysis limit"],
18761
+ 429: ["logs_rate_limited", "Log service rate limit reached"]
18762
+ };
18763
+ if (status === 422 && message === "Requested window extends beyond the safe retained log window")
18764
+ return new DeveloperLogError("logs_retention", status, "Requested time window is outside retained logs");
18765
+ if (status === 422 && ["Analysis exceeds 100 pages; reduce the time window", "Analysis exceeds 10000 records; reduce the time window", "Analysis budget exceeded; reduce the time window", "More than 100 groups; narrow query or choose a coarser dimension"].includes(String(message)))
18766
+ return new DeveloperLogError("logs_budget_exceeded", status, "Analysis limit exceeded; use metric=count for counts, or narrow the window/dimensions for detailed metrics");
18767
+ const [code, detail] = errors[status] ?? ["logs_unavailable", "Log service temporarily unavailable"];
18768
+ return new DeveloperLogError(code, status, `${detail} (HTTP ${status})`);
18769
+ }
18746
18770
  var appRef = "pmate:developers";
18747
18771
  async function queryDeveloperLogs(input, fetcher = fetch) {
18748
18772
  if (input.app) parseNamespacedRef(input.app, { entityLabel: "app ref" });
@@ -18750,17 +18774,30 @@ async function queryDeveloperLogs(input, fetcher = fetch) {
18750
18774
  if (!Number.isSafeInteger(input.page) || input.page < 0) throw new Error("Invalid log page");
18751
18775
  if (input.sinceMs !== void 0 && (!Number.isSafeInteger(input.sinceMs) || input.sinceMs < 0)) throw new Error("Invalid log time");
18752
18776
  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) {
18777
+ const params = new URLSearchParams({ service: input.service, page: String(input.page), topic: input.topic ?? "application" });
18778
+ if (input.environment) params.set("environment", input.environment);
18779
+ if (input.fields) params.set("fields", JSON.stringify(input.fields));
18780
+ if (input.keyword) params.set("keyword", input.keyword);
18781
+ if (input.sinceMs !== void 0) params.set("sinceMs", String(input.sinceMs));
18782
+ return requestDeveloperLogTool("query", params, fetcher);
18783
+ }
18784
+ async function requestDeveloperLogTool(tool, params, fetcher = fetch, delegate) {
18785
+ if (!["schema", "query", "aggregate", "trace", "compare"].includes(tool)) throw new Error("Unknown log tool");
18786
+ async function request2(url2, init) {
18757
18787
  let response;
18758
18788
  try {
18759
- response = await fetcher(url, { ...init, redirect: "error", signal: AbortSignal.timeout(3e4) });
18789
+ response = await fetcher(url2, { ...init, redirect: "error", signal: AbortSignal.timeout(3e4) });
18760
18790
  } catch {
18761
18791
  throw new Error("Developers log request failed; no anonymous fallback was attempted");
18762
18792
  }
18763
- if (!response.ok) throw new Error(`Developers log request failed (HTTP ${response.status}); check login, pmate:dev role`);
18793
+ if (!response.ok) {
18794
+ let detail;
18795
+ try {
18796
+ detail = (await response.json())?.message;
18797
+ } catch {
18798
+ }
18799
+ throw logResponseError(response.status, detail);
18800
+ }
18764
18801
  let body;
18765
18802
  try {
18766
18803
  body = await response.json();
@@ -18770,6 +18807,15 @@ async function queryDeveloperLogs(input, fetcher = fetch) {
18770
18807
  if (!body?.success) throw new Error("Developers API rejected the request");
18771
18808
  return body.data;
18772
18809
  }
18810
+ const suffix = tool === "query" ? "" : `/${tool}`;
18811
+ const url = `https://dev-api.pmate.chat/api/logs${suffix}?${params}`;
18812
+ if (delegate) {
18813
+ if (!delegate.token || !/^[a-zA-Z0-9_.:@-]{1,128}$/.test(delegate.profileId)) throw new Error("Invalid log delegation");
18814
+ return request2(url, { headers: { Authorization: `Bearer ${delegate.token}`, "x-pmate-actor-profile-id": delegate.profileId } });
18815
+ }
18816
+ const session = readRequiredSession({ preferredApp: appRef, fallbackToActiveApp: true });
18817
+ if (Date.parse(session.expiresAt) <= Date.now() || !Number.isFinite(Date.parse(session.expiresAt)))
18818
+ throw new Error("Session expired. Run pmate login --app pmate:developers");
18773
18819
  const exchanged = await request2("https://auth-api-v2.pmate.chat/session/exchange", {
18774
18820
  method: "POST",
18775
18821
  headers: { Authorization: `Bearer ${session.token}`, "Content-Type": "application/json" },
@@ -18777,12 +18823,7 @@ async function queryDeveloperLogs(input, fetcher = fetch) {
18777
18823
  });
18778
18824
  if (exchanged?.session?.identity?.app !== appRef || exchanged.session.identity.accountId !== session.accountId || typeof exchanged.token !== "string" || !exchanged.token)
18779
18825
  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}` } });
18826
+ return request2(url, { headers: { Authorization: `Bearer ${exchanged.token}` } });
18786
18827
  }
18787
18828
 
18788
18829
  // src/services/ServiceRuntimeRunner.ts
@@ -24744,6 +24785,75 @@ function parseServiceEnvArgs(appId, tokens) {
24744
24785
  throw new Error(`Unknown service env action: ${action}`);
24745
24786
  }
24746
24787
 
24788
+ // src/logsCommandParser.ts
24789
+ init_namespacedRef();
24790
+ function parseLogsArgs(input) {
24791
+ const rest = [...input], tool = rest.shift();
24792
+ if (!["schema", "query", "aggregate", "trace", "compare"].includes(tool)) throw Error("Usage: pmate logs schema|query|aggregate|trace|compare [ns:service] [options]");
24793
+ const params = {}, fields = {};
24794
+ let json = false, actorProfileId;
24795
+ if (rest[0] && !rest[0].startsWith("--")) params.service = parseNamespacedRef(rest.shift(), { entityLabel: "service" }).fullId;
24796
+ const keys = { "--metric": "metric", "--topic": "topic", "--env": "environment", "--search": "keyword", "--group-by": "groupBy", "--interval": "interval", "--timezone": "timezone", "--request-id": "requestId", "--page": "page" };
24797
+ const times = { "--from": "from", "--to": "to", "--compare-from": "compareFrom", "--compare-to": "compareTo" };
24798
+ while (rest.length) {
24799
+ const flag = rest.shift();
24800
+ if (flag === "--json") {
24801
+ json = true;
24802
+ continue;
24803
+ }
24804
+ const value = rest.shift();
24805
+ if (value === void 0 || value.startsWith("--")) throw Error(`Missing value for ${flag}`);
24806
+ if (flag === "--actor-profile-id") {
24807
+ if (!/^[a-zA-Z0-9_.:@-]{1,128}$/.test(value)) throw Error("Invalid actor profile");
24808
+ actorProfileId = value;
24809
+ } else if (flag === "--field") {
24810
+ const match = /^([a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)=(.*)$/.exec(value);
24811
+ if (!match || match[1].length > 128 || match[2].length > 256) throw Error("Use --field name=value");
24812
+ let parsed = match[2];
24813
+ try {
24814
+ parsed = JSON.parse(match[2]);
24815
+ } catch {
24816
+ }
24817
+ if (!["string", "number", "boolean"].includes(typeof parsed)) throw Error("Field must be a scalar");
24818
+ fields[match[1]] = parsed;
24819
+ if (Object.keys(fields).length > 20) throw Error("At most 20 field filters");
24820
+ } else if (times[flag]) {
24821
+ 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");
24822
+ const milliseconds = /^\d+$/.test(value) ? Number(value) : Date.parse(value);
24823
+ if (!Number.isSafeInteger(milliseconds) || milliseconds < 0) throw Error(`Invalid ${flag}`);
24824
+ params[times[flag]] = String(milliseconds);
24825
+ } else if (keys[flag]) params[keys[flag]] = value;
24826
+ else throw Error(`Unknown log option: ${flag}`);
24827
+ }
24828
+ if (tool !== "schema" && !params.service) throw Error("Service reference required");
24829
+ if (tool === "schema" && (Object.keys(params).length || Object.keys(fields).length)) throw Error("logs schema only accepts --json");
24830
+ if (tool === "aggregate" || tool === "compare") {
24831
+ if (params.metric && params.metric !== "count") throw Error("Supported metric: count");
24832
+ if (!params.from || !params.to || Number(params.from) >= Number(params.to)) throw Error("An increasing --from/--to window is required");
24833
+ if (params.page || params.requestId) throw Error("Use --field requestId=value for analysis; --page only applies to query/trace");
24834
+ } else {
24835
+ if (params.metric || params.groupBy || params.interval || params.timezone || params.compareFrom || params.compareTo) throw Error("Grouping and comparison options require aggregate/compare");
24836
+ if (params.from) {
24837
+ params.sinceMs = params.from;
24838
+ delete params.from;
24839
+ }
24840
+ if (params.to) {
24841
+ params.untilMs = params.to;
24842
+ delete params.to;
24843
+ }
24844
+ }
24845
+ if (tool !== "compare" && (params.compareFrom || params.compareTo)) throw Error("Comparison bounds require compare");
24846
+ if (tool === "compare" && (!params.compareFrom || !params.compareTo)) throw Error("compare requires --compare-from and --compare-to");
24847
+ if (tool === "trace" && !params.requestId) throw Error("trace requires --request-id");
24848
+ if (tool === "query" && params.requestId) {
24849
+ fields.requestId = params.requestId;
24850
+ delete params.requestId;
24851
+ }
24852
+ if (params.page !== void 0 && (!/^\d+$/.test(params.page) || !Number.isSafeInteger(Number(params.page)))) throw Error("Invalid --page");
24853
+ if (Object.keys(fields).length) params.fields = JSON.stringify(fields);
24854
+ return { type: "logs" /* Logs */, tool, params, json, ...actorProfileId ? { actorProfileId } : {} };
24855
+ }
24856
+
24747
24857
  // src/roleCommandParser.ts
24748
24858
  function parseRoleArgs(tokens) {
24749
24859
  const rest = [...tokens];
@@ -26147,6 +26257,7 @@ var HELP_TEXT = [
26147
26257
  import_chalk4.default.bold(`pmate v${pkg.version}`),
26148
26258
  "",
26149
26259
  import_chalk4.default.bold("Usage:"),
26260
+ " logs schema|query|aggregate|trace|compare [service] --from <ISO|ms> --to <ISO|ms> --topic <topic> --field name=value --json",
26150
26261
  ` ${import_chalk4.default.cyan("pmate")} ${import_chalk4.default.gray("[options]")}`,
26151
26262
  "",
26152
26263
  import_chalk4.default.bold("Options:"),
@@ -26501,6 +26612,7 @@ function parseCommandArgs(args) {
26501
26612
  if (args.rest[0] === "app") {
26502
26613
  return parseAppArgs(args.rest.slice(1));
26503
26614
  }
26615
+ if (args.rest[0] === "logs") return parseLogsArgs(args.rest.slice(1));
26504
26616
  if (args.rest[0] === "service") {
26505
26617
  return parseServiceArgs(args.rest.slice(1));
26506
26618
  }
@@ -28196,6 +28308,21 @@ async function main() {
28196
28308
  await handler.handle(commandArgs);
28197
28309
  return;
28198
28310
  }
28311
+ case "logs" /* Logs */: {
28312
+ const delegate = commandArgs.actorProfileId ? { profileId: commandArgs.actorProfileId, token: process.env.PMATE_LOG_DELEGATE_TOKEN ?? "" } : void 0;
28313
+ if (delegate && !delegate.token) throw new Error("PMATE_LOG_DELEGATE_TOKEN is required for delegated log tools");
28314
+ if (!delegate && process.env.PMATE_LOG_DELEGATE_TOKEN) throw new Error("Delegated log tools require --actor-profile-id");
28315
+ try {
28316
+ const result = await requestDeveloperLogTool(commandArgs.tool, new URLSearchParams(commandArgs.params), fetch, delegate);
28317
+ console.log(JSON.stringify(result, null, commandArgs.json ? void 0 : 2));
28318
+ } catch (error) {
28319
+ if (!commandArgs.json) throw error;
28320
+ const failure = error instanceof DeveloperLogError ? { code: error.code, status: error.status, message: error.message } : { code: "logs_unavailable", message: "Log tool failed before receiving a valid response" };
28321
+ console.log(JSON.stringify({ ok: false, error: failure }));
28322
+ process.exitCode = 1;
28323
+ }
28324
+ return;
28325
+ }
28199
28326
  case "service" /* Service */: {
28200
28327
  const handler = new ServiceCommandHandler();
28201
28328
  await handler.handle(commandArgs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/cli",
3
- "version": "0.33.1",
3
+ "version": "0.34.1",
4
4
  "description": "CLI utilities for the pMate project",
5
5
  "license": "MIT",
6
6
  "bin": {