@pmate/cli 0.25.7 → 0.25.9

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
@@ -50,8 +50,9 @@ pmate vercel projects
50
50
  pmate vercel deploys --project web --last-week
51
51
  pmate im1 health
52
52
  pmate im1 threads --limit 20
53
+ pmate im1 threads --search "titlte" --limit 5
53
54
  pmate im1 messages --thread "<conversationId-or-thread-name>" --limit 50
54
- pmate im1 reply --source cc --thread "<conversationId-or-thread-name>" --text "hello"
55
+ pmate im1 reply --thread "<conversationId-or-thread-name>" --text "hello"
55
56
  ```
56
57
 
57
58
  Notes:
@@ -175,8 +176,9 @@ Useful commands:
175
176
  pmate im1 health
176
177
  pmate im1 threads --limit 20
177
178
  pmate im1 threads --q Bug --limit 10
179
+ pmate im1 threads --search "titlte" --limit 5
178
180
  pmate im1 messages --thread "<conversationId-or-thread-name>" --limit 50
179
- pmate im1 reply --source cc --thread "<conversationId-or-thread-name>" --text "hello"
181
+ pmate im1 reply --thread "<conversationId-or-thread-name>" --text "hello"
180
182
  ```
181
183
 
182
184
  `threads` and `messages` output JSON by default. Thread rows include both
@@ -184,11 +186,15 @@ pmate im1 reply --source cc --thread "<conversationId-or-thread-name>" --text "h
184
186
  passing a thread back into `messages`/`reply`. Add `--text` for compact
185
187
  human-readable output.
186
188
 
189
+ Use `pmate im1 threads --search "<title>"` for fuzzy title lookup. It compares
190
+ the lowercased search text against thread titles with edit distance, including
191
+ nearby token/window matches for typos such as `titlte` vs `title`. JSON output
192
+ includes `searchDistance` and `searchScore`.
193
+
187
194
  CC sync is owned by the background service in `~/dev/pcs/cc-bot`, which runs
188
195
  under PM2, keeps TimescaleDB running, listens for new CC messages, and performs
189
- bounded 3-day backfill. `pmate im1 collect sync --source cc` is intentionally
190
- disabled. Legacy yelling/custom endpoint collection still uses
191
- `pmate im1 collect sync --source yelling` or `--endpoint`.
196
+ bounded 3-day backfill. `pmate im1` is CC-only and does not accept `--source`
197
+ or custom `--endpoint` options.
192
198
 
193
199
  ## Notion config
194
200
 
package/dist/index.d.ts CHANGED
@@ -452,6 +452,47 @@ type AccountCommandArgs = {
452
452
  role?: "practitioner" | "mate";
453
453
  baseUrl?: string;
454
454
  json: boolean;
455
+ } | {
456
+ type: Command.Account;
457
+ action: "access-key-create";
458
+ app: string;
459
+ name: string;
460
+ namespace?: string;
461
+ scopes: string[];
462
+ expiresAt: string;
463
+ out?: string;
464
+ baseUrl?: string;
465
+ json: boolean;
466
+ } | {
467
+ type: Command.Account;
468
+ action: "access-key-list";
469
+ app?: string;
470
+ namespace?: string;
471
+ baseUrl?: string;
472
+ json: boolean;
473
+ } | {
474
+ type: Command.Account;
475
+ action: "access-key-disable";
476
+ id: string;
477
+ baseUrl?: string;
478
+ json: boolean;
479
+ } | {
480
+ type: Command.Account;
481
+ action: "access-key-rotate";
482
+ id: string;
483
+ expiresAt: string;
484
+ gracePeriodSeconds?: number;
485
+ out?: string;
486
+ baseUrl?: string;
487
+ json: boolean;
488
+ } | {
489
+ type: Command.Account;
490
+ action: "access-key-login";
491
+ app: string;
492
+ key?: string;
493
+ keyFile?: string;
494
+ baseUrl?: string;
495
+ json: boolean;
455
496
  };
456
497
  type OrgCommandArgs = {
457
498
  type: Command.Org;
@@ -554,22 +595,19 @@ type CollectCommandArgs = {
554
595
  } | {
555
596
  type: Command.Collect;
556
597
  action: "threads";
557
- source?: "cc";
558
598
  query?: string;
599
+ search?: string;
559
600
  limit?: number;
560
601
  json?: boolean;
561
602
  } | {
562
603
  type: Command.Collect;
563
604
  action: "messages";
564
- source?: "cc";
565
605
  thread: string;
566
606
  limit?: number;
567
607
  json?: boolean;
568
608
  } | {
569
609
  type: Command.Collect;
570
610
  action: "reply";
571
- source?: "cc" | "yelling";
572
- endpoint?: string;
573
611
  thread: string;
574
612
  text: string;
575
613
  dryRun: boolean;
package/dist/index.js CHANGED
@@ -15748,21 +15748,15 @@ var CollectCommandHandler = class {
15748
15748
  await this.handleBotMessages(args);
15749
15749
  return;
15750
15750
  }
15751
- if (args.source === "cc" && !args.endpoint) {
15752
- if (args.action === "sync") {
15753
- throw new Error(
15754
- 'pmate im1 collect sync has moved to cc-bot. Start ~/dev/pcs/cc-bot and use "pmate im1 threads", "pmate im1 messages", or "pmate im1 reply".'
15755
- );
15756
- }
15757
- if (args.action === "reply") {
15758
- await this.handleBotReply(args);
15759
- return;
15760
- }
15761
- }
15762
15751
  if (args.action === "reply") {
15763
- await this.handleReply(args);
15752
+ await this.handleBotReply(args);
15764
15753
  return;
15765
15754
  }
15755
+ if (args.source === "cc" && !args.endpoint) {
15756
+ throw new Error(
15757
+ 'CC sync has moved to cc-bot. Start ~/dev/pcs/cc-bot and use "pmate im1 threads", "pmate im1 messages", or "pmate im1 reply".'
15758
+ );
15759
+ }
15766
15760
  await this.handleSync(args);
15767
15761
  }
15768
15762
  async handleBotHealth() {
@@ -15771,12 +15765,14 @@ var CollectCommandHandler = class {
15771
15765
  }
15772
15766
  async handleBotThreads(args) {
15773
15767
  const client = new Im1BotClient();
15774
- const threads = await client.listThreads({ q: args.query, limit: args.limit });
15768
+ const fetchLimit = args.search ? Math.max(args.limit ?? 10, 200) : args.limit;
15769
+ const threads = await client.listThreads({ q: args.query, limit: fetchLimit });
15770
+ const outputThreads = args.search ? rankThreadsByTitleSearch(threads, args.search).slice(0, args.limit ?? 10) : threads;
15775
15771
  if (args.json !== false) {
15776
- ui.plain(JSON.stringify({ ok: true, threads }, null, 2));
15772
+ ui.plain(JSON.stringify({ ok: true, threads: outputThreads }, null, 2));
15777
15773
  return;
15778
15774
  }
15779
- for (const thread of threads) {
15775
+ for (const thread of outputThreads) {
15780
15776
  ui.plain(formatBotThread(thread));
15781
15777
  }
15782
15778
  }
@@ -16179,9 +16175,88 @@ function isExternalImageRef(imagePath) {
16179
16175
  }
16180
16176
  function formatBotThread(thread) {
16181
16177
  const unread = thread.unread && thread.unread > 0 ? ` unread=${thread.unread}` : "";
16178
+ const distance = typeof thread.searchDistance === "number" ? ` distance=${thread.searchDistance}` : "";
16182
16179
  const time = thread.lastMessageAt ? ` ${thread.lastMessageAt}` : "";
16183
16180
  const preview = thread.preview ? ` - ${truncateCliPreview(thread.preview, 120)}` : "";
16184
- return `${thread.name} (${thread.conversationId})${unread}${time}${preview}`;
16181
+ return `${thread.name} (${thread.conversationId})${unread}${distance}${time}${preview}`;
16182
+ }
16183
+ function rankThreadsByTitleSearch(threads, rawSearch) {
16184
+ const search = normalizeSearchTitle(rawSearch);
16185
+ if (!search) {
16186
+ return threads.map((thread) => ({
16187
+ ...thread,
16188
+ search: rawSearch,
16189
+ searchDistance: 0,
16190
+ searchScore: 1
16191
+ }));
16192
+ }
16193
+ return threads.map((thread) => {
16194
+ const title = normalizeSearchTitle(thread.name);
16195
+ const distance = fuzzyTitleDistance(search, title);
16196
+ const score = 1 - distance / Math.max(search.replace(/\s+/g, "").length, 1);
16197
+ return {
16198
+ ...thread,
16199
+ search: rawSearch,
16200
+ searchDistance: distance,
16201
+ searchScore: Number(Math.max(0, score).toFixed(4))
16202
+ };
16203
+ }).sort((left, right) => {
16204
+ if ((left.searchDistance ?? 0) !== (right.searchDistance ?? 0)) {
16205
+ return (left.searchDistance ?? 0) - (right.searchDistance ?? 0);
16206
+ }
16207
+ if ((left.searchScore ?? 0) !== (right.searchScore ?? 0)) {
16208
+ return (right.searchScore ?? 0) - (left.searchScore ?? 0);
16209
+ }
16210
+ return compareNullableIsoDesc(left.lastMessageAt, right.lastMessageAt);
16211
+ });
16212
+ }
16213
+ function fuzzyTitleDistance(search, title) {
16214
+ if (!search) return 0;
16215
+ if (!title) return search.length;
16216
+ if (title.includes(search)) return 0;
16217
+ const compactSearch = search.replace(/\s+/g, "");
16218
+ const compactTitle = title.replace(/\s+/g, "");
16219
+ const candidates = /* @__PURE__ */ new Set([title, compactTitle]);
16220
+ for (const token of title.split(/\s+/)) {
16221
+ if (token) candidates.add(token);
16222
+ }
16223
+ if (compactSearch && compactTitle.length >= compactSearch.length) {
16224
+ for (let index = 0; index <= compactTitle.length - compactSearch.length; index += 1) {
16225
+ candidates.add(compactTitle.slice(index, index + compactSearch.length));
16226
+ }
16227
+ }
16228
+ let best = Number.POSITIVE_INFINITY;
16229
+ for (const candidate of candidates) {
16230
+ best = Math.min(best, damerauLevenshteinDistance(compactSearch || search, candidate));
16231
+ }
16232
+ return Number.isFinite(best) ? best : search.length;
16233
+ }
16234
+ function normalizeSearchTitle(value) {
16235
+ return value.toLowerCase().normalize("NFKC").replace(/[\p{P}\p{S}]+/gu, " ").replace(/\s+/g, " ").trim();
16236
+ }
16237
+ function damerauLevenshteinDistance(left, right) {
16238
+ const matrix = Array.from({ length: left.length + 1 }, () => Array(right.length + 1).fill(0));
16239
+ for (let row = 0; row <= left.length; row += 1) matrix[row][0] = row;
16240
+ for (let column = 0; column <= right.length; column += 1) matrix[0][column] = column;
16241
+ for (let row = 1; row <= left.length; row += 1) {
16242
+ for (let column = 1; column <= right.length; column += 1) {
16243
+ const cost = left[row - 1] === right[column - 1] ? 0 : 1;
16244
+ matrix[row][column] = Math.min(
16245
+ matrix[row - 1][column] + 1,
16246
+ matrix[row][column - 1] + 1,
16247
+ matrix[row - 1][column - 1] + cost
16248
+ );
16249
+ if (row > 1 && column > 1 && left[row - 1] === right[column - 2] && left[row - 2] === right[column - 1]) {
16250
+ matrix[row][column] = Math.min(matrix[row][column], matrix[row - 2][column - 2] + 1);
16251
+ }
16252
+ }
16253
+ }
16254
+ return matrix[left.length][right.length];
16255
+ }
16256
+ function compareNullableIsoDesc(left, right) {
16257
+ const leftTime = left ? Date.parse(left) : 0;
16258
+ const rightTime = right ? Date.parse(right) : 0;
16259
+ return rightTime - leftTime;
16185
16260
  }
16186
16261
  function formatBotMessage(message) {
16187
16262
  const sender = message.sender ? `${message.sender}: ` : "";
@@ -22228,6 +22303,88 @@ var AccountCommandHandler = class {
22228
22303
  this.renderJson(profile, args.json);
22229
22304
  return;
22230
22305
  }
22306
+ case "access-key-create": {
22307
+ const token = this.readToken();
22308
+ const baseUrl = this.resolveAuthBaseUrl(args.baseUrl);
22309
+ const result = await this.request(
22310
+ baseUrl,
22311
+ token,
22312
+ "POST",
22313
+ "/access-keys",
22314
+ {
22315
+ app: args.app,
22316
+ name: args.name,
22317
+ namespace: args.namespace,
22318
+ scopes: args.scopes,
22319
+ expiresAt: args.expiresAt
22320
+ }
22321
+ );
22322
+ this.persistAccessKeySecret(result, args.out);
22323
+ this.renderAccessKeyCreateResult(result, args.json, args.out);
22324
+ return;
22325
+ }
22326
+ case "access-key-list": {
22327
+ const token = this.readToken();
22328
+ const baseUrl = this.resolveAuthBaseUrl(args.baseUrl);
22329
+ const result = await this.request(
22330
+ baseUrl,
22331
+ token,
22332
+ "GET",
22333
+ `/access-keys?${this.buildAccessKeyListQuery(args)}`
22334
+ );
22335
+ this.renderAccessKeys(result.items, args.json);
22336
+ return;
22337
+ }
22338
+ case "access-key-disable": {
22339
+ const token = this.readToken();
22340
+ const baseUrl = this.resolveAuthBaseUrl(args.baseUrl);
22341
+ const item = await this.request(
22342
+ baseUrl,
22343
+ token,
22344
+ "DELETE",
22345
+ `/access-keys/${encodeURIComponent(args.id)}`
22346
+ );
22347
+ if (!args.json) {
22348
+ ui.success(`Disabled access key ${item.id}`);
22349
+ }
22350
+ this.renderJson(item, args.json);
22351
+ return;
22352
+ }
22353
+ case "access-key-rotate": {
22354
+ const token = this.readToken();
22355
+ const baseUrl = this.resolveAuthBaseUrl(args.baseUrl);
22356
+ const result = await this.request(
22357
+ baseUrl,
22358
+ token,
22359
+ "POST",
22360
+ `/access-keys/${encodeURIComponent(args.id)}/rotate`,
22361
+ {
22362
+ expiresAt: args.expiresAt,
22363
+ gracePeriodSeconds: args.gracePeriodSeconds
22364
+ }
22365
+ );
22366
+ this.persistAccessKeySecret(result, args.out);
22367
+ this.renderAccessKeyCreateResult(result, args.json, args.out);
22368
+ return;
22369
+ }
22370
+ case "access-key-login": {
22371
+ const baseUrl = this.resolveAuthBaseUrl(args.baseUrl);
22372
+ const secret = args.key ?? this.readAccessKeyFile(args.keyFile, args.app).secret;
22373
+ const login = await this.request(
22374
+ baseUrl,
22375
+ secret,
22376
+ "POST",
22377
+ "/access-keys/session",
22378
+ { app: args.app }
22379
+ );
22380
+ const profileId = await this.resolveLoginProfileId(login, args.app, baseUrl);
22381
+ const saved = saveLoginSession({ ...login, profileId }, args.app);
22382
+ if (!args.json) {
22383
+ ui.success(`Logged in access key ${login.session.identity.accessKeyId ?? "unknown"} to ${saved.app}`);
22384
+ }
22385
+ this.renderJson(login, args.json);
22386
+ return;
22387
+ }
22231
22388
  }
22232
22389
  }
22233
22390
  resolveAuthBaseUrl(baseUrl) {
@@ -22269,6 +22426,12 @@ var AccountCommandHandler = class {
22269
22426
  if (args.namespace) params.set("namespace", args.namespace);
22270
22427
  return params.toString();
22271
22428
  }
22429
+ buildAccessKeyListQuery(args) {
22430
+ const params = new URLSearchParams();
22431
+ if (args.app) params.set("app", args.app);
22432
+ if (args.namespace) params.set("namespace", args.namespace);
22433
+ return params.toString();
22434
+ }
22272
22435
  async request(baseUrl, token, method, pathname, body) {
22273
22436
  const response = await fetch(`${baseUrl}${pathname}`, {
22274
22437
  method,
@@ -22426,6 +22589,9 @@ var AccountCommandHandler = class {
22426
22589
  resolveDefaultBotKeyPath(app, name) {
22427
22590
  return import_node_path26.default.join(this.homeDir(), ".pmate", "bots", app, `${name}.bot-key.json`);
22428
22591
  }
22592
+ resolveDefaultAccessKeyPath(app, name) {
22593
+ return import_node_path26.default.join(this.homeDir(), ".pmate", "access-keys", app, `${name}.access-key.json`);
22594
+ }
22429
22595
  homeDir() {
22430
22596
  return process.env.HOME || process.env.USERPROFILE || process.cwd();
22431
22597
  }
@@ -22462,6 +22628,89 @@ var AccountCommandHandler = class {
22462
22628
  }
22463
22629
  return parsed;
22464
22630
  }
22631
+ readAccessKeyFile(filePath, expectedApp) {
22632
+ if (!filePath) {
22633
+ throw new Error("Missing access key file");
22634
+ }
22635
+ const absolutePath = this.resolveFilePath(filePath);
22636
+ if (!(0, import_node_fs22.existsSync)(absolutePath)) {
22637
+ throw new Error(`Access key file not found: ${absolutePath}`);
22638
+ }
22639
+ const parsed = JSON.parse((0, import_node_fs22.readFileSync)(absolutePath, "utf8"));
22640
+ if (parsed.type !== "pmate-access-key" || parsed.version !== 1 || !parsed.id || !parsed.app || !parsed.secret || !parsed.expiresAt) {
22641
+ throw new Error(`Invalid PMate access key file: ${absolutePath}`);
22642
+ }
22643
+ if (expectedApp && parsed.app !== expectedApp) {
22644
+ throw new Error(`Access key app mismatch. expected=${expectedApp}, received=${parsed.app}`);
22645
+ }
22646
+ return parsed;
22647
+ }
22648
+ persistAccessKeySecret(result, out) {
22649
+ const keyFile = {
22650
+ type: "pmate-access-key",
22651
+ version: 1,
22652
+ id: result.key.id,
22653
+ app: result.key.app,
22654
+ name: result.key.name,
22655
+ namespace: result.key.namespace,
22656
+ secret: result.secret,
22657
+ expiresAt: result.key.expiresAt,
22658
+ createdAt: result.key.createdAt
22659
+ };
22660
+ const outPath = out ? this.resolveFilePath(out) : this.resolveDefaultAccessKeyPath(result.key.app, result.key.name);
22661
+ (0, import_node_fs22.mkdirSync)(import_node_path26.default.dirname(outPath), { recursive: true });
22662
+ (0, import_node_fs22.writeFileSync)(outPath, JSON.stringify(keyFile, null, 2), { mode: 384 });
22663
+ (0, import_node_fs22.chmodSync)(outPath, 384);
22664
+ }
22665
+ renderAccessKeyCreateResult(result, asJson, out) {
22666
+ if (asJson) {
22667
+ this.renderJson(result, true);
22668
+ return;
22669
+ }
22670
+ const outPath = out ? this.resolveFilePath(out) : this.resolveDefaultAccessKeyPath(result.key.app, result.key.name);
22671
+ ui.success(`Access key saved to ${outPath}`);
22672
+ ui.info(result.key.id, {
22673
+ app: result.key.app,
22674
+ name: result.key.name,
22675
+ namespace: result.key.namespace,
22676
+ scopes: result.key.scopes,
22677
+ expiresAt: result.key.expiresAt
22678
+ });
22679
+ }
22680
+ renderAccessKeys(items, asJson) {
22681
+ if (asJson) {
22682
+ this.renderJson({ items }, true);
22683
+ return;
22684
+ }
22685
+ if (items.length === 0) {
22686
+ ui.warn("No access keys found.");
22687
+ return;
22688
+ }
22689
+ for (const item of items) {
22690
+ ui.info(item.id, {
22691
+ app: item.app,
22692
+ name: item.name,
22693
+ namespace: item.namespace,
22694
+ scopes: item.scopes,
22695
+ status: item.status,
22696
+ expiresAt: item.expiresAt,
22697
+ lastUsedAt: item.lastUsedAt,
22698
+ rotationExpiresAt: item.rotationExpiresAt
22699
+ });
22700
+ }
22701
+ }
22702
+ async resolveLoginProfileId(login, app, authApiBaseUrl) {
22703
+ try {
22704
+ return await fetchDefaultProfileId({
22705
+ token: login.token,
22706
+ app,
22707
+ accountId: login.session.identity.accountId,
22708
+ authApiBaseUrl
22709
+ });
22710
+ } catch {
22711
+ return login.session.identity.accessKeyId ?? login.session.identity.accountId;
22712
+ }
22713
+ }
22465
22714
  signBotChallenge(challenge, privateKey) {
22466
22715
  const key = (0, import_node_crypto5.createPrivateKey)({
22467
22716
  key: Buffer.from(privateKey, "base64url"),
@@ -25264,6 +25513,104 @@ function parseAccountArgs(tokens) {
25264
25513
  }
25265
25514
  throw new Error(`Unknown account bot action: ${botAction}`);
25266
25515
  }
25516
+ if (action === "access-key") {
25517
+ const keyAction = rest.shift();
25518
+ if (!keyAction) {
25519
+ throw new Error("Missing account access-key action. Use: account access-key create|list|rotate|disable|login");
25520
+ }
25521
+ let app;
25522
+ let id;
25523
+ let name;
25524
+ let namespace2;
25525
+ const scopes = [];
25526
+ let expiresAt;
25527
+ let out;
25528
+ let key;
25529
+ let keyFile;
25530
+ let gracePeriodSeconds;
25531
+ let baseUrl2;
25532
+ let json2 = false;
25533
+ while (rest.length > 0) {
25534
+ const token = rest.shift();
25535
+ switch (token) {
25536
+ case "--app":
25537
+ app = parseNamespacedRef(requireValue8(rest, token), {
25538
+ entityLabel: "app ref"
25539
+ }).fullId;
25540
+ break;
25541
+ case "--name":
25542
+ name = requireValue8(rest, token);
25543
+ break;
25544
+ case "--namespace":
25545
+ namespace2 = requireValue8(rest, token);
25546
+ break;
25547
+ case "--scope":
25548
+ scopes.push(requireValue8(rest, token));
25549
+ break;
25550
+ case "--expires-at":
25551
+ expiresAt = requireValue8(rest, token);
25552
+ break;
25553
+ case "--expires-in":
25554
+ expiresAt = parseExpiresIn(requireValue8(rest, token));
25555
+ break;
25556
+ case "--grace-period":
25557
+ gracePeriodSeconds = parseDurationSeconds(requireValue8(rest, token), token);
25558
+ break;
25559
+ case "--out":
25560
+ out = requireValue8(rest, token);
25561
+ break;
25562
+ case "--key":
25563
+ key = requireValue8(rest, token);
25564
+ break;
25565
+ case "--key-file":
25566
+ keyFile = requireValue8(rest, token);
25567
+ break;
25568
+ case "--base-url":
25569
+ baseUrl2 = requireValue8(rest, token);
25570
+ break;
25571
+ case "--json":
25572
+ json2 = true;
25573
+ break;
25574
+ default:
25575
+ if ((keyAction === "rotate" || keyAction === "disable") && !token.startsWith("-")) {
25576
+ if (id) {
25577
+ throw new Error(`Unexpected account access-key id: ${token}`);
25578
+ }
25579
+ id = token;
25580
+ continue;
25581
+ }
25582
+ throw new Error(`Unknown account access-key option: ${token}`);
25583
+ }
25584
+ }
25585
+ if (keyAction === "create") {
25586
+ if (!app || !name || scopes.length === 0 || !expiresAt) {
25587
+ throw new Error("Usage: account access-key create --app <ns:app> --name <name> --scope <scope>... (--expires-at <iso> | --expires-in <duration>) [--namespace <ns>] [--out <path>] [--base-url <url>] [--json]");
25588
+ }
25589
+ return { type: "account" /* Account */, action: "access-key-create", app, name, namespace: namespace2, scopes, expiresAt, out, baseUrl: baseUrl2, json: json2 };
25590
+ }
25591
+ if (keyAction === "list") {
25592
+ return { type: "account" /* Account */, action: "access-key-list", app, namespace: namespace2, baseUrl: baseUrl2, json: json2 };
25593
+ }
25594
+ if (keyAction === "disable") {
25595
+ if (!id) {
25596
+ throw new Error("Usage: account access-key disable <id> [--base-url <url>] [--json]");
25597
+ }
25598
+ return { type: "account" /* Account */, action: "access-key-disable", id, baseUrl: baseUrl2, json: json2 };
25599
+ }
25600
+ if (keyAction === "rotate") {
25601
+ if (!id || !expiresAt) {
25602
+ throw new Error("Usage: account access-key rotate <id> (--expires-at <iso> | --expires-in <duration>) [--grace-period <duration>] [--out <path>] [--base-url <url>] [--json]");
25603
+ }
25604
+ return { type: "account" /* Account */, action: "access-key-rotate", id, expiresAt, gracePeriodSeconds, out, baseUrl: baseUrl2, json: json2 };
25605
+ }
25606
+ if (keyAction === "login") {
25607
+ if (!app || !key && !keyFile || key && keyFile) {
25608
+ throw new Error("Usage: account access-key login --app <ns:app> (--key <secret> | --key-file <path>) [--base-url <url>] [--json]");
25609
+ }
25610
+ return { type: "account" /* Account */, action: "access-key-login", app, key, keyFile, baseUrl: baseUrl2, json: json2 };
25611
+ }
25612
+ throw new Error(`Unknown account access-key action: ${keyAction}`);
25613
+ }
25267
25614
  if (action !== "role") {
25268
25615
  throw new Error(`Unknown account action: ${action}`);
25269
25616
  }
@@ -25345,6 +25692,19 @@ function parsePositiveInt(value, optionName) {
25345
25692
  }
25346
25693
  return parsed;
25347
25694
  }
25695
+ function parseExpiresIn(value) {
25696
+ return new Date(Date.now() + parseDurationSeconds(value, "--expires-in") * 1e3).toISOString();
25697
+ }
25698
+ function parseDurationSeconds(value, optionName) {
25699
+ const match = /^(\d+)(s|m|h|d)?$/.exec(value.trim());
25700
+ if (!match) {
25701
+ throw new Error(`${optionName} must be a duration like 3600s, 60m, 24h, or 30d`);
25702
+ }
25703
+ const amount = Number(match[1]);
25704
+ const unit = match[2] ?? "s";
25705
+ const multiplier = unit === "d" ? 86400 : unit === "h" ? 3600 : unit === "m" ? 60 : 1;
25706
+ return amount * multiplier;
25707
+ }
25348
25708
 
25349
25709
  // src/orgCommandParser.ts
25350
25710
  init_namespacedRef();
@@ -25683,10 +26043,7 @@ function parseIm1Args(tokens) {
25683
26043
  const rest = [...tokens];
25684
26044
  const action = rest.shift();
25685
26045
  if (!action) {
25686
- throw new Error("Usage: im1 collect sync (--source <cc|yelling> | --endpoint <url>) | im1 reply --thread <name> --text <text> (--source <cc|yelling> | --endpoint <url>)");
25687
- }
25688
- if (action === "collect") {
25689
- return parseIm1CollectArgs(rest);
26046
+ throw new Error("Usage: im1 health | im1 threads [--q <keyword>] [--search <title>] | im1 messages --thread <name|conversation-id> | im1 reply --thread <name|conversation-id> --text <text>");
25690
26047
  }
25691
26048
  if (action === "health") {
25692
26049
  return parseIm1HealthArgs(rest);
@@ -25720,22 +26077,19 @@ function parseIm1HealthArgs(tokens) {
25720
26077
  function parseIm1ThreadsArgs(tokens) {
25721
26078
  const rest = [...tokens];
25722
26079
  let query;
26080
+ let search;
25723
26081
  let limit;
25724
- let source = "cc";
25725
26082
  let json = true;
25726
26083
  while (rest.length > 0) {
25727
26084
  const token = rest.shift();
25728
26085
  switch (token) {
25729
- case "--source": {
25730
- const value = requireValue11(rest, token);
25731
- if (value !== "cc") throw new Error(`Unsupported im1 threads source: ${value}`);
25732
- source = value;
25733
- break;
25734
- }
25735
26086
  case "--q":
25736
26087
  case "--query":
25737
26088
  query = requireValue11(rest, token);
25738
26089
  break;
26090
+ case "--search":
26091
+ search = requireValue11(rest, token);
26092
+ break;
25739
26093
  case "--limit":
25740
26094
  limit = parsePositiveInt2(requireValue11(rest, token), token);
25741
26095
  break;
@@ -25749,23 +26103,16 @@ function parseIm1ThreadsArgs(tokens) {
25749
26103
  throw new Error(`Unknown im1 threads option: ${token}`);
25750
26104
  }
25751
26105
  }
25752
- return { type: "collect" /* Collect */, action: "threads", source, query, limit, json };
26106
+ return { type: "collect" /* Collect */, action: "threads", query, search, limit, json };
25753
26107
  }
25754
26108
  function parseIm1MessagesArgs(tokens) {
25755
26109
  const rest = [...tokens];
25756
26110
  let thread;
25757
26111
  let limit;
25758
- let source = "cc";
25759
26112
  let json = true;
25760
26113
  while (rest.length > 0) {
25761
26114
  const token = rest.shift();
25762
26115
  switch (token) {
25763
- case "--source": {
25764
- const value = requireValue11(rest, token);
25765
- if (value !== "cc") throw new Error(`Unsupported im1 messages source: ${value}`);
25766
- source = value;
25767
- break;
25768
- }
25769
26116
  case "--thread":
25770
26117
  case "--channel":
25771
26118
  thread = requireValue11(rest, token);
@@ -25786,33 +26133,10 @@ function parseIm1MessagesArgs(tokens) {
25786
26133
  if (!thread) {
25787
26134
  throw new Error("Usage: im1 messages --thread <name|conversation-id> [--limit <n>]");
25788
26135
  }
25789
- return { type: "collect" /* Collect */, action: "messages", source, thread, limit, json };
25790
- }
25791
- function parseIm1CollectArgs(tokens) {
25792
- const rest = [...tokens];
25793
- const action = rest.shift();
25794
- if (action !== "sync") {
25795
- throw new Error(
25796
- "Usage: im1 collect sync [--thread <name|conversation-id>] (--source <cc|yelling> | --endpoint <url>)"
25797
- );
25798
- }
25799
- const target = parseTargetOptions(rest);
25800
- if (!target.source && !target.endpoint) {
25801
- throw new Error(
25802
- "Usage: im1 collect sync [--thread <name|conversation-id>] (--source <cc|yelling> | --endpoint <url>)"
25803
- );
25804
- }
25805
- return {
25806
- type: "collect" /* Collect */,
25807
- action: "sync",
25808
- source: target.source,
25809
- endpoint: target.endpoint,
25810
- thread: target.thread
25811
- };
26136
+ return { type: "collect" /* Collect */, action: "messages", thread, limit, json };
25812
26137
  }
25813
26138
  function parseIm1ReplyArgs(tokens) {
25814
26139
  const rest = [...tokens];
25815
- const targetTokens = [];
25816
26140
  let thread;
25817
26141
  let text;
25818
26142
  let dryRun = false;
@@ -25829,61 +26153,21 @@ function parseIm1ReplyArgs(tokens) {
25829
26153
  case "--dry-run":
25830
26154
  dryRun = true;
25831
26155
  break;
25832
- case "--source":
25833
- case "--endpoint":
25834
- targetTokens.push(token, requireValue11(rest, token));
25835
- break;
25836
26156
  default:
25837
26157
  throw new Error(`Unknown im1 reply option: ${token}`);
25838
26158
  }
25839
26159
  }
25840
26160
  if (!thread || !text) {
25841
- throw new Error("Usage: im1 reply --thread <name> --text <text> (--source <cc|yelling> | --endpoint <url>)");
25842
- }
25843
- const target = parseTargetOptions(targetTokens);
25844
- if (!target.source && !target.endpoint) {
25845
- throw new Error("Usage: im1 reply --thread <name> --text <text> (--source <cc|yelling> | --endpoint <url>)");
26161
+ throw new Error("Usage: im1 reply --thread <name|conversation-id> --text <text>");
25846
26162
  }
25847
26163
  return {
25848
26164
  type: "collect" /* Collect */,
25849
26165
  action: "reply",
25850
- source: target.source,
25851
- endpoint: target.endpoint,
25852
26166
  thread,
25853
26167
  text,
25854
26168
  dryRun
25855
26169
  };
25856
26170
  }
25857
- function parseTargetOptions(tokens) {
25858
- const rest = [...tokens];
25859
- const options = {};
25860
- while (rest.length > 0) {
25861
- const token = rest.shift();
25862
- switch (token) {
25863
- case "--source": {
25864
- const value = requireValue11(rest, token);
25865
- if (value !== "cc" && value !== "yelling") {
25866
- throw new Error(`Unsupported im1 source: ${value}`);
25867
- }
25868
- options.source = value;
25869
- break;
25870
- }
25871
- case "--endpoint":
25872
- options.endpoint = requireValue11(rest, token);
25873
- break;
25874
- case "--thread":
25875
- case "--channel":
25876
- options.thread = requireValue11(rest, token);
25877
- break;
25878
- default:
25879
- throw new Error(`Unknown im1 option: ${token}`);
25880
- }
25881
- }
25882
- if (options.source && options.endpoint) {
25883
- throw new Error("Use either --source or --endpoint, not both.");
25884
- }
25885
- return options;
25886
- }
25887
26171
  function requireValue11(tokens, optionName) {
25888
26172
  const value = tokens.shift();
25889
26173
  if (!value || value.startsWith("--")) {
@@ -26120,10 +26404,9 @@ var HELP_TEXT = [
26120
26404
  ` ${import_chalk4.default.gray(" --easy [--words <n>] [--digits <n>] [--separator <s>] [--count <n>]")}`,
26121
26405
  ` ${import_chalk4.default.cyan("im1")} Read and reply to local CC messages through cc-bot`,
26122
26406
  ` ${import_chalk4.default.gray(" health [--json]")}`,
26123
- ` ${import_chalk4.default.gray(" threads [--q <keyword>] [--limit <n>] [--json|--text]")}`,
26407
+ ` ${import_chalk4.default.gray(" threads [--q <keyword>] [--search <title>] [--limit <n>] [--json|--text]")}`,
26124
26408
  ` ${import_chalk4.default.gray(" messages --thread <name|conversation-id> [--limit <n>] [--json|--text]")}`,
26125
- ` ${import_chalk4.default.gray(" collect sync [--thread <name|conversation-id>] (--source <cc|yelling> | --endpoint <url>)")}`,
26126
- ` ${import_chalk4.default.gray(" reply --thread <name> --text <text> (--source <cc|yelling> | --endpoint <url>)")}`,
26409
+ ` ${import_chalk4.default.gray(" reply --thread <name|conversation-id> --text <text> [--dryrun]")}`,
26127
26410
  ` ${import_chalk4.default.cyan("collect")} Legacy alias for CC/yelling collection`,
26128
26411
  ` ${import_chalk4.default.gray(" sync [--thread <name|conversation-id>] (--source <cc|yelling> | --endpoint <url>)")}`,
26129
26412
  ` ${import_chalk4.default.cyan("chat-room")} Run social chat regression commands with PMate bot key JSON`,
@@ -26175,6 +26458,11 @@ var HELP_TEXT = [
26175
26458
  ` ${import_chalk4.default.gray(" bot list --app <ns:app> [--base-url <url>] [--json]")}`,
26176
26459
  ` ${import_chalk4.default.gray(" bot login --app <ns:app> --name <name> (--key-file <path> | --private-key <key>) [--base-url <url>] [--json]")}`,
26177
26460
  ` ${import_chalk4.default.gray(" bot profile create --app <ns:app> --name <name> --nickname <nickname> [--role <role>] [--base-url <url>] [--json]")}`,
26461
+ ` ${import_chalk4.default.gray(" access-key create --app <ns:app> --name <name> --scope <scope>... (--expires-at <iso> | --expires-in <duration>) [--namespace <ns>] [--out <path>] [--base-url <url>] [--json]")}`,
26462
+ ` ${import_chalk4.default.gray(" access-key list [--app <ns:app>] [--namespace <ns>] [--base-url <url>] [--json]")}`,
26463
+ ` ${import_chalk4.default.gray(" access-key rotate <id> (--expires-at <iso> | --expires-in <duration>) [--grace-period <duration>] [--out <path>] [--base-url <url>] [--json]")}`,
26464
+ ` ${import_chalk4.default.gray(" access-key disable <id> [--base-url <url>] [--json]")}`,
26465
+ ` ${import_chalk4.default.gray(" access-key login --app <ns:app> (--key <secret> | --key-file <path>) [--base-url <url>] [--json]")}`,
26178
26466
  ` ${import_chalk4.default.gray(" default base URL: https://auth-api-v2.pmate.chat")}`,
26179
26467
  ` ${import_chalk4.default.cyan("org")} Manage orgs, org members, and org visible apps via apps-api`,
26180
26468
  ` ${import_chalk4.default.gray(" list [--base-url <url>] [--json]")}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/cli",
3
- "version": "0.25.7",
3
+ "version": "0.25.9",
4
4
  "description": "CLI utilities for the pMate project",
5
5
  "license": "MIT",
6
6
  "bin": {