@botcord/daemon 0.2.56 → 0.2.58

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.
@@ -15,6 +15,8 @@ export interface DiagnosticBundleResult {
15
15
  filename: string;
16
16
  sizeBytes: number;
17
17
  createdAt: string;
18
+ revealCommand: string;
19
+ copyPathCommand: string;
18
20
  }
19
21
  export interface DiagnosticUploadResult {
20
22
  bundleId: string;
@@ -189,6 +189,28 @@ function createZip(entries) {
189
189
  ]);
190
190
  return Buffer.concat([...localParts, central, end]);
191
191
  }
192
+ function shellQuote(s) {
193
+ return `'${s.replace(/'/g, `'\\''`)}'`;
194
+ }
195
+ function diagnosticBundleCommands(filePath) {
196
+ if (process.platform === "darwin") {
197
+ return {
198
+ revealCommand: `open -R ${shellQuote(filePath)}`,
199
+ copyPathCommand: `printf '%s' ${shellQuote(filePath)} | pbcopy`,
200
+ };
201
+ }
202
+ if (process.platform === "win32") {
203
+ const psPath = filePath.replace(/'/g, "''");
204
+ return {
205
+ revealCommand: `explorer.exe /select,"${filePath.replace(/"/g, '""')}"`,
206
+ copyPathCommand: `powershell.exe -NoProfile -Command "Set-Clipboard -Value '${psPath}'"`,
207
+ };
208
+ }
209
+ return {
210
+ revealCommand: `xdg-open ${shellQuote(path.dirname(filePath))}`,
211
+ copyPathCommand: `printf '%s' ${shellQuote(filePath)} | xclip -selection clipboard`,
212
+ };
213
+ }
192
214
  export async function createDiagnosticBundle(opts = {}) {
193
215
  const createdAt = new Date();
194
216
  const stamp = createdAt.toISOString().replace(/[:.]/g, "-");
@@ -238,11 +260,13 @@ export async function createDiagnosticBundle(opts = {}) {
238
260
  const zip = createZip(entries);
239
261
  const out = path.join(diagnosticsDir, filename);
240
262
  writeFileSync(out, zip, { mode: 0o600 });
263
+ const commands = diagnosticBundleCommands(out);
241
264
  return {
242
265
  path: out,
243
266
  filename,
244
267
  sizeBytes: zip.length,
245
268
  createdAt: createdAt.toISOString(),
269
+ ...commands,
246
270
  };
247
271
  }
248
272
  export async function uploadDiagnosticBundle(opts) {
@@ -18,6 +18,8 @@ export interface WechatChannelOptions {
18
18
  stateDebounceMs?: number;
19
19
  /** Test hook: override Date.now() for trace cache TTL assertions. */
20
20
  now?: () => number;
21
+ /** Test hook: override trace context cache cap without a 5000-poll test. */
22
+ traceContextMax?: number;
21
23
  }
22
24
  /**
23
25
  * WeChat (iLink Bot API) channel adapter.
@@ -53,6 +53,9 @@ export function createWechatChannel(opts) {
53
53
  const allowedSenderIds = new Set((opts.allowedSenderIds ?? []).map((s) => String(s)));
54
54
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
55
55
  const now = opts.now ?? (() => Date.now());
56
+ const traceContextMax = opts.traceContextMax && opts.traceContextMax > 0
57
+ ? opts.traceContextMax
58
+ : TRACE_CONTEXT_MAX;
56
59
  let botToken = opts.botToken;
57
60
  let stateStore = null;
58
61
  let stopCallback = null;
@@ -103,7 +106,7 @@ export function createWechatChannel(opts) {
103
106
  }
104
107
  function rememberTrace(traceId, ctx) {
105
108
  // W1: prune oldest entry by updatedAt when cap is reached.
106
- if (traceContexts.size >= TRACE_CONTEXT_MAX) {
109
+ if (traceContexts.size >= traceContextMax) {
107
110
  let oldestKey;
108
111
  let oldestAt = Infinity;
109
112
  for (const [k, v] of traceContexts) {
package/dist/index.js CHANGED
@@ -1223,6 +1223,8 @@ async function cmdDoctor(args) {
1223
1223
  }
1224
1224
  console.log(`diagnostic bundle written: ${bundle.path}`);
1225
1225
  console.log(`size: ${bundle.sizeBytes} bytes`);
1226
+ console.log(`open in Finder/file manager: ${bundle.revealCommand}`);
1227
+ console.log(`copy path to clipboard: ${bundle.copyPathCommand}`);
1226
1228
  console.log("Send this zip file to the BotCord developer/support contact.");
1227
1229
  return;
1228
1230
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/daemon",
3
- "version": "0.2.56",
3
+ "version": "0.2.58",
4
4
  "description": "BotCord local daemon — bridges Hub inbox push to local Claude Code / Codex / Gemini CLIs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,6 +25,12 @@ describe("diagnostics bundle", () => {
25
25
  });
26
26
  expect(bundle.filename).toMatch(/^botcord-daemon-diagnostics-.*\.zip$/);
27
27
  expect(bundle.path).toContain(diagnosticsDir);
28
+ if (process.platform === "linux") {
29
+ expect(bundle.revealCommand).toContain(diagnosticsDir);
30
+ } else {
31
+ expect(bundle.revealCommand).toContain(bundle.path);
32
+ }
33
+ expect(bundle.copyPathCommand).toContain(bundle.path);
28
34
  expect(existsSync(bundle.path)).toBe(true);
29
35
  const bytes = readFileSync(bundle.path);
30
36
  expect(bytes.subarray(0, 4).toString("binary")).toBe("PK\u0003\u0004");
@@ -1080,7 +1080,7 @@ describe("W1: traceContexts hard cap", () => {
1080
1080
  rmSync(tmpCap, { recursive: true, force: true });
1081
1081
  });
1082
1082
 
1083
- it("inserting 5001 entries keeps the map at <= 5000 (oldest pruned)", async () => {
1083
+ it("keeps trace context map at the configured cap (oldest pruned)", async () => {
1084
1084
  // Build an adapter with a fake clock so we can control updatedAt order.
1085
1085
  let nowMs = 1_000_000;
1086
1086
  const fetchImpl = buildFetchStub(
@@ -1088,8 +1088,9 @@ describe("W1: traceContexts hard cap", () => {
1088
1088
  {
1089
1089
  match: "getupdates",
1090
1090
  respond: (idx) => {
1091
- if (idx < 5001) {
1092
- // Each poll returns one message so we get 5001 trace entries.
1091
+ if (idx < 3) {
1092
+ // Each poll returns one message so we get one more entry than
1093
+ // the test cap without looping 5001 times in CI.
1093
1094
  nowMs += 1;
1094
1095
  return {
1095
1096
  body: {
@@ -1106,7 +1107,7 @@ describe("W1: traceContexts hard cap", () => {
1106
1107
  },
1107
1108
  };
1108
1109
  }
1109
- return { body: { ret: 0, get_updates_buf: `buf-5001`, msgs: [] } };
1110
+ return { body: { ret: 0, get_updates_buf: `buf-3`, msgs: [] } };
1110
1111
  },
1111
1112
  },
1112
1113
  ],
@@ -1121,11 +1122,12 @@ describe("W1: traceContexts hard cap", () => {
1121
1122
  stateDebounceMs: 0,
1122
1123
  allowedSenderIds: ["alice@im.wechat"],
1123
1124
  now: () => nowMs,
1125
+ traceContextMax: 2,
1124
1126
  });
1125
- const h = startAdapter(adapter, { stopAfterEnvelopes: 5001 });
1127
+ const h = startAdapter(adapter, { stopAfterEnvelopes: 3 });
1126
1128
  await h.pollDone;
1127
- // 5001 messages were accepted; the cap should have kept the map <= 5000.
1128
- expect(h.envelopes.length).toBe(5001);
1129
+ // 3 messages were accepted; the cap should have kept the map <= 2.
1130
+ expect(h.envelopes.length).toBe(3);
1129
1131
  // We can't read traceContexts directly, but we verify that the send() for
1130
1132
  // the very first trace ID now fails (it was evicted as the oldest entry).
1131
1133
  const firstTraceId = h.envelopes[0]!.message.trace!.id;
@@ -43,6 +43,8 @@ export interface DiagnosticBundleResult {
43
43
  filename: string;
44
44
  sizeBytes: number;
45
45
  createdAt: string;
46
+ revealCommand: string;
47
+ copyPathCommand: string;
46
48
  }
47
49
 
48
50
  export interface DiagnosticUploadResult {
@@ -242,6 +244,35 @@ function createZip(entries: Array<{ name: string; data: string | Buffer }>): Buf
242
244
  return Buffer.concat([...localParts, central, end]);
243
245
  }
244
246
 
247
+ function shellQuote(s: string): string {
248
+ return `'${s.replace(/'/g, `'\\''`)}'`;
249
+ }
250
+
251
+ function diagnosticBundleCommands(filePath: string): {
252
+ revealCommand: string;
253
+ copyPathCommand: string;
254
+ } {
255
+ if (process.platform === "darwin") {
256
+ return {
257
+ revealCommand: `open -R ${shellQuote(filePath)}`,
258
+ copyPathCommand: `printf '%s' ${shellQuote(filePath)} | pbcopy`,
259
+ };
260
+ }
261
+
262
+ if (process.platform === "win32") {
263
+ const psPath = filePath.replace(/'/g, "''");
264
+ return {
265
+ revealCommand: `explorer.exe /select,"${filePath.replace(/"/g, '""')}"`,
266
+ copyPathCommand: `powershell.exe -NoProfile -Command "Set-Clipboard -Value '${psPath}'"`,
267
+ };
268
+ }
269
+
270
+ return {
271
+ revealCommand: `xdg-open ${shellQuote(path.dirname(filePath))}`,
272
+ copyPathCommand: `printf '%s' ${shellQuote(filePath)} | xclip -selection clipboard`,
273
+ };
274
+ }
275
+
245
276
  export async function createDiagnosticBundle(
246
277
  opts: CreateDiagnosticBundleOptions = {},
247
278
  ): Promise<DiagnosticBundleResult> {
@@ -296,11 +327,13 @@ export async function createDiagnosticBundle(
296
327
  const zip = createZip(entries);
297
328
  const out = path.join(diagnosticsDir, filename);
298
329
  writeFileSync(out, zip, { mode: 0o600 });
330
+ const commands = diagnosticBundleCommands(out);
299
331
  return {
300
332
  path: out,
301
333
  filename,
302
334
  sizeBytes: zip.length,
303
335
  createdAt: createdAt.toISOString(),
336
+ ...commands,
304
337
  };
305
338
  }
306
339
 
@@ -65,6 +65,8 @@ export interface WechatChannelOptions {
65
65
  stateDebounceMs?: number;
66
66
  /** Test hook: override Date.now() for trace cache TTL assertions. */
67
67
  now?: () => number;
68
+ /** Test hook: override trace context cache cap without a 5000-poll test. */
69
+ traceContextMax?: number;
68
70
  }
69
71
 
70
72
  interface WechatSecret {
@@ -138,6 +140,10 @@ export function createWechatChannel(opts: WechatChannelOptions): ChannelAdapter
138
140
  const fetchImpl: FetchLike =
139
141
  opts.fetchImpl ?? ((globalThis.fetch as unknown) as FetchLike);
140
142
  const now: () => number = opts.now ?? (() => Date.now());
143
+ const traceContextMax =
144
+ opts.traceContextMax && opts.traceContextMax > 0
145
+ ? opts.traceContextMax
146
+ : TRACE_CONTEXT_MAX;
141
147
 
142
148
  let botToken: string | undefined = opts.botToken;
143
149
  let stateStore: GatewayStateStore | null = null;
@@ -195,7 +201,7 @@ export function createWechatChannel(opts: WechatChannelOptions): ChannelAdapter
195
201
 
196
202
  function rememberTrace(traceId: string, ctx: TraceContext): void {
197
203
  // W1: prune oldest entry by updatedAt when cap is reached.
198
- if (traceContexts.size >= TRACE_CONTEXT_MAX) {
204
+ if (traceContexts.size >= traceContextMax) {
199
205
  let oldestKey: string | undefined;
200
206
  let oldestAt = Infinity;
201
207
  for (const [k, v] of traceContexts) {
package/src/index.ts CHANGED
@@ -1354,6 +1354,8 @@ async function cmdDoctor(args: ParsedArgs): Promise<void> {
1354
1354
  }
1355
1355
  console.log(`diagnostic bundle written: ${bundle.path}`);
1356
1356
  console.log(`size: ${bundle.sizeBytes} bytes`);
1357
+ console.log(`open in Finder/file manager: ${bundle.revealCommand}`);
1358
+ console.log(`copy path to clipboard: ${bundle.copyPathCommand}`);
1357
1359
  console.log("Send this zip file to the BotCord developer/support contact.");
1358
1360
  return;
1359
1361
  }