@wibeco/bridge 0.2.9 → 0.2.12

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,18 +15,75 @@ import {
15
15
  observeRepositoryTransitions,
16
16
  pollDeviceToken,
17
17
  requestDeviceAuthorization,
18
+ resolveLatestPresenceSession,
18
19
  startPresenceSession,
19
20
  stopPresenceSession,
20
21
  updatePresenceSession
21
- } from "./chunk-DSMXRUNM.js";
22
+ } from "./chunk-JXH47J5I.js";
22
23
 
23
24
  // src/cli/commands.ts
24
25
  import { access, cp, mkdir, readFile, writeFile } from "fs/promises";
25
26
  import { homedir, hostname } from "os";
26
- import { join, resolve } from "path";
27
+ import { basename, join, resolve } from "path";
27
28
  import { fileURLToPath } from "url";
28
29
  import { execFile } from "child_process";
29
30
  var ADAPTERS = ["cursor", "claude-code", "codex"];
31
+ var MAX_SCREENSHOT_BYTES = 5 * 1024 * 1024;
32
+ function screenshotMimeType(bytes) {
33
+ if ([137, 80, 78, 71, 13, 10, 26, 10].every(
34
+ (byte, index) => bytes[index] === byte
35
+ )) {
36
+ return "image/png";
37
+ }
38
+ if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) {
39
+ return "image/jpeg";
40
+ }
41
+ if (new TextDecoder().decode(bytes.slice(0, 4)) === "RIFF" && new TextDecoder().decode(bytes.slice(8, 12)) === "WEBP") {
42
+ return "image/webp";
43
+ }
44
+ return void 0;
45
+ }
46
+ async function uploadProgressScreenshot(screenshotPath, altText, credential, cwd) {
47
+ try {
48
+ const resolvedPath = resolve(cwd, screenshotPath);
49
+ const bytes = await readFile(resolvedPath);
50
+ if (!bytes.length || bytes.length > MAX_SCREENSHOT_BYTES) {
51
+ return {
52
+ warning: "Screenshot skipped: image must be between 1 byte and 5 MB."
53
+ };
54
+ }
55
+ const mimeType = screenshotMimeType(bytes);
56
+ if (!mimeType) {
57
+ return {
58
+ warning: "Screenshot skipped: only PNG, JPEG, and WebP are supported."
59
+ };
60
+ }
61
+ const form = new FormData();
62
+ form.set(
63
+ "screenshot",
64
+ new Blob([new Uint8Array(bytes)], { type: mimeType }),
65
+ basename(resolvedPath)
66
+ );
67
+ form.set("alt_text", altText);
68
+ const response = await fetch(`${credential.appUrl}/api/artifacts`, {
69
+ method: "POST",
70
+ headers: { authorization: `Bearer ${credential.accessToken}` },
71
+ body: form,
72
+ signal: AbortSignal.timeout(1e4)
73
+ });
74
+ const body = await response.json().catch(() => ({}));
75
+ if (!response.ok || typeof body.artifact_id !== "string") {
76
+ return {
77
+ warning: `Screenshot skipped: ${typeof body.error === "string" ? body.error : `upload failed with status ${response.status}`}.`
78
+ };
79
+ }
80
+ return { artifactId: body.artifact_id };
81
+ } catch (error) {
82
+ return {
83
+ warning: `Screenshot skipped: ${error instanceof Error ? error.message : "upload failed"}.`
84
+ };
85
+ }
86
+ }
30
87
  async function setupCommand(requestedAdapter, options = {}) {
31
88
  const cwd = options.cwd ?? process.cwd();
32
89
  const expectedRepository = options.expectedRepository ? normalizeGitHubRepository(options.expectedRepository) : void 0;
@@ -351,6 +408,10 @@ async function shareProgressCommand(options, cwd = process.cwd()) {
351
408
  if (title && title.length > 120) {
352
409
  throw new Error("Progress title must be 120 characters or fewer.");
353
410
  }
411
+ const screenshotAlt = options.screenshotAlt?.trim() || (title ? `${title} screenshot` : "Agent update screenshot");
412
+ if (screenshotAlt.length > 240) {
413
+ throw new Error("Screenshot alt text must be 240 characters or fewer.");
414
+ }
354
415
  const phases = [
355
416
  "planning",
356
417
  "implementing",
@@ -366,14 +427,26 @@ async function shareProgressCommand(options, cwd = process.cwd()) {
366
427
  }
367
428
  const repo = await detectRepository(cwd);
368
429
  const workingTree = repo ? await detectWorkingTreeMetrics(repo.root) : void 0;
430
+ const sessionId = await resolveLatestPresenceSession(
431
+ projectConfig.adapter,
432
+ cwd
433
+ );
434
+ const screenshot = options.screenshot?.trim() ? await uploadProgressScreenshot(
435
+ options.screenshot.trim(),
436
+ screenshotAlt,
437
+ credential,
438
+ cwd
439
+ ) : {};
369
440
  const event = createHookEvent({
370
441
  source: projectConfig.adapter,
371
442
  kind: "progress.shared",
443
+ ...sessionId ? { sessionId } : {},
372
444
  metadata: {
373
445
  summary,
374
446
  ...title ? { title } : {},
375
447
  ...options.phase ? { phase: options.phase } : {},
376
448
  ...options.confidence !== void 0 ? { confidence: options.confidence } : {},
449
+ ...screenshot.artifactId ? { artifact_id: screenshot.artifactId } : {},
377
450
  ...workingTree ? {
378
451
  paths: workingTree.paths,
379
452
  lines_added: workingTree.linesAdded,
@@ -393,7 +466,7 @@ async function shareProgressCommand(options, cwd = process.cwd()) {
393
466
  }).capture(event);
394
467
  return {
395
468
  exitCode: result.error ? 1 : 0,
396
- message: result.error ? `Progress was queued; delivery failed: ${result.error}` : `Shared progress with Wibe; ${result.remaining} event(s) queued.`
469
+ message: result.error ? `Progress was queued; delivery failed: ${result.error}` : `Shared progress with Wibe; ${result.remaining} event(s) queued.${screenshot.warning ? ` ${screenshot.warning}` : ""}`
397
470
  };
398
471
  }
399
472
  async function doctorCommand(cwd = process.cwd(), requestedRepository) {
@@ -389,6 +389,7 @@ function toEnvelope(event, options) {
389
389
  paths,
390
390
  phase: event.metadata.phase,
391
391
  confidence: event.metadata.confidence,
392
+ artifact_id: typeof event.metadata.artifact_id === "string" ? event.metadata.artifact_id : void 0,
392
393
  lines_added: typeof event.metadata.lines_added === "number" ? event.metadata.lines_added : void 0,
393
394
  lines_deleted: typeof event.metadata.lines_deleted === "number" ? event.metadata.lines_deleted : void 0
394
395
  } : event.kind === "file.changed" ? {
@@ -628,7 +629,7 @@ var JsonFileOfflineQueue = class {
628
629
  // src/presence.ts
629
630
  import { createHash, randomUUID as randomUUID3 } from "crypto";
630
631
  import { spawn } from "child_process";
631
- import { mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
632
+ import { mkdir as mkdir2, readFile as readFile2, readdir, rm, writeFile as writeFile2 } from "fs/promises";
632
633
  import { homedir } from "os";
633
634
  import { join } from "path";
634
635
  var PRESENCE_HEARTBEAT_INTERVAL_MS = 45e3;
@@ -815,6 +816,35 @@ function presenceStatePath(source, sessionId, cwd = process.cwd()) {
815
816
  const key = createHash("sha256").update(`${source}\0${sessionId ?? ""}\0${cwd}`).digest("hex");
816
817
  return join(presenceDirectory(), `${key}.json`);
817
818
  }
819
+ async function resolveLatestPresenceSession(source, cwd = process.cwd(), maxAgeMs = 5 * 6e4) {
820
+ let names;
821
+ try {
822
+ names = await readdir(presenceDirectory());
823
+ } catch {
824
+ return void 0;
825
+ }
826
+ const now = Date.now();
827
+ let latest;
828
+ for (const name of names) {
829
+ if (!name.endsWith(".json")) continue;
830
+ const path = join(presenceDirectory(), name);
831
+ const state = await readPresenceState(path);
832
+ if (!state?.sessionId || state.source !== source || path !== presenceStatePath(source, state.sessionId, cwd)) {
833
+ continue;
834
+ }
835
+ const lastActivity = Date.parse(state.lastActivityAt);
836
+ if (!Number.isFinite(lastActivity) || lastActivity > now + 6e4 || now - lastActivity > maxAgeMs) {
837
+ continue;
838
+ }
839
+ if (!latest || state.lastActivityAt > latest.lastActivityAt) {
840
+ latest = {
841
+ sessionId: state.sessionId,
842
+ lastActivityAt: state.lastActivityAt
843
+ };
844
+ }
845
+ }
846
+ return latest?.sessionId;
847
+ }
818
848
  function presenceDirectory() {
819
849
  return process.env.WIBE_PRESENCE_DIR ?? join(homedir(), ".wibe", "presence");
820
850
  }
@@ -1417,6 +1447,7 @@ export {
1417
1447
  stopPresenceSession,
1418
1448
  runPresenceHeartbeat,
1419
1449
  presenceStatePath,
1450
+ resolveLatestPresenceSession,
1420
1451
  classifyHeadTransition,
1421
1452
  isObservedPush,
1422
1453
  detectRepository,
package/dist/cli.js CHANGED
@@ -6,10 +6,10 @@ import {
6
6
  setupCommand,
7
7
  shareProgressCommand,
8
8
  statusCommand
9
- } from "./chunk-6P6IVLMW.js";
9
+ } from "./chunk-7AU52DBJ.js";
10
10
  import {
11
11
  runPresenceHeartbeat
12
- } from "./chunk-DSMXRUNM.js";
12
+ } from "./chunk-JXH47J5I.js";
13
13
 
14
14
  // src/cli.ts
15
15
  var HELP = `wibe-bridge <command>
@@ -21,7 +21,7 @@ Commands:
21
21
  Use --reauthorize only to replace a rejected or revoked device token.
22
22
  status
23
23
  emit --adapter <name> --event <hook-name> (JSON payload on stdin)
24
- share-progress --summary <20-55 words> [--title <title>] [--phase <phase>] [--confidence <0-1>]
24
+ share-progress --summary <20-55 words> [--title <title>] [--phase <phase>] [--confidence <0-1>] [--screenshot <path>] [--screenshot-alt <text>]
25
25
  doctor [--repository <owner/repo>]`;
26
26
  async function main() {
27
27
  const [command, ...args] = process.argv.slice(2);
@@ -54,7 +54,9 @@ async function main() {
54
54
  summary: option(args, "--summary"),
55
55
  title: option(args, "--title"),
56
56
  phase: option(args, "--phase"),
57
- confidence: confidence === void 0 ? void 0 : Number(confidence)
57
+ confidence: confidence === void 0 ? void 0 : Number(confidence),
58
+ screenshot: option(args, "--screenshot"),
59
+ screenshotAlt: option(args, "--screenshot-alt")
58
60
  });
59
61
  } else if (command === "_presence-heartbeat") {
60
62
  parseAdapter(option(args, "--adapter"));
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  emitCommand
4
- } from "./chunk-6P6IVLMW.js";
5
- import "./chunk-DSMXRUNM.js";
4
+ } from "./chunk-7AU52DBJ.js";
5
+ import "./chunk-JXH47J5I.js";
6
6
 
7
7
  // src/codex-hook.ts
8
8
  async function main() {
package/dist/index.d.ts CHANGED
@@ -304,6 +304,7 @@ declare function updatePresenceSession(source: AgentSource, sessionId: string |
304
304
  declare function stopPresenceSession(source: AgentSource, sessionId: string | undefined, cwd?: string): Promise<PresenceMetrics | undefined>;
305
305
  declare function runPresenceHeartbeat(statePath: string, expectedInstanceId: string, emit: (source: AgentSource, eventName: string, payload: Record<string, unknown>) => Promise<unknown>, intervalMs?: number): Promise<void>;
306
306
  declare function presenceStatePath(source: AgentSource, sessionId: string | undefined, cwd?: string): string;
307
+ declare function resolveLatestPresenceSession(source: AgentSource, cwd?: string, maxAgeMs?: number): Promise<string | undefined>;
307
308
 
308
309
  interface RedactionOptions {
309
310
  allowContent?: boolean;
@@ -345,4 +346,4 @@ declare function sanitizeRemote(remote: string): string;
345
346
  declare function normalizeGitHubRepository(value: string): string | undefined;
346
347
  declare function matchesGitHubRepository(remote: string | undefined, expected: string): boolean;
347
348
 
348
- export { type AgentSource, type CanonicalHookEvent, type CredentialStore, type DeviceAuthorization, type DeviceTokenResponse, type FlushResult, type HookEventKind, JsonFileOfflineQueue, MemoryOfflineQueue, type OfflineQueue, PRESENCE_HEARTBEAT_INTERVAL_MS, type PresenceMetrics, type RedactionOptions, type RepositoryInfo, type RepositoryTransition, type SafeValue, SignedBatchClient, type SignedBatchClientOptions, type StoredCredential, SystemCredentialStore, type WorkingTreeMetrics, agentSourceSchema, canonicalHookEventSchema, classifyHeadTransition, createHookEvent, detectRepository, detectWorkingTreeMetrics, deviceAuthorizationSchema, deviceTokenResponseSchema, eventTypeForHook, hookEventKindSchema, isObservedPush, mapClaudeCodeHook, mapCodexHook, mapCursorHook, matchesGitHubRepository, normalizeGitHubRepository, observeRepositoryTransitions, pollDeviceToken, presenceStatePath, redact, requestDeviceAuthorization, runPresenceHeartbeat, safeMetadata, safeValueSchema, sanitizeRemote, startPresenceSession, stopPresenceSession, updatePresenceSession };
349
+ export { type AgentSource, type CanonicalHookEvent, type CredentialStore, type DeviceAuthorization, type DeviceTokenResponse, type FlushResult, type HookEventKind, JsonFileOfflineQueue, MemoryOfflineQueue, type OfflineQueue, PRESENCE_HEARTBEAT_INTERVAL_MS, type PresenceMetrics, type RedactionOptions, type RepositoryInfo, type RepositoryTransition, type SafeValue, SignedBatchClient, type SignedBatchClientOptions, type StoredCredential, SystemCredentialStore, type WorkingTreeMetrics, agentSourceSchema, canonicalHookEventSchema, classifyHeadTransition, createHookEvent, detectRepository, detectWorkingTreeMetrics, deviceAuthorizationSchema, deviceTokenResponseSchema, eventTypeForHook, hookEventKindSchema, isObservedPush, mapClaudeCodeHook, mapCodexHook, mapCursorHook, matchesGitHubRepository, normalizeGitHubRepository, observeRepositoryTransitions, pollDeviceToken, presenceStatePath, redact, requestDeviceAuthorization, resolveLatestPresenceSession, runPresenceHeartbeat, safeMetadata, safeValueSchema, sanitizeRemote, startPresenceSession, stopPresenceSession, updatePresenceSession };
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  presenceStatePath,
26
26
  redact,
27
27
  requestDeviceAuthorization,
28
+ resolveLatestPresenceSession,
28
29
  runPresenceHeartbeat,
29
30
  safeMetadata,
30
31
  safeValueSchema,
@@ -32,7 +33,7 @@ import {
32
33
  startPresenceSession,
33
34
  stopPresenceSession,
34
35
  updatePresenceSession
35
- } from "./chunk-DSMXRUNM.js";
36
+ } from "./chunk-JXH47J5I.js";
36
37
  export {
37
38
  JsonFileOfflineQueue,
38
39
  MemoryOfflineQueue,
@@ -60,6 +61,7 @@ export {
60
61
  presenceStatePath,
61
62
  redact,
62
63
  requestDeviceAuthorization,
64
+ resolveLatestPresenceSession,
63
65
  runPresenceHeartbeat,
64
66
  safeMetadata,
65
67
  safeValueSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wibeco/bridge",
3
- "version": "0.2.9",
3
+ "version": "0.2.12",
4
4
  "description": "Privacy-first live activity bridge for Cursor, Claude Code, and Codex.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -7,7 +7,10 @@ alwaysApply: true
7
7
 
8
8
  - This repository is connected to Wibe project `${WIBE_PROJECT_ID}`.
9
9
  - For most tasks, publish one update only after the requested outcome is complete. Do not publish plans, routine progress, or separate updates for editing, testing, committing, or pushing.
10
- - Use `wibe_share_progress` with phase `shipped` at completion. Publish earlier only with phase `blocked` when a material blocker leaves the task unfinished.
10
+ - For text-only outcomes, use `wibe_share_progress` with phase `shipped` at completion. Publish earlier only with phase `blocked` when a material blocker leaves the task unfinished.
11
11
  - Write one self-contained paragraph of 20–55 words in past tense. Lead with what now works, then explain the user or team impact and meaningful validation. Mention important limitations when present.
12
12
  - Describe the outcome, not the agent’s process. Never include first-person narration, prompts, source code, secrets, raw commands, routine tool calls, filenames, line counts, praise, hype, or speculation; Wibe displays code and file changes separately.
13
+ - Bias frontend outcomes toward visual evidence without asking for per-task approval. For UI components, pages, styling, responsive behavior, interactions, and visual fixes, attach one screenshot when the app or preview is already runnable. Prefer a screenshot already made during visual QA; otherwise capture the clearest final state.
14
+ - Skip screenshots for backend, infrastructure, documentation, refactors, and non-visual frontend logic. Never launch or repair an app solely for Wibe, and omit the image when navigation needs manual authentication, the state contains sensitive data, visual validation is blocked, or the project has visual updates disabled.
15
+ - When a screenshot is available, do not call `wibe_share_progress`. Instead run `npx --yes --package @wibeco/bridge@latest wibe share-progress --summary "<summary>" --phase <shipped|blocked> --screenshot "<path>"` so the local bridge uploads the image and publishes the outcome together.
13
16
  - If the Wibe MCP tool is unavailable, run `npx --yes --package @wibeco/bridge@latest wibe share-progress --summary "<summary>" --phase <shipped|blocked>`.