@ishlabs/cli 0.15.0 → 0.17.0

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.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * SSE consumer for the backend's per-study event stream.
3
+ *
4
+ * Used by `study run --wait` to wake up the poll loop as soon as a tester
5
+ * status / interaction event arrives, instead of waiting for the next poll
6
+ * tick. The canonical truth source remains `GET /studies/{id}` — SSE here
7
+ * only shortens the latency between a backend event and the next status
8
+ * fetch; the poll fallback still runs on a slow timer in case events are
9
+ * missed.
10
+ *
11
+ * Best-effort:
12
+ * - Mints a short-lived stream token (POST /auth/stream-token).
13
+ * - Opens `GET /studies/{id}/events?token=…` via `fetch` and streams the
14
+ * response body.
15
+ * - Returns (silently exits the iterator) on any failure — token mint
16
+ * 503 (server not configured), endpoint 503 (broker offline on this
17
+ * instance), network error, abort. The caller's polling rhythm is the
18
+ * safety net; we never raise.
19
+ *
20
+ * Stream-token TTL is 1h on the backend. For runs longer than that the
21
+ * fetch will end (server closes); the caller falls back to pure polling
22
+ * for the remainder.
23
+ */
24
+ import { ApiClient, ApiError } from "./api-client.js";
25
+ export interface StudyEvent {
26
+ type: string;
27
+ study_id: string;
28
+ iteration_id?: string | null;
29
+ tester_id?: string | null;
30
+ interaction_id?: string | null;
31
+ frame_id?: string | null;
32
+ frame_version_id?: string | null;
33
+ tester_status?: string | null;
34
+ ts: string;
35
+ seq: number;
36
+ payload?: unknown;
37
+ }
38
+ /**
39
+ * Async generator that yields parsed StudyEvents from the backend SSE
40
+ * stream. Exits silently (without throwing) on failure or abort — callers
41
+ * MUST have a polling fallback that drives correctness.
42
+ */
43
+ export declare function streamStudyEvents(client: ApiClient, studyId: string, signal: AbortSignal): AsyncGenerator<StudyEvent, void, void>;
44
+ /** Type narrower used by callers to skip the synthetic LAG marker. */
45
+ export declare function isLagEvent(event: StudyEvent): boolean;
46
+ export { ApiError };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * SSE consumer for the backend's per-study event stream.
3
+ *
4
+ * Used by `study run --wait` to wake up the poll loop as soon as a tester
5
+ * status / interaction event arrives, instead of waiting for the next poll
6
+ * tick. The canonical truth source remains `GET /studies/{id}` — SSE here
7
+ * only shortens the latency between a backend event and the next status
8
+ * fetch; the poll fallback still runs on a slow timer in case events are
9
+ * missed.
10
+ *
11
+ * Best-effort:
12
+ * - Mints a short-lived stream token (POST /auth/stream-token).
13
+ * - Opens `GET /studies/{id}/events?token=…` via `fetch` and streams the
14
+ * response body.
15
+ * - Returns (silently exits the iterator) on any failure — token mint
16
+ * 503 (server not configured), endpoint 503 (broker offline on this
17
+ * instance), network error, abort. The caller's polling rhythm is the
18
+ * safety net; we never raise.
19
+ *
20
+ * Stream-token TTL is 1h on the backend. For runs longer than that the
21
+ * fetch will end (server closes); the caller falls back to pure polling
22
+ * for the remainder.
23
+ */
24
+ import { ApiError } from "./api-client.js";
25
+ /**
26
+ * Async generator that yields parsed StudyEvents from the backend SSE
27
+ * stream. Exits silently (without throwing) on failure or abort — callers
28
+ * MUST have a polling fallback that drives correctness.
29
+ */
30
+ export async function* streamStudyEvents(client, studyId, signal) {
31
+ let token;
32
+ try {
33
+ const minted = await client.post("/auth/stream-token", { scope: `study:${studyId}:read` });
34
+ token = minted.token;
35
+ }
36
+ catch (err) {
37
+ // 503 = server not configured for realtime; anything else = transient.
38
+ // Either way, fall back silently.
39
+ void err;
40
+ return;
41
+ }
42
+ const url = `${client.apiBase}/studies/${studyId}/events?token=${encodeURIComponent(token)}`;
43
+ let response;
44
+ try {
45
+ response = await fetch(url, {
46
+ headers: { Accept: "text/event-stream" },
47
+ signal,
48
+ });
49
+ }
50
+ catch (err) {
51
+ void err;
52
+ return;
53
+ }
54
+ if (!response.ok || response.body === null) {
55
+ // 503 = broker not running on this instance; anything else = unexpected.
56
+ // Caller's polling rhythm handles it.
57
+ return;
58
+ }
59
+ const reader = response.body.getReader();
60
+ const decoder = new TextDecoder("utf-8");
61
+ let buffer = "";
62
+ try {
63
+ while (true) {
64
+ const { value, done } = await reader.read();
65
+ if (done)
66
+ return;
67
+ buffer += decoder.decode(value, { stream: true });
68
+ // SSE framing: events are separated by blank lines. A line beginning
69
+ // with ":" is a comment (heartbeat) and is ignored.
70
+ let sep = buffer.indexOf("\n\n");
71
+ while (sep !== -1) {
72
+ const block = buffer.slice(0, sep);
73
+ buffer = buffer.slice(sep + 2);
74
+ const event = parseEventBlock(block);
75
+ if (event !== null)
76
+ yield event;
77
+ sep = buffer.indexOf("\n\n");
78
+ }
79
+ }
80
+ }
81
+ catch (err) {
82
+ void err;
83
+ return;
84
+ }
85
+ finally {
86
+ // Best-effort cancel; ignore errors if the connection is already gone.
87
+ try {
88
+ await reader.cancel();
89
+ }
90
+ catch {
91
+ // ignore
92
+ }
93
+ }
94
+ }
95
+ function parseEventBlock(block) {
96
+ // Skip comment-only blocks (e.g. `: ping - …`).
97
+ const lines = block.split(/\r?\n/);
98
+ let data = null;
99
+ for (const line of lines) {
100
+ if (line.startsWith(":") || line.length === 0)
101
+ continue;
102
+ if (line.startsWith("data:")) {
103
+ // Per SSE spec the value starts after the optional single space.
104
+ const value = line.slice(5).replace(/^ /, "");
105
+ data = data === null ? value : data + "\n" + value;
106
+ }
107
+ // We intentionally ignore `event:` and `id:` here — the type and seq
108
+ // are also present inside the JSON payload, which is the canonical
109
+ // form. Reading them from the framing too would just be redundant.
110
+ }
111
+ if (data === null)
112
+ return null;
113
+ try {
114
+ return JSON.parse(data);
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ }
120
+ /** Type narrower used by callers to skip the synthetic LAG marker. */
121
+ export function isLagEvent(event) {
122
+ return event.type === "stream.lag";
123
+ }
124
+ // Re-export ApiError so callers don't need a separate import path when
125
+ // branching on whether the stream is available.
126
+ export { ApiError };
@@ -82,7 +82,6 @@ export interface Study {
82
82
  source_study_id?: string;
83
83
  assignments?: Assignment[];
84
84
  interview_questions?: InterviewQuestion[];
85
- frames?: unknown[];
86
85
  iterations?: Iteration[];
87
86
  created_at: string;
88
87
  updated_at: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ishlabs/cli",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "The command-line interface for ish",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,10 @@
10
10
  "build": "tsc",
11
11
  "patch:playwright": "node scripts/patch-playwright-core.mjs",
12
12
  "build:binary": "npm run patch:playwright && bun build --compile --external chromium-bidi --external electron src/index.ts --outfile ish",
13
+ "build:skills-repo": "npm run build && node scripts/generate-skills-repo.mjs",
14
+ "verify:skills-parity": "npm run build && node scripts/verify-skills-parity.mjs",
13
15
  "dev": "tsc --watch",
16
+ "test": "npm run build && node --test \"tests/*.test.mjs\"",
14
17
  "prepublishOnly": "npm run build"
15
18
  },
16
19
  "engines": {