@pmate/cli 0.25.7 → 0.25.8

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
@@ -554,22 +554,19 @@ type CollectCommandArgs = {
554
554
  } | {
555
555
  type: Command.Collect;
556
556
  action: "threads";
557
- source?: "cc";
558
557
  query?: string;
558
+ search?: string;
559
559
  limit?: number;
560
560
  json?: boolean;
561
561
  } | {
562
562
  type: Command.Collect;
563
563
  action: "messages";
564
- source?: "cc";
565
564
  thread: string;
566
565
  limit?: number;
567
566
  json?: boolean;
568
567
  } | {
569
568
  type: Command.Collect;
570
569
  action: "reply";
571
- source?: "cc" | "yelling";
572
- endpoint?: string;
573
570
  thread: string;
574
571
  text: string;
575
572
  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}: ` : "";
@@ -25683,10 +25758,7 @@ function parseIm1Args(tokens) {
25683
25758
  const rest = [...tokens];
25684
25759
  const action = rest.shift();
25685
25760
  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);
25761
+ 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
25762
  }
25691
25763
  if (action === "health") {
25692
25764
  return parseIm1HealthArgs(rest);
@@ -25720,22 +25792,19 @@ function parseIm1HealthArgs(tokens) {
25720
25792
  function parseIm1ThreadsArgs(tokens) {
25721
25793
  const rest = [...tokens];
25722
25794
  let query;
25795
+ let search;
25723
25796
  let limit;
25724
- let source = "cc";
25725
25797
  let json = true;
25726
25798
  while (rest.length > 0) {
25727
25799
  const token = rest.shift();
25728
25800
  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
25801
  case "--q":
25736
25802
  case "--query":
25737
25803
  query = requireValue11(rest, token);
25738
25804
  break;
25805
+ case "--search":
25806
+ search = requireValue11(rest, token);
25807
+ break;
25739
25808
  case "--limit":
25740
25809
  limit = parsePositiveInt2(requireValue11(rest, token), token);
25741
25810
  break;
@@ -25749,23 +25818,16 @@ function parseIm1ThreadsArgs(tokens) {
25749
25818
  throw new Error(`Unknown im1 threads option: ${token}`);
25750
25819
  }
25751
25820
  }
25752
- return { type: "collect" /* Collect */, action: "threads", source, query, limit, json };
25821
+ return { type: "collect" /* Collect */, action: "threads", query, search, limit, json };
25753
25822
  }
25754
25823
  function parseIm1MessagesArgs(tokens) {
25755
25824
  const rest = [...tokens];
25756
25825
  let thread;
25757
25826
  let limit;
25758
- let source = "cc";
25759
25827
  let json = true;
25760
25828
  while (rest.length > 0) {
25761
25829
  const token = rest.shift();
25762
25830
  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
25831
  case "--thread":
25770
25832
  case "--channel":
25771
25833
  thread = requireValue11(rest, token);
@@ -25786,33 +25848,10 @@ function parseIm1MessagesArgs(tokens) {
25786
25848
  if (!thread) {
25787
25849
  throw new Error("Usage: im1 messages --thread <name|conversation-id> [--limit <n>]");
25788
25850
  }
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
- };
25851
+ return { type: "collect" /* Collect */, action: "messages", thread, limit, json };
25812
25852
  }
25813
25853
  function parseIm1ReplyArgs(tokens) {
25814
25854
  const rest = [...tokens];
25815
- const targetTokens = [];
25816
25855
  let thread;
25817
25856
  let text;
25818
25857
  let dryRun = false;
@@ -25829,61 +25868,21 @@ function parseIm1ReplyArgs(tokens) {
25829
25868
  case "--dry-run":
25830
25869
  dryRun = true;
25831
25870
  break;
25832
- case "--source":
25833
- case "--endpoint":
25834
- targetTokens.push(token, requireValue11(rest, token));
25835
- break;
25836
25871
  default:
25837
25872
  throw new Error(`Unknown im1 reply option: ${token}`);
25838
25873
  }
25839
25874
  }
25840
25875
  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>)");
25876
+ throw new Error("Usage: im1 reply --thread <name|conversation-id> --text <text>");
25846
25877
  }
25847
25878
  return {
25848
25879
  type: "collect" /* Collect */,
25849
25880
  action: "reply",
25850
- source: target.source,
25851
- endpoint: target.endpoint,
25852
25881
  thread,
25853
25882
  text,
25854
25883
  dryRun
25855
25884
  };
25856
25885
  }
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
25886
  function requireValue11(tokens, optionName) {
25888
25887
  const value = tokens.shift();
25889
25888
  if (!value || value.startsWith("--")) {
@@ -26120,10 +26119,9 @@ var HELP_TEXT = [
26120
26119
  ` ${import_chalk4.default.gray(" --easy [--words <n>] [--digits <n>] [--separator <s>] [--count <n>]")}`,
26121
26120
  ` ${import_chalk4.default.cyan("im1")} Read and reply to local CC messages through cc-bot`,
26122
26121
  ` ${import_chalk4.default.gray(" health [--json]")}`,
26123
- ` ${import_chalk4.default.gray(" threads [--q <keyword>] [--limit <n>] [--json|--text]")}`,
26122
+ ` ${import_chalk4.default.gray(" threads [--q <keyword>] [--search <title>] [--limit <n>] [--json|--text]")}`,
26124
26123
  ` ${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>)")}`,
26124
+ ` ${import_chalk4.default.gray(" reply --thread <name|conversation-id> --text <text> [--dryrun]")}`,
26127
26125
  ` ${import_chalk4.default.cyan("collect")} Legacy alias for CC/yelling collection`,
26128
26126
  ` ${import_chalk4.default.gray(" sync [--thread <name|conversation-id>] (--source <cc|yelling> | --endpoint <url>)")}`,
26129
26127
  ` ${import_chalk4.default.cyan("chat-room")} Run social chat regression commands with PMate bot key 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.8",
4
4
  "description": "CLI utilities for the pMate project",
5
5
  "license": "MIT",
6
6
  "bin": {