yaver-feedback-react-native 0.9.2 → 0.9.3
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 +102 -1
- package/dist/AuthOverlay.d.ts +1 -14
- package/dist/AuthOverlay.js +9 -62
- package/dist/Discovery.js +0 -6
- package/dist/DogfoodRuntime.d.ts +124 -0
- package/dist/DogfoodRuntime.js +273 -0
- package/dist/FeedbackModal.js +347 -61
- package/dist/LoginScreen.d.ts +1 -5
- package/dist/LoginScreen.js +2 -6
- package/dist/MachinePickerScreen.d.ts +1 -3
- package/dist/MachinePickerScreen.js +6 -18
- package/dist/P2PClient.d.ts +116 -3
- package/dist/P2PClient.js +273 -3
- package/dist/P2PDogfoodDriver.d.ts +12 -0
- package/dist/P2PDogfoodDriver.js +118 -0
- package/dist/PairDeviceModal.d.ts +2 -3
- package/dist/VibeChatScreen.d.ts +11 -1
- package/dist/VibeChatScreen.js +200 -41
- package/dist/YaverFeedback.d.ts +34 -0
- package/dist/YaverFeedback.js +124 -19
- package/dist/YaverModeBadge.d.ts +22 -0
- package/dist/YaverModeBadge.js +219 -0
- package/dist/__tests__/AuthDevices.test.js +1 -48
- package/dist/__tests__/DogfoodRuntime.test.d.ts +1 -0
- package/dist/__tests__/DogfoodRuntime.test.js +116 -0
- package/dist/__tests__/P2PDogfoodDriver.test.d.ts +1 -0
- package/dist/__tests__/P2PDogfoodDriver.test.js +64 -0
- package/dist/__tests__/ReportIdentity.test.d.ts +25 -1
- package/dist/__tests__/ReportIdentity.test.js +34 -22
- package/dist/__tests__/YaverFeedback.test.js +13 -3
- package/dist/__tests__/deviceDogfood.test.d.ts +1 -0
- package/dist/__tests__/deviceDogfood.test.js +86 -0
- package/dist/__tests__/dogfoodPolicy.test.d.ts +1 -0
- package/dist/__tests__/dogfoodPolicy.test.js +33 -0
- package/dist/_core/ansi.d.ts +117 -0
- package/dist/_core/ansi.js +468 -0
- package/dist/_core/ansi.test.d.ts +1 -0
- package/dist/_core/ansi.test.js +225 -0
- package/dist/_core/buildFeedbackPrompt.d.ts +4 -9
- package/dist/_core/buildFeedbackPrompt.js +18 -72
- package/dist/_core/constants.d.ts +19 -6
- package/dist/_core/constants.js +20 -7
- package/dist/_core/device.d.ts +9 -23
- package/dist/_core/device.js +13 -28
- package/dist/_core/endpoints.d.ts +0 -7
- package/dist/_core/endpoints.js +0 -7
- package/dist/_core/index.d.ts +4 -0
- package/dist/_core/index.js +4 -0
- package/dist/_core/remoteless.d.ts +44 -0
- package/dist/_core/remoteless.js +75 -0
- package/dist/_core/trace.d.ts +47 -0
- package/dist/_core/trace.js +38 -0
- package/dist/_core/trace.test.d.ts +1 -0
- package/dist/_core/trace.test.js +60 -0
- package/dist/auth.d.ts +3 -60
- package/dist/auth.js +6 -88
- package/dist/deviceDogfood.d.ts +53 -0
- package/dist/deviceDogfood.js +137 -0
- package/dist/dogfoodPolicy.d.ts +25 -0
- package/dist/dogfoodPolicy.js +24 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.js +24 -8
- package/dist/reloadActions.js +2 -2
- package/dist/types.d.ts +50 -7
- package/package.json +12 -3
- package/src/AuthOverlay.tsx +20 -106
- package/src/Discovery.ts +0 -6
- package/src/DogfoodRuntime.ts +373 -0
- package/src/FeedbackModal.tsx +445 -67
- package/src/LoginScreen.tsx +2 -22
- package/src/MachinePickerScreen.tsx +6 -21
- package/src/P2PClient.ts +347 -4
- package/src/P2PDogfoodDriver.ts +132 -0
- package/src/PairDeviceModal.tsx +2 -3
- package/src/VibeChatScreen.tsx +232 -42
- package/src/YaverFeedback.ts +133 -22
- package/src/YaverModeBadge.tsx +234 -0
- package/src/__tests__/AuthDevices.test.ts +1 -52
- package/src/__tests__/DogfoodRuntime.test.ts +135 -0
- package/src/__tests__/P2PDogfoodDriver.test.ts +80 -0
- package/src/__tests__/ReportIdentity.test.ts +36 -27
- package/src/__tests__/YaverFeedback.test.ts +15 -3
- package/src/__tests__/deviceDogfood.test.ts +77 -0
- package/src/__tests__/dogfoodPolicy.test.ts +34 -0
- package/src/_core/ansi.test.ts +250 -0
- package/src/_core/ansi.ts +475 -0
- package/src/_core/buildFeedbackPrompt.ts +18 -95
- package/src/_core/constants.ts +20 -6
- package/src/_core/device.ts +13 -30
- package/src/_core/endpoints.ts +0 -7
- package/src/_core/index.ts +4 -0
- package/src/_core/remoteless.ts +110 -0
- package/src/_core/trace.test.ts +58 -0
- package/src/_core/trace.ts +75 -0
- package/src/auth.ts +7 -156
- package/src/deviceDogfood.ts +171 -0
- package/src/dogfoodPolicy.ts +40 -0
- package/src/index.ts +40 -11
- package/src/reloadActions.ts +2 -2
- package/src/types.ts +45 -7
- package/dist/GuestOnboardingScreen.d.ts +0 -8
- package/dist/GuestOnboardingScreen.js +0 -282
- package/src/GuestOnboardingScreen.tsx +0 -307
package/src/_core/device.ts
CHANGED
|
@@ -41,10 +41,6 @@ export interface CoreDevice {
|
|
|
41
41
|
runnerDown: boolean;
|
|
42
42
|
/** Unix ms of the latest heartbeat the agent sent to Convex. */
|
|
43
43
|
lastHeartbeat: number;
|
|
44
|
-
isGuest: boolean;
|
|
45
|
-
hostName?: string;
|
|
46
|
-
hostEmail?: string;
|
|
47
|
-
accessScope?: 'owner' | 'shared-scoped' | 'shared-legacy';
|
|
48
44
|
/** Primary LAN IP (or tunnel host) the agent advertised. */
|
|
49
45
|
quicHost: string;
|
|
50
46
|
quicPort: number;
|
|
@@ -75,10 +71,6 @@ function normHost(host: string | undefined): string {
|
|
|
75
71
|
export function deviceIdentityKey(d: CoreDevice): string {
|
|
76
72
|
if (d.hwid) return `hwid:${d.hwid}`;
|
|
77
73
|
if (d.publicKey) return `pub:${d.publicKey}`;
|
|
78
|
-
if (d.isGuest) {
|
|
79
|
-
const scope = d.hostEmail || d.hostName || 'guest';
|
|
80
|
-
return `guest:${scope}:${d.deviceId || d.name}`;
|
|
81
|
-
}
|
|
82
74
|
const n = normName(d.name);
|
|
83
75
|
const os = String(d.platform || '').trim().toLowerCase();
|
|
84
76
|
if (n && os) return `host:${os}:${n}`;
|
|
@@ -87,7 +79,6 @@ export function deviceIdentityKey(d: CoreDevice): string {
|
|
|
87
79
|
}
|
|
88
80
|
|
|
89
81
|
export function deviceAliasKey(d: CoreDevice): string | null {
|
|
90
|
-
if (d.isGuest) return null;
|
|
91
82
|
const n = normName(d.name);
|
|
92
83
|
const os = String(d.platform || '').trim().toLowerCase();
|
|
93
84
|
if (!n || !os) return null;
|
|
@@ -95,7 +86,6 @@ export function deviceAliasKey(d: CoreDevice): string | null {
|
|
|
95
86
|
}
|
|
96
87
|
|
|
97
88
|
export function deviceEndpointKey(d: CoreDevice): string | null {
|
|
98
|
-
if (d.isGuest) return null;
|
|
99
89
|
const h = normHost(d.quicHost);
|
|
100
90
|
if (!h) return null;
|
|
101
91
|
return `${h}:${d.quicPort || 0}`;
|
|
@@ -212,11 +202,10 @@ export function collapseDevices(devices: CoreDevice[]): CoreDevice[] {
|
|
|
212
202
|
// ── Freshness + target pick ───────────────────────────────────────────
|
|
213
203
|
|
|
214
204
|
/**
|
|
215
|
-
* "Fresh" matches the mobile app: online + heartbeat
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* picks.
|
|
205
|
+
* "Fresh" matches the mobile app: online + heartbeat < 90 s. Clients
|
|
206
|
+
* read Convex's `isOnline` first (backend already applies its own 90 s
|
|
207
|
+
* gate from the server clock), then use this helper when they need the
|
|
208
|
+
* phone-side freshness opinion too — e.g. for auto-connect picks.
|
|
220
209
|
*/
|
|
221
210
|
export function isDeviceFresh(d: CoreDevice, now = Date.now()): boolean {
|
|
222
211
|
if (!d.isOnline) return false;
|
|
@@ -225,20 +214,11 @@ export function isDeviceFresh(d: CoreDevice, now = Date.now()): boolean {
|
|
|
225
214
|
}
|
|
226
215
|
|
|
227
216
|
/**
|
|
228
|
-
* Choose the best candidate for an auto-connect attempt.
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* still reachable off-LAN — which is precisely when quicHost is absent.
|
|
234
|
-
* - If the id is not in the list, return null. Falling through to another
|
|
235
|
-
* machine silently lands the user's fix on the wrong host: they pick the
|
|
236
|
-
* Mac mini, the commit shows up on the laptop, and nothing reports an
|
|
237
|
-
* error. Not connecting is the better failure — the caller surfaces
|
|
238
|
-
* "selected machine missing, re-select it".
|
|
239
|
-
*
|
|
240
|
-
* With no preference, fall back: fresh + quicHost → online + quicHost →
|
|
241
|
-
* first with a quicHost.
|
|
217
|
+
* Choose the best candidate for an auto-connect attempt. Preference:
|
|
218
|
+
* 1. explicit `preferredDeviceId`, by identity alone; missing means fail
|
|
219
|
+
* 2. fresh (online + recent heartbeat) + has a quicHost
|
|
220
|
+
* 3. online + has a quicHost
|
|
221
|
+
* 4. first with a quicHost
|
|
242
222
|
*/
|
|
243
223
|
export function pickTargetDevice(
|
|
244
224
|
devices: CoreDevice[],
|
|
@@ -246,7 +226,10 @@ export function pickTargetDevice(
|
|
|
246
226
|
): CoreDevice | null {
|
|
247
227
|
if (!devices.length) return null;
|
|
248
228
|
if (preferredDeviceId) {
|
|
249
|
-
|
|
229
|
+
// An off-LAN device normally has no quicHost; the relay addresses it by
|
|
230
|
+
// id. Never fall through to a different healthy machine after the user
|
|
231
|
+
// selected one explicitly — failure is safer than misrouting their work.
|
|
232
|
+
return devices.find((d) => d.deviceId === preferredDeviceId) || null;
|
|
250
233
|
}
|
|
251
234
|
const fresh = devices.find((d) => isDeviceFresh(d) && d.quicHost);
|
|
252
235
|
if (fresh) return fresh;
|
package/src/_core/endpoints.ts
CHANGED
|
@@ -65,13 +65,6 @@ export const CONVEX_ENDPOINTS = {
|
|
|
65
65
|
authLogin: '/auth/login',
|
|
66
66
|
userSettings: '/settings',
|
|
67
67
|
platformConfig: '/config',
|
|
68
|
-
guestsList: '/guests/list',
|
|
69
|
-
guestsHosts: '/guests/hosts',
|
|
70
|
-
guestsAllowed: '/guests/allowed',
|
|
71
|
-
guestsInvite: '/guests/invite',
|
|
72
|
-
guestsAccept: '/guests/accept',
|
|
73
|
-
guestsAcceptCode: '/guests/accept-code',
|
|
74
|
-
guestsRevoke: '/guests/revoke',
|
|
75
68
|
} as const;
|
|
76
69
|
|
|
77
70
|
/** Routes on a relay server. */
|
package/src/_core/index.ts
CHANGED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// AUTO-SYNCED from shared/client-core/src/remoteless.ts.
|
|
2
|
+
// DO NOT EDIT IN PLACE. Edit the source and re-run
|
|
3
|
+
// scripts/sync-client-core.sh. CI checks drift via `--check`.
|
|
4
|
+
|
|
5
|
+
// One pure decision for every client surface that can explicitly select, or
|
|
6
|
+
// fall back to, a local or hosted DeepSeek lane. Remoteless is bounded capacity
|
|
7
|
+
// and never becomes the default ahead of configured devices and runners.
|
|
8
|
+
|
|
9
|
+
export type RemotelessCapability =
|
|
10
|
+
| "analysis-chat" | "code-edit" | "git-read" | "git-commit" | "git-push"
|
|
11
|
+
| "static-preflight" | "existing-web-artifact"
|
|
12
|
+
| "dev-server" | "web-build" | "flutter-render" | "native-build"
|
|
13
|
+
| "simulator" | "shell" | "test" | "deploy" | "container";
|
|
14
|
+
|
|
15
|
+
export type RemotelessSupport = "supported" | "bounded" | "unavailable";
|
|
16
|
+
|
|
17
|
+
export type RemotelessCapabilityResult = {
|
|
18
|
+
capability: RemotelessCapability;
|
|
19
|
+
support: RemotelessSupport;
|
|
20
|
+
code: string;
|
|
21
|
+
summary: string;
|
|
22
|
+
detail: string;
|
|
23
|
+
route: { label: string; path: "/devices" | "/cloud-onboarding" };
|
|
24
|
+
alternateRoute?: { label: string; path: "/devices" | "/cloud-onboarding" };
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ExecutionCandidate = {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
role: "explicit" | "primary" | "secondary" | "focused";
|
|
31
|
+
connected: boolean;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type RemotelessPlacement =
|
|
35
|
+
| { lane: "remote"; target: ExecutionCandidate; degraded: boolean; banner: string | null }
|
|
36
|
+
| { lane: "remoteless"; capability: RemotelessCapabilityResult; banner: string }
|
|
37
|
+
| { lane: "blocked"; capability: RemotelessCapabilityResult; banner: string };
|
|
38
|
+
|
|
39
|
+
const LOCAL_SUPPORT: Record<RemotelessCapability, RemotelessSupport> = {
|
|
40
|
+
"analysis-chat": "supported", "code-edit": "bounded", "git-read": "supported", "git-commit": "bounded",
|
|
41
|
+
"git-push": "bounded", "static-preflight": "supported",
|
|
42
|
+
"existing-web-artifact": "supported", "dev-server": "unavailable",
|
|
43
|
+
"web-build": "unavailable", "flutter-render": "unavailable",
|
|
44
|
+
"native-build": "unavailable", simulator: "unavailable", shell: "unavailable",
|
|
45
|
+
test: "unavailable", deploy: "unavailable", container: "unavailable",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const LABELS: Record<RemotelessCapability, string> = {
|
|
49
|
+
"analysis-chat": "analysis and chat", "code-edit": "editing", "git-read": "Git inspection", "git-commit": "Git commit",
|
|
50
|
+
"git-push": "Git push", "static-preflight": "static preflight",
|
|
51
|
+
"existing-web-artifact": "an existing web artifact", "dev-server": "a dev server",
|
|
52
|
+
"web-build": "a web build", "flutter-render": "Flutter rendering",
|
|
53
|
+
"native-build": "a native build", simulator: "a simulator", shell: "shell commands",
|
|
54
|
+
test: "tests", deploy: "deployment", container: "containers",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function remotelessCapability(capability: RemotelessCapability, surface: "ios" | "android" | "web" | "companion"): RemotelessCapabilityResult {
|
|
58
|
+
const support = capability === "analysis-chat" ? "supported" : surface === "ios" || surface === "android"
|
|
59
|
+
? LOCAL_SUPPORT[capability]
|
|
60
|
+
: capability === "existing-web-artifact" ? "supported" : "unavailable";
|
|
61
|
+
const label = LABELS[capability];
|
|
62
|
+
const host = surface === "ios" ? "iPhone/iPad" : surface === "android" ? "Android device" : surface === "web" ? "browser" : "companion device";
|
|
63
|
+
if (support === "supported") return {
|
|
64
|
+
capability, support, code: `remoteless.${capability}.supported`,
|
|
65
|
+
summary: `${label} can run on this device`,
|
|
66
|
+
detail: `${label} does not require a remote shell or build toolchain.`,
|
|
67
|
+
route: { label: "Choose a device", path: "/devices" },
|
|
68
|
+
};
|
|
69
|
+
if (support === "bounded") return {
|
|
70
|
+
capability, support, code: `remoteless.${capability}.bounded`,
|
|
71
|
+
summary: `${label} is available on this device with limits`,
|
|
72
|
+
detail: `${label} can run locally, but background execution is bounded and interrupted work returns to Review.`,
|
|
73
|
+
route: { label: "Choose a device", path: "/devices" },
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
capability, support, code: `remoteless.${capability}.unavailable`,
|
|
77
|
+
summary: `${label} needs another execution target`,
|
|
78
|
+
detail: capability === "flutter-render"
|
|
79
|
+
? `The ${host} can display an already-built Flutter web artifact, but Yaver's remoteless runtime has no Flutter SDK, shell, dev server, or simulator. Build and serve it on your primary/secondary device or Cloud Workspace.`
|
|
80
|
+
: `Yaver's remoteless runtime on this ${host} cannot provide ${label}: it has no general shell, package manager, persistent process host, simulator, native SDK, or container runtime. Use an eligible primary/secondary device or Cloud Workspace.`,
|
|
81
|
+
route: { label: "Choose a capable device", path: "/devices" },
|
|
82
|
+
alternateRoute: { label: "Use Cloud Workspace", path: "/cloud-onboarding" },
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolveRemotelessPlacement(input: {
|
|
87
|
+
capability: RemotelessCapability;
|
|
88
|
+
surface: "ios" | "android" | "web" | "companion";
|
|
89
|
+
candidates: ExecutionCandidate[];
|
|
90
|
+
forceLocal?: boolean;
|
|
91
|
+
}): RemotelessPlacement {
|
|
92
|
+
const capability = remotelessCapability(input.capability, input.surface);
|
|
93
|
+
if (!input.forceLocal) {
|
|
94
|
+
// Enforce precedence here instead of trusting every surface to construct
|
|
95
|
+
// its array correctly. Within a role, caller order remains significant
|
|
96
|
+
// (for example project-assigned primary before account-wide primary).
|
|
97
|
+
const target = (["explicit", "primary", "secondary", "focused"] as const)
|
|
98
|
+
.map((role) => input.candidates.find((candidate) => candidate.role === role && candidate.connected))
|
|
99
|
+
.find((candidate): candidate is ExecutionCandidate => !!candidate);
|
|
100
|
+
if (target) {
|
|
101
|
+
const degraded = target.role === "secondary";
|
|
102
|
+
return { lane: "remote", target, degraded, banner: degraded ? `Using secondary · ${target.name} · primary unavailable` : null };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const configured = input.candidates.filter((candidate) => candidate.role !== "focused");
|
|
106
|
+
const reason = configured.length ? "Primary and secondary devices are unavailable." : "No eligible remote device is configured.";
|
|
107
|
+
if (capability.support === "unavailable") return { lane: "blocked", capability, banner: `${reason} ${capability.summary}.` };
|
|
108
|
+
if (input.forceLocal) return { lane: "remoteless", capability, banner: `No remote box selected · ${capability.summary}.` };
|
|
109
|
+
return { lane: "remoteless", capability, banner: `Remoteless fallback · ${reason} ${capability.summary}.` };
|
|
110
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// AUTO-SYNCED from shared/client-core/src/trace.test.ts.
|
|
2
|
+
// DO NOT EDIT IN PLACE. Edit the source and re-run
|
|
3
|
+
// scripts/sync-client-core.sh. CI checks drift via `--check`.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* trace.test.ts — guards for the shared trace assembler.
|
|
7
|
+
*
|
|
8
|
+
* Run: npx tsx shared/client-core/src/trace.test.ts
|
|
9
|
+
*/
|
|
10
|
+
import { assembleTrace } from "./trace";
|
|
11
|
+
|
|
12
|
+
let failures = 0;
|
|
13
|
+
const eq = (got: unknown, want: unknown, label: string) => {
|
|
14
|
+
if (JSON.stringify(got) === JSON.stringify(want)) console.log(`ok ${label}`);
|
|
15
|
+
else { console.error(`FAIL ${label}:\n got ${JSON.stringify(got)}\n want ${JSON.stringify(want)}`); failures++; }
|
|
16
|
+
};
|
|
17
|
+
const ok = (c: unknown, label: string) => eq(Boolean(c), true, label);
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
const t = assembleTrace({
|
|
21
|
+
surface: "web",
|
|
22
|
+
surfaceVersion: "1.1.164",
|
|
23
|
+
agentVersion: "1.99.409",
|
|
24
|
+
device: "ubuntu-4gb-hel1-1 (2ed7da41…)",
|
|
25
|
+
relay: "public-free",
|
|
26
|
+
task: { id: "abc123", status: "failed", runner: "opencode", model: "deepseek/deepseek-v4-flash", title: "build" },
|
|
27
|
+
error: "flutter exited before becoming ready",
|
|
28
|
+
raw: "the raw failure bytes",
|
|
29
|
+
logTail: "line1\nline2",
|
|
30
|
+
ts: 1700000000000,
|
|
31
|
+
});
|
|
32
|
+
ok(t.includes("surface: web"), "surface line present");
|
|
33
|
+
ok(t.includes("agent.version: 1.99.409"), "agent version present");
|
|
34
|
+
ok(t.includes("task: abc123 status=failed runner=opencode"), "task line present");
|
|
35
|
+
ok(t.includes("error: flutter exited before becoming ready"), "error present");
|
|
36
|
+
ok(t.includes("log-tail:\nline1\nline2"), "log tail present under its label");
|
|
37
|
+
ok(t.includes("ts: 1700000000000"), "timestamp present");
|
|
38
|
+
}
|
|
39
|
+
{
|
|
40
|
+
// No invented fields when absent.
|
|
41
|
+
const t = assembleTrace({ surface: "mobile", task: { id: "x" } });
|
|
42
|
+
ok(!t.includes("agent.version"), "no agent.version when absent");
|
|
43
|
+
ok(!t.includes("error:"), "no error when absent");
|
|
44
|
+
ok(t.includes("surface: mobile"), "surface still present");
|
|
45
|
+
}
|
|
46
|
+
{
|
|
47
|
+
// Secrets are redacted.
|
|
48
|
+
const t = assembleTrace({ surface: "web", error: "failed with token=abc123xyz" });
|
|
49
|
+
ok(t.includes("[redacted]") && !t.includes("abc123xyz"), "token redacted from error");
|
|
50
|
+
const t2 = assembleTrace({ surface: "mobile", raw: "Authorization: Bearer deadbeef" });
|
|
51
|
+
ok(t2.includes("[redacted]") && !t2.includes("deadbeef"), "bearer redacted from raw");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (failures > 0) {
|
|
55
|
+
console.error(`\n${failures} FAILURE(s)`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
console.log("\nall trace tests pass");
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// AUTO-SYNCED from shared/client-core/src/trace.ts.
|
|
2
|
+
// DO NOT EDIT IN PLACE. Edit the source and re-run
|
|
3
|
+
// scripts/sync-client-core.sh. CI checks drift via `--check`.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* trace.ts — one shared paste-ready trace assembler for every surface.
|
|
7
|
+
*
|
|
8
|
+
* WHY (2026-08-09): a crash/failure on web, mobile, or the console used to
|
|
9
|
+
* copy different things — RawFailureBanner copied just `failure.raw`, the
|
|
10
|
+
* mobile Logs sheet copied `combinedLogText`, RuntimeLab copied raw console
|
|
11
|
+
* lines. None carried the surface identity, versions, task id, runner/model,
|
|
12
|
+
* or the log tail, so pasting into an issue or a vibing follow-up lost the
|
|
13
|
+
* context that makes a bug reproducible. One assembler here gives every
|
|
14
|
+
* surface the SAME structured blob (surface → versions → device/relay →
|
|
15
|
+
* task → error → log tail), so web and mobile can never drift
|
|
16
|
+
* (AGENTS.md: one shared classifier, no copies). Mirrored into
|
|
17
|
+
* mobile/src/_core and web/lib/_core by scripts/sync-client-core.sh.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface TraceContext {
|
|
21
|
+
/** Surface name: "web" | "mobile" | "console" | "cli" | "watch" ... */
|
|
22
|
+
surface: string;
|
|
23
|
+
/** e.g. web build label, mobile JS bundle version, cli version. */
|
|
24
|
+
surfaceVersion?: string;
|
|
25
|
+
/** Agent version reported by /info (e.g. "1.99.409"). */
|
|
26
|
+
agentVersion?: string;
|
|
27
|
+
/** Device the task ran on (name + id). */
|
|
28
|
+
device?: string;
|
|
29
|
+
/** Relay region / id if known. */
|
|
30
|
+
relay?: string;
|
|
31
|
+
/** Task identity + lifecycle. */
|
|
32
|
+
task?: {
|
|
33
|
+
id: string;
|
|
34
|
+
status?: string;
|
|
35
|
+
runner?: string;
|
|
36
|
+
model?: string;
|
|
37
|
+
title?: string;
|
|
38
|
+
};
|
|
39
|
+
/** The human-readable error (already extracted/named). */
|
|
40
|
+
error?: string;
|
|
41
|
+
/** Raw failure blob (the undecorated original). */
|
|
42
|
+
raw?: string;
|
|
43
|
+
/** Log tail — last N lines of the relevant log (agent/relay/runtime). */
|
|
44
|
+
logTail?: string;
|
|
45
|
+
/** When the trace was captured (ms epoch). */
|
|
46
|
+
ts?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const MASK = /token\s*[=:]\s*\S+|Bearer\s+\S+|api[_-]?key\s*[=:]\s*\S+|password\s*[=:]\s*\S+/gi;
|
|
50
|
+
|
|
51
|
+
function sanitize(s: string): string {
|
|
52
|
+
return String(s || "").replace(MASK, "[redacted]");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Assemble a paste-ready trace. Every present field becomes one labelled
|
|
57
|
+
* line; nothing is ever invented. Returns a plain-text blob.
|
|
58
|
+
*/
|
|
59
|
+
export function assembleTrace(ctx: TraceContext): string {
|
|
60
|
+
const out: string[] = [];
|
|
61
|
+
out.push("--- Yaver trace ---");
|
|
62
|
+
out.push(`surface: ${sanitize(ctx.surface)}`);
|
|
63
|
+
if (ctx.surfaceVersion) out.push(`surface.version: ${sanitize(ctx.surfaceVersion)}`);
|
|
64
|
+
if (ctx.agentVersion) out.push(`agent.version: ${sanitize(ctx.agentVersion)}`);
|
|
65
|
+
if (ctx.device) out.push(`device: ${sanitize(ctx.device)}`);
|
|
66
|
+
if (ctx.relay) out.push(`relay: ${sanitize(ctx.relay)}`);
|
|
67
|
+
if (ctx.task) {
|
|
68
|
+
out.push(`task: ${sanitize(ctx.task.id)}${ctx.task.status ? ` status=${sanitize(ctx.task.status)}` : ""}${ctx.task.runner ? ` runner=${sanitize(ctx.task.runner)}` : ""}${ctx.task.model ? ` model=${sanitize(ctx.task.model)}` : ""}${ctx.task.title ? ` title=${sanitize(ctx.task.title)}` : ""}`);
|
|
69
|
+
}
|
|
70
|
+
if (ctx.error) out.push(`error: ${sanitize(ctx.error)}`);
|
|
71
|
+
if (ctx.raw) out.push(`raw: ${sanitize(ctx.raw)}`);
|
|
72
|
+
if (ctx.logTail) out.push(`log-tail:\n${sanitize(ctx.logTail)}`);
|
|
73
|
+
out.push(`ts: ${ctx.ts ?? Date.now()}`);
|
|
74
|
+
return out.join("\n");
|
|
75
|
+
}
|
package/src/auth.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* (`yaver://oauth-callback`) the Yaver mobile app uses.
|
|
11
11
|
* - Email / password sign-up + login (no 2FA flow — for SDK simplicity).
|
|
12
12
|
* - Token validation.
|
|
13
|
-
* - `/devices/list` →
|
|
13
|
+
* - `/devices/list` → owner remote dev machines.
|
|
14
14
|
*
|
|
15
15
|
* Mobile-only. A web equivalent will ship as a separate `yaver-web-feedback`
|
|
16
16
|
* package; do not import this module from a browser bundle.
|
|
@@ -399,7 +399,7 @@ export async function loginWithEmail(
|
|
|
399
399
|
return { token: data.token, userId: data.userId };
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
-
// ───
|
|
402
|
+
// ─── Owner devices ────────────────────────────────────────────────────
|
|
403
403
|
|
|
404
404
|
export interface RemoteDevice {
|
|
405
405
|
deviceId: string;
|
|
@@ -409,12 +409,6 @@ export interface RemoteDevice {
|
|
|
409
409
|
needsAuth: boolean;
|
|
410
410
|
runnerDown: boolean;
|
|
411
411
|
lastHeartbeat: number;
|
|
412
|
-
isGuest: boolean;
|
|
413
|
-
hostUserId?: string;
|
|
414
|
-
hostName?: string;
|
|
415
|
-
hostEmail?: string;
|
|
416
|
-
hostUserIdString?: string;
|
|
417
|
-
accessScope: 'owner' | 'shared-scoped' | 'shared-legacy';
|
|
418
412
|
quicHost: string;
|
|
419
413
|
quicPort: number;
|
|
420
414
|
/** Agent HTTP port — preferred over quicPort when present. */
|
|
@@ -432,7 +426,6 @@ export interface RemoteDevice {
|
|
|
432
426
|
|
|
433
427
|
export interface DeviceList {
|
|
434
428
|
owned: RemoteDevice[];
|
|
435
|
-
shared: RemoteDevice[];
|
|
436
429
|
}
|
|
437
430
|
|
|
438
431
|
export interface DeviceReachability {
|
|
@@ -440,52 +433,9 @@ export interface DeviceReachability {
|
|
|
440
433
|
url?: string;
|
|
441
434
|
}
|
|
442
435
|
|
|
443
|
-
export interface GuestInvitation {
|
|
444
|
-
hostUserId: string;
|
|
445
|
-
hostName: string;
|
|
446
|
-
hostEmail: string;
|
|
447
|
-
hostUserIdString?: string;
|
|
448
|
-
createdAt: number;
|
|
449
|
-
expiresAt: number;
|
|
450
|
-
inviteCode?: string;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
export interface ActiveGuestHost {
|
|
454
|
-
hostUserId: string;
|
|
455
|
-
hostName: string;
|
|
456
|
-
hostEmail: string;
|
|
457
|
-
grantedAt: number;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
export interface GuestHostsResponse {
|
|
461
|
-
pending: GuestInvitation[];
|
|
462
|
-
active: ActiveGuestHost[];
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
export interface InvitationHostDevice {
|
|
466
|
-
deviceId: string;
|
|
467
|
-
name: string;
|
|
468
|
-
platform: string;
|
|
469
|
-
lastHeartbeat?: number;
|
|
470
|
-
proposed: boolean;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
export interface InvitationPreview {
|
|
474
|
-
inviteCode: string;
|
|
475
|
-
hostUserId: string;
|
|
476
|
-
hostName: string;
|
|
477
|
-
hostEmail: string;
|
|
478
|
-
hostUserIdString?: string;
|
|
479
|
-
proposedDeviceIds?: string[];
|
|
480
|
-
hostDevices: InvitationHostDevice[];
|
|
481
|
-
invitedByUserId?: boolean;
|
|
482
|
-
expiresAt: number;
|
|
483
|
-
createdAt: number;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
436
|
/**
|
|
487
|
-
* Fetch the
|
|
488
|
-
*
|
|
437
|
+
* Fetch the signed-in owner's remote dev machines. Legacy non-owner rows are
|
|
438
|
+
* discarded even if a stale backend still returns them.
|
|
489
439
|
*
|
|
490
440
|
* Collapses duplicate rows before splitting — Convex can return multiple
|
|
491
441
|
* rows per physical machine after a re-pair or hostname change, and the
|
|
@@ -498,7 +448,7 @@ export async function listReachableDevices(
|
|
|
498
448
|
const res = await fetch(`${convexSiteUrl}/devices/list`, {
|
|
499
449
|
headers: { Authorization: `Bearer ${token}` },
|
|
500
450
|
});
|
|
501
|
-
if (!res.ok) return { owned: []
|
|
451
|
+
if (!res.ok) return { owned: [] };
|
|
502
452
|
const data = await res.json();
|
|
503
453
|
const raw = (data.devices ?? []) as any[];
|
|
504
454
|
// Normalise Convex field names → SDK's RemoteDevice shape. The
|
|
@@ -512,12 +462,6 @@ export async function listReachableDevices(
|
|
|
512
462
|
needsAuth: !!d.needsAuth,
|
|
513
463
|
runnerDown: !!d.runnerDown,
|
|
514
464
|
lastHeartbeat: d.lastHeartbeat ?? 0,
|
|
515
|
-
isGuest: !!d.isGuest,
|
|
516
|
-
hostUserId: d.hostUserId,
|
|
517
|
-
hostName: d.hostName,
|
|
518
|
-
hostEmail: d.hostEmail,
|
|
519
|
-
hostUserIdString: d.hostUserIdString,
|
|
520
|
-
accessScope: d.accessScope ?? 'owner',
|
|
521
465
|
quicHost: d.quicHost ?? d.host ?? '',
|
|
522
466
|
quicPort: d.quicPort ?? 0,
|
|
523
467
|
httpPort: d.httpPort ?? d.quicPort,
|
|
@@ -532,12 +476,9 @@ export async function listReachableDevices(
|
|
|
532
476
|
// Lazy require so Jest + tree-shakers don't choke on a circular import.
|
|
533
477
|
const { collapseRemoteDevices } = require('./deviceDedup') as typeof import('./deviceDedup');
|
|
534
478
|
const deduped = collapseRemoteDevices(normalised);
|
|
535
|
-
return {
|
|
536
|
-
owned: deduped.filter((d) => !d.isGuest),
|
|
537
|
-
shared: deduped.filter((d) => d.isGuest),
|
|
538
|
-
};
|
|
479
|
+
return { owned: deduped };
|
|
539
480
|
} catch {
|
|
540
|
-
return { owned: []
|
|
481
|
+
return { owned: [] };
|
|
541
482
|
}
|
|
542
483
|
}
|
|
543
484
|
|
|
@@ -581,93 +522,3 @@ export async function probeDeviceReachability(
|
|
|
581
522
|
}
|
|
582
523
|
return { reachable: false };
|
|
583
524
|
}
|
|
584
|
-
|
|
585
|
-
export async function mintGuestSdkToken(
|
|
586
|
-
token: string,
|
|
587
|
-
hostUserId: string,
|
|
588
|
-
targetDeviceId: string,
|
|
589
|
-
): Promise<{ token: string; expiresAt: number; allowedProjects?: string[] }> {
|
|
590
|
-
const res = await fetch(`${convexSiteUrl}/guests/sdk-token`, {
|
|
591
|
-
method: 'POST',
|
|
592
|
-
headers: {
|
|
593
|
-
Authorization: `Bearer ${token}`,
|
|
594
|
-
'Content-Type': 'application/json',
|
|
595
|
-
},
|
|
596
|
-
body: JSON.stringify({ hostUserId, targetDeviceId }),
|
|
597
|
-
});
|
|
598
|
-
if (!res.ok) {
|
|
599
|
-
const data = await res.json().catch(() => ({}));
|
|
600
|
-
throw new Error(data.error || 'Failed to mint delegated SDK token');
|
|
601
|
-
}
|
|
602
|
-
return res.json();
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
export async function fetchGuestHosts(token: string): Promise<GuestHostsResponse> {
|
|
606
|
-
const res = await fetch(`${convexSiteUrl}/guests/hosts`, {
|
|
607
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
608
|
-
});
|
|
609
|
-
if (!res.ok) {
|
|
610
|
-
const data = await res.json().catch(() => ({}));
|
|
611
|
-
throw new Error(data.error || 'Failed to fetch guest hosts');
|
|
612
|
-
}
|
|
613
|
-
return res.json();
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
export async function findInviteByCode(
|
|
617
|
-
token: string,
|
|
618
|
-
code: string,
|
|
619
|
-
): Promise<InvitationPreview | null> {
|
|
620
|
-
const cleaned = code.toUpperCase().trim();
|
|
621
|
-
const res = await fetch(
|
|
622
|
-
`${convexSiteUrl}/guests/find-by-code?code=${encodeURIComponent(cleaned)}`,
|
|
623
|
-
{ headers: { Authorization: `Bearer ${token}` } },
|
|
624
|
-
);
|
|
625
|
-
if (res.status === 404) return null;
|
|
626
|
-
if (!res.ok) {
|
|
627
|
-
const data = await res.json().catch(() => ({}));
|
|
628
|
-
throw new Error(data.error || 'Failed to load invite');
|
|
629
|
-
}
|
|
630
|
-
return res.json();
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
export async function acceptGuestByCode(
|
|
634
|
-
token: string,
|
|
635
|
-
code: string,
|
|
636
|
-
approvedDeviceIds?: string[],
|
|
637
|
-
): Promise<{ hostName: string; hostEmail: string }> {
|
|
638
|
-
const res = await fetch(`${convexSiteUrl}/guests/accept-code`, {
|
|
639
|
-
method: 'POST',
|
|
640
|
-
headers: {
|
|
641
|
-
Authorization: `Bearer ${token}`,
|
|
642
|
-
'Content-Type': 'application/json',
|
|
643
|
-
},
|
|
644
|
-
body: JSON.stringify({
|
|
645
|
-
code: code.toUpperCase().trim(),
|
|
646
|
-
approvedDeviceIds,
|
|
647
|
-
}),
|
|
648
|
-
});
|
|
649
|
-
if (!res.ok) {
|
|
650
|
-
const data = await res.json().catch(() => ({}));
|
|
651
|
-
throw new Error(data.error || 'Invalid invite code');
|
|
652
|
-
}
|
|
653
|
-
return res.json();
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
export async function acceptGuestInvitation(
|
|
657
|
-
token: string,
|
|
658
|
-
hostUserId: string,
|
|
659
|
-
approvedDeviceIds?: string[],
|
|
660
|
-
): Promise<void> {
|
|
661
|
-
const res = await fetch(`${convexSiteUrl}/guests/accept`, {
|
|
662
|
-
method: 'POST',
|
|
663
|
-
headers: {
|
|
664
|
-
Authorization: `Bearer ${token}`,
|
|
665
|
-
'Content-Type': 'application/json',
|
|
666
|
-
},
|
|
667
|
-
body: JSON.stringify({ hostUserId, approvedDeviceIds }),
|
|
668
|
-
});
|
|
669
|
-
if (!res.ok) {
|
|
670
|
-
const data = await res.json().catch(() => ({}));
|
|
671
|
-
throw new Error(data.error || 'Failed to accept invitation');
|
|
672
|
-
}
|
|
673
|
-
}
|