@pmate/cli 0.25.6 → 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
@@ -48,6 +48,11 @@ pmate notion create --private --title "Private Notes" --file ./notes.md
48
48
  pmate notion update <page-id-or-url> --file ./notes.md --append
49
49
  pmate vercel projects
50
50
  pmate vercel deploys --project web --last-week
51
+ pmate im1 health
52
+ pmate im1 threads --limit 20
53
+ pmate im1 threads --search "titlte" --limit 5
54
+ pmate im1 messages --thread "<conversationId-or-thread-name>" --limit 50
55
+ pmate im1 reply --thread "<conversationId-or-thread-name>" --text "hello"
51
56
  ```
52
57
 
53
58
  Notes:
@@ -159,6 +164,38 @@ Notes:
159
164
  - Platform commands such as `pmate sts`, `pmate agent`, and `pmate app` still
160
165
  default to the `@pmate/cli` session.
161
166
 
167
+ ## IM1 / CC messages
168
+
169
+ `pmate im1` reads CC messages through the local `cc-bot` service at
170
+ `http://localhost:39223` by default. Override the base URL with
171
+ `PMATE_IM1_BASE_URL` only when debugging another local service instance.
172
+
173
+ Useful commands:
174
+
175
+ ```bash
176
+ pmate im1 health
177
+ pmate im1 threads --limit 20
178
+ pmate im1 threads --q Bug --limit 10
179
+ pmate im1 threads --search "titlte" --limit 5
180
+ pmate im1 messages --thread "<conversationId-or-thread-name>" --limit 50
181
+ pmate im1 reply --thread "<conversationId-or-thread-name>" --text "hello"
182
+ ```
183
+
184
+ `threads` and `messages` output JSON by default. Thread rows include both
185
+ `conversationId` and `threadKey`; prefer `conversationId` when scripting or
186
+ passing a thread back into `messages`/`reply`. Add `--text` for compact
187
+ human-readable output.
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
+
194
+ CC sync is owned by the background service in `~/dev/pcs/cc-bot`, which runs
195
+ under PM2, keeps TimescaleDB running, listens for new CC messages, and performs
196
+ bounded 3-day backfill. `pmate im1` is CC-only and does not accept `--source`
197
+ or custom `--endpoint` options.
198
+
162
199
  ## Notion config
163
200
 
164
201
  Add a Notion integration token to `~/.pmate/config.yaml`:
package/dist/index.d.ts CHANGED
@@ -550,23 +550,23 @@ type CollectCommandArgs = {
550
550
  } | {
551
551
  type: Command.Collect;
552
552
  action: "health";
553
+ json?: boolean;
553
554
  } | {
554
555
  type: Command.Collect;
555
556
  action: "threads";
556
- source?: "cc";
557
557
  query?: string;
558
+ search?: string;
558
559
  limit?: number;
560
+ json?: boolean;
559
561
  } | {
560
562
  type: Command.Collect;
561
563
  action: "messages";
562
- source?: "cc";
563
564
  thread: string;
564
565
  limit?: number;
566
+ json?: boolean;
565
567
  } | {
566
568
  type: Command.Collect;
567
569
  action: "reply";
568
- source?: "cc" | "yelling";
569
- endpoint?: string;
570
570
  thread: string;
571
571
  text: string;
572
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,14 +15765,24 @@ 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 });
15775
- for (const thread of threads) {
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;
15771
+ if (args.json !== false) {
15772
+ ui.plain(JSON.stringify({ ok: true, threads: outputThreads }, null, 2));
15773
+ return;
15774
+ }
15775
+ for (const thread of outputThreads) {
15776
15776
  ui.plain(formatBotThread(thread));
15777
15777
  }
15778
15778
  }
15779
15779
  async handleBotMessages(args) {
15780
15780
  const client = new Im1BotClient();
15781
15781
  const messages2 = await client.listMessages({ thread: args.thread, limit: args.limit });
15782
+ if (args.json !== false) {
15783
+ ui.plain(JSON.stringify({ ok: true, messages: messages2 }, null, 2));
15784
+ return;
15785
+ }
15782
15786
  for (const message of messages2.reverse()) {
15783
15787
  ui.plain(formatBotMessage(message));
15784
15788
  }
@@ -15790,11 +15794,7 @@ var CollectCommandHandler = class {
15790
15794
  text: args.text,
15791
15795
  dryRun: args.dryRun
15792
15796
  });
15793
- if (args.dryRun) {
15794
- ui.plain(JSON.stringify(result, null, 2));
15795
- return;
15796
- }
15797
- ui.success("im1 reply sent", { source: "cc", thread: args.thread });
15797
+ ui.plain(JSON.stringify(result, null, 2));
15798
15798
  }
15799
15799
  async handleSync(args) {
15800
15800
  let browser = null;
@@ -16175,14 +16175,98 @@ function isExternalImageRef(imagePath) {
16175
16175
  }
16176
16176
  function formatBotThread(thread) {
16177
16177
  const unread = thread.unread && thread.unread > 0 ? ` unread=${thread.unread}` : "";
16178
+ const distance = typeof thread.searchDistance === "number" ? ` distance=${thread.searchDistance}` : "";
16178
16179
  const time = thread.lastMessageAt ? ` ${thread.lastMessageAt}` : "";
16179
- const preview = thread.preview ? ` - ${thread.preview}` : "";
16180
- return `${thread.name} (${thread.conversationId})${unread}${time}${preview}`;
16180
+ const preview = thread.preview ? ` - ${truncateCliPreview(thread.preview, 120)}` : "";
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;
16181
16260
  }
16182
16261
  function formatBotMessage(message) {
16183
16262
  const sender = message.sender ? `${message.sender}: ` : "";
16184
16263
  return `[${message.sentAt}] ${sender}${message.body}`;
16185
16264
  }
16265
+ function truncateCliPreview(value, maxLength) {
16266
+ const normalized = value.replace(/\s+/g, " ").replace(/\u200B/g, "").trim();
16267
+ if (normalized.length <= maxLength) return normalized;
16268
+ return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}\u2026`;
16269
+ }
16186
16270
  function filterThreadsForSync(threads, target) {
16187
16271
  if (!target) {
16188
16272
  return threads;
@@ -25674,13 +25758,10 @@ function parseIm1Args(tokens) {
25674
25758
  const rest = [...tokens];
25675
25759
  const action = rest.shift();
25676
25760
  if (!action) {
25677
- throw new Error("Usage: im1 collect sync (--source <cc|yelling> | --endpoint <url>) | im1 reply --thread <name> --text <text> (--source <cc|yelling> | --endpoint <url>)");
25678
- }
25679
- if (action === "collect") {
25680
- 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>");
25681
25762
  }
25682
25763
  if (action === "health") {
25683
- return { type: "collect" /* Collect */, action: "health" };
25764
+ return parseIm1HealthArgs(rest);
25684
25765
  }
25685
25766
  if (action === "threads") {
25686
25767
  return parseIm1ThreadsArgs(rest);
@@ -25693,47 +25774,60 @@ function parseIm1Args(tokens) {
25693
25774
  }
25694
25775
  throw new Error(`Unknown im1 action: ${action}`);
25695
25776
  }
25777
+ function parseIm1HealthArgs(tokens) {
25778
+ const rest = [...tokens];
25779
+ let json = false;
25780
+ while (rest.length > 0) {
25781
+ const token = rest.shift();
25782
+ switch (token) {
25783
+ case "--json":
25784
+ json = true;
25785
+ break;
25786
+ default:
25787
+ throw new Error(`Unknown im1 health option: ${token}`);
25788
+ }
25789
+ }
25790
+ return { type: "collect" /* Collect */, action: "health", json };
25791
+ }
25696
25792
  function parseIm1ThreadsArgs(tokens) {
25697
25793
  const rest = [...tokens];
25698
25794
  let query;
25795
+ let search;
25699
25796
  let limit;
25700
- let source = "cc";
25797
+ let json = true;
25701
25798
  while (rest.length > 0) {
25702
25799
  const token = rest.shift();
25703
25800
  switch (token) {
25704
- case "--source": {
25705
- const value = requireValue11(rest, token);
25706
- if (value !== "cc") throw new Error(`Unsupported im1 threads source: ${value}`);
25707
- source = value;
25708
- break;
25709
- }
25710
25801
  case "--q":
25711
25802
  case "--query":
25712
25803
  query = requireValue11(rest, token);
25713
25804
  break;
25805
+ case "--search":
25806
+ search = requireValue11(rest, token);
25807
+ break;
25714
25808
  case "--limit":
25715
25809
  limit = parsePositiveInt2(requireValue11(rest, token), token);
25716
25810
  break;
25811
+ case "--json":
25812
+ json = true;
25813
+ break;
25814
+ case "--text":
25815
+ json = false;
25816
+ break;
25717
25817
  default:
25718
25818
  throw new Error(`Unknown im1 threads option: ${token}`);
25719
25819
  }
25720
25820
  }
25721
- return { type: "collect" /* Collect */, action: "threads", source, query, limit };
25821
+ return { type: "collect" /* Collect */, action: "threads", query, search, limit, json };
25722
25822
  }
25723
25823
  function parseIm1MessagesArgs(tokens) {
25724
25824
  const rest = [...tokens];
25725
25825
  let thread;
25726
25826
  let limit;
25727
- let source = "cc";
25827
+ let json = true;
25728
25828
  while (rest.length > 0) {
25729
25829
  const token = rest.shift();
25730
25830
  switch (token) {
25731
- case "--source": {
25732
- const value = requireValue11(rest, token);
25733
- if (value !== "cc") throw new Error(`Unsupported im1 messages source: ${value}`);
25734
- source = value;
25735
- break;
25736
- }
25737
25831
  case "--thread":
25738
25832
  case "--channel":
25739
25833
  thread = requireValue11(rest, token);
@@ -25741,6 +25835,12 @@ function parseIm1MessagesArgs(tokens) {
25741
25835
  case "--limit":
25742
25836
  limit = parsePositiveInt2(requireValue11(rest, token), token);
25743
25837
  break;
25838
+ case "--json":
25839
+ json = true;
25840
+ break;
25841
+ case "--text":
25842
+ json = false;
25843
+ break;
25744
25844
  default:
25745
25845
  throw new Error(`Unknown im1 messages option: ${token}`);
25746
25846
  }
@@ -25748,33 +25848,10 @@ function parseIm1MessagesArgs(tokens) {
25748
25848
  if (!thread) {
25749
25849
  throw new Error("Usage: im1 messages --thread <name|conversation-id> [--limit <n>]");
25750
25850
  }
25751
- return { type: "collect" /* Collect */, action: "messages", source, thread, limit };
25752
- }
25753
- function parseIm1CollectArgs(tokens) {
25754
- const rest = [...tokens];
25755
- const action = rest.shift();
25756
- if (action !== "sync") {
25757
- throw new Error(
25758
- "Usage: im1 collect sync [--thread <name|conversation-id>] (--source <cc|yelling> | --endpoint <url>)"
25759
- );
25760
- }
25761
- const target = parseTargetOptions(rest);
25762
- if (!target.source && !target.endpoint) {
25763
- throw new Error(
25764
- "Usage: im1 collect sync [--thread <name|conversation-id>] (--source <cc|yelling> | --endpoint <url>)"
25765
- );
25766
- }
25767
- return {
25768
- type: "collect" /* Collect */,
25769
- action: "sync",
25770
- source: target.source,
25771
- endpoint: target.endpoint,
25772
- thread: target.thread
25773
- };
25851
+ return { type: "collect" /* Collect */, action: "messages", thread, limit, json };
25774
25852
  }
25775
25853
  function parseIm1ReplyArgs(tokens) {
25776
25854
  const rest = [...tokens];
25777
- const targetTokens = [];
25778
25855
  let thread;
25779
25856
  let text;
25780
25857
  let dryRun = false;
@@ -25791,61 +25868,21 @@ function parseIm1ReplyArgs(tokens) {
25791
25868
  case "--dry-run":
25792
25869
  dryRun = true;
25793
25870
  break;
25794
- case "--source":
25795
- case "--endpoint":
25796
- targetTokens.push(token, requireValue11(rest, token));
25797
- break;
25798
25871
  default:
25799
25872
  throw new Error(`Unknown im1 reply option: ${token}`);
25800
25873
  }
25801
25874
  }
25802
25875
  if (!thread || !text) {
25803
- throw new Error("Usage: im1 reply --thread <name> --text <text> (--source <cc|yelling> | --endpoint <url>)");
25804
- }
25805
- const target = parseTargetOptions(targetTokens);
25806
- if (!target.source && !target.endpoint) {
25807
- 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>");
25808
25877
  }
25809
25878
  return {
25810
25879
  type: "collect" /* Collect */,
25811
25880
  action: "reply",
25812
- source: target.source,
25813
- endpoint: target.endpoint,
25814
25881
  thread,
25815
25882
  text,
25816
25883
  dryRun
25817
25884
  };
25818
25885
  }
25819
- function parseTargetOptions(tokens) {
25820
- const rest = [...tokens];
25821
- const options = {};
25822
- while (rest.length > 0) {
25823
- const token = rest.shift();
25824
- switch (token) {
25825
- case "--source": {
25826
- const value = requireValue11(rest, token);
25827
- if (value !== "cc" && value !== "yelling") {
25828
- throw new Error(`Unsupported im1 source: ${value}`);
25829
- }
25830
- options.source = value;
25831
- break;
25832
- }
25833
- case "--endpoint":
25834
- options.endpoint = requireValue11(rest, token);
25835
- break;
25836
- case "--thread":
25837
- case "--channel":
25838
- options.thread = requireValue11(rest, token);
25839
- break;
25840
- default:
25841
- throw new Error(`Unknown im1 option: ${token}`);
25842
- }
25843
- }
25844
- if (options.source && options.endpoint) {
25845
- throw new Error("Use either --source or --endpoint, not both.");
25846
- }
25847
- return options;
25848
- }
25849
25886
  function requireValue11(tokens, optionName) {
25850
25887
  const value = tokens.shift();
25851
25888
  if (!value || value.startsWith("--")) {
@@ -26081,11 +26118,10 @@ var HELP_TEXT = [
26081
26118
  ` ${import_chalk4.default.gray(" [--count <n>]")}`,
26082
26119
  ` ${import_chalk4.default.gray(" --easy [--words <n>] [--digits <n>] [--separator <s>] [--count <n>]")}`,
26083
26120
  ` ${import_chalk4.default.cyan("im1")} Read and reply to local CC messages through cc-bot`,
26084
- ` ${import_chalk4.default.gray(" health")}`,
26085
- ` ${import_chalk4.default.gray(" threads [--q <keyword>] [--limit <n>]")}`,
26086
- ` ${import_chalk4.default.gray(" messages --thread <name|conversation-id> [--limit <n>]")}`,
26087
- ` ${import_chalk4.default.gray(" collect sync [--thread <name|conversation-id>] (--source <cc|yelling> | --endpoint <url>)")}`,
26088
- ` ${import_chalk4.default.gray(" reply --thread <name> --text <text> (--source <cc|yelling> | --endpoint <url>)")}`,
26121
+ ` ${import_chalk4.default.gray(" health [--json]")}`,
26122
+ ` ${import_chalk4.default.gray(" threads [--q <keyword>] [--search <title>] [--limit <n>] [--json|--text]")}`,
26123
+ ` ${import_chalk4.default.gray(" messages --thread <name|conversation-id> [--limit <n>] [--json|--text]")}`,
26124
+ ` ${import_chalk4.default.gray(" reply --thread <name|conversation-id> --text <text> [--dryrun]")}`,
26089
26125
  ` ${import_chalk4.default.cyan("collect")} Legacy alias for CC/yelling collection`,
26090
26126
  ` ${import_chalk4.default.gray(" sync [--thread <name|conversation-id>] (--source <cc|yelling> | --endpoint <url>)")}`,
26091
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.6",
3
+ "version": "0.25.8",
4
4
  "description": "CLI utilities for the pMate project",
5
5
  "license": "MIT",
6
6
  "bin": {