bluekiwi 0.2.4 → 0.3.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.
@@ -27,26 +27,84 @@ Select a registered workflow, create a task, and immediately execute the first s
27
27
  - `header` must be 12 characters or fewer.
28
28
  - `multiSelect` must be `false`.
29
29
 
30
- ## Session Restore (Resume In-Progress Task)
30
+ ## Session Restore (Resume In-Progress or Timed-Out Task)
31
31
 
32
- Before starting, check for running tasks using `advance(task_id, peek=true)` if a task ID is known, or prompt the user.
32
+ <HARD-RULE>
33
+ Follow all steps in order before proceeding to workflow selection.
34
+
35
+ ### Step A — Mark zombie tasks
36
+
37
+ Call `POST /api/tasks/timeout-stale` with `{"timeout_minutes": 120}`.
38
+ This converts any `running` tasks idle for over 2 hours to `timed_out`.
39
+
40
+ ### Step B — Fetch existing tasks
41
+
42
+ Call `list_tasks` with `status=running` and again with `status=timed_out`.
43
+ Collect all results into a combined candidate list sorted by `updated_at` descending (most recent first).
44
+
45
+ If the list is empty → skip to workflow selection.
33
46
 
34
- If an in-progress task is found, ask via AskUserQuestion:
47
+ ### Step C Build the task summary
48
+
49
+ For each candidate, compute age from `updated_at` to now.
50
+ Format: `"Task #{id} — {workflow_name} (Step {current_step}/{total_steps}, {age}전)"`
51
+
52
+ Example output:
53
+
54
+ ```
55
+ 미완료 태스크 2건이 있습니다:
56
+ ① Task #31 — 코드 리뷰 워크플로 (Step 3/6, 3시간 전) [timed_out]
57
+ ② Task #28 — 보안 점검 (Step 1/4, 30분 전) [running]
58
+ ```
35
59
 
36
- - header: "Resume?"
37
- - "Task #{id} ({workflow name}, Step {N}/{total}) is in progress. Resume or start a new workflow?"
38
- - options: "Resume (Recommended)" / "Start new workflow"
60
+ ### Step D — Ask what to do
39
61
 
40
- If resuming call `advance(task_id, peek=true)`, read `task_context`, then switch to `/bk-next` flow.
62
+ **Case 1: exactly 1 candidate**
63
+
64
+ Ask via AskUserQuestion:
65
+
66
+ - header: "미완료 태스크"
67
+ - preview: `"Task #{id} — {workflow_name}\nStep {N}/{total} · {age}전 중단"`
68
+ - options (pick the most appropriate 3–4):
69
+ - `"이어서 진행"` — resume this task
70
+ - `"종료하고 새로 시작"` — close this task then start fresh
71
+ - `"닫지 않고 새로 시작"` — leave as-is, open a new task
72
+ - _(only if status=running)_ `"계속 실행 중"` — another agent is handling it, do nothing
73
+
74
+ **Case 2: 2 or more candidates**
75
+
76
+ Ask via AskUserQuestion:
77
+
78
+ - header: "미완료 태스크"
79
+ - preview: the task summary list built above
80
+ - options:
81
+ - `"가장 최근 태스크 이어서"` — resume the most recent one
82
+ - `"모두 종료하고 새로 시작"` — close all candidates, then start fresh
83
+ - `"새 태스크 시작 (기존 유지)"` — leave existing as-is, open a new task
84
+
85
+ ### Step E — Execute the choice
86
+
87
+ **"이어서 진행" / "가장 최근 태스크 이어서"**:
88
+ Call `advance(task_id, peek=true)`, read `task_context`, then continue with the auto-advance loop.
89
+
90
+ **"종료하고 새로 시작" / "모두 종료하고 새로 시작"**:
91
+ For each task to close, call `complete_task(task_id, summary="사용자 요청으로 종료됨")`.
92
+ Confirm: "기존 태스크를 종료했습니다. 새 워크플로를 선택하세요." → proceed to workflow selection.
93
+
94
+ **"닫지 않고 새로 시작" / "새 태스크 시작 (기존 유지)"**:
95
+ Proceed to workflow selection without touching existing tasks.
96
+ </HARD-RULE>
41
97
 
42
98
  ## execute_step Required Parameters
43
99
 
44
100
  <HARD-RULE>
45
101
  Always populate these parameters when calling execute_step:
46
102
  - `context_snapshot`: JSON string. Store decisions made, key findings, and hints for the next step.
47
- - `agent_id`: Model name in use (e.g., "claude-opus-4-6")
103
+ - `model_id`: Current LLM model ID (e.g., "claude-opus-4-6"). Check your system prompt.
48
104
  - `user_name`: User name (omit if unknown)
49
105
 
106
+ Note: `provider_slug` (coding tool identity) is auto-injected by the MCP server from the connection handshake. Do not send it manually.
107
+
50
108
  If files were created or modified, record them in the `artifacts` array:
51
109
 
52
110
  - File created: `{artifact_type: "file", title: "Design Doc", file_path: "docs/specs/design.md"}`
@@ -65,15 +123,31 @@ echo "GIT_REMOTE: $(git remote get-url origin 2>/dev/null || echo 'none')"
65
123
  echo "GIT_BRANCH: $(git branch --show-current 2>/dev/null || echo 'unknown')"
66
124
  echo "USER: $(whoami 2>/dev/null || echo 'unknown')"
67
125
  echo "OS: $(uname -s 2>/dev/null || echo 'unknown') $(uname -m 2>/dev/null)"
126
+ # Detect CLI tool by walking up the process tree (up to 4 levels)
127
+ _AGENT=unknown; _PID=$PPID
128
+ for _L in 1 2 3 4; do
129
+ _C=$(ps -o comm= -p $_PID 2>/dev/null | tr '[:upper:]' '[:lower:]')
130
+ case "$_C" in
131
+ *claude*) _AGENT=claude-code; break ;;
132
+ *gemini*) _AGENT=gemini-cli; break ;;
133
+ *codex*) _AGENT=codex-cli; break ;;
134
+ *cursor*) _AGENT=cursor; break ;;
135
+ *windsurf*) _AGENT=windsurf; break ;;
136
+ *opencode*) _AGENT=opencode; break ;;
137
+ esac
138
+ _PID=$(ps -o ppid= -p $_PID 2>/dev/null | tr -d ' ')
139
+ [ -z "$_PID" ] || [ "$_PID" = "0" ] || [ "$_PID" = "1" ] && break
140
+ done
141
+ echo "AGENT: $_AGENT"
68
142
  ```
69
143
 
70
144
  Build a JSON object:
71
145
 
72
146
  ```json
73
147
  {
148
+ "agent": "claude-code",
74
149
  "project_dir": "/Users/dante/workspace/project",
75
150
  "user_name": "dante",
76
- "agent": "claude-code",
77
151
  "model_id": "claude-opus-4-6",
78
152
  "git_remote": "git@github.com:user/repo.git",
79
153
  "git_branch": "main",
@@ -82,7 +156,7 @@ Build a JSON object:
82
156
  }
83
157
  ```
84
158
 
85
- - `agent`: always "claude-code" when running in Claude Code
159
+ - `agent`: detected CLI tool name (from AGENT output above)
86
160
  - `model_id`: current model ID (check system prompt)
87
161
  - `started_at`: current UTC time
88
162
  </HARD-RULE>
@@ -104,17 +178,37 @@ Record only results (URL, status code, response summary).
104
178
 
105
179
  Call `list_workflows` to retrieve the list.
106
180
 
181
+ **No workflows exist**: Ask via AskUserQuestion:
182
+
183
+ - header: "No workflows"
184
+ - "No workflows found. Would you like to create one now?"
185
+ - options: "Create new workflow" / "Cancel"
186
+
187
+ If "Create new workflow" → immediately invoke the `bk-design` skill. Pass the user's original argument (if any) as the goal so `bk-design` can pre-fill the design step. After `bk-design` completes and the workflow is registered, return here and proceed with Step 2 using the newly created workflow.
188
+
107
189
  **Single workflow**: Skip the selection UI, just confirm:
108
190
 
109
191
  - "Start the '{title}' workflow?" (AskUserQuestion: "Start" / "Cancel")
110
192
 
111
193
  **Multiple workflows**: Show selection via AskUserQuestion.
112
194
 
113
- ### 2. Create Task
195
+ If the user selects "Create new workflow" from the selection UI → invoke `bk-design`, then continue as above.
196
+
197
+ ### 2. Create Task + Open Monitoring Page
114
198
 
115
199
  Call `start_workflow`. Pass any argument as `context`.
116
200
 
117
- ### 3. Execute First Step + auto_advance Loop
201
+ <HARD-RULE>
202
+ After `start_workflow` returns the task_id, immediately open the task monitoring page in the user's browser:
203
+
204
+ ```bash
205
+ open "${BLUEKIWI_URL:-http://localhost:3100}/tasks/${TASK_ID}"
206
+ ```
207
+
208
+ Use `open` on macOS, `xdg-open` on Linux. Derive `BLUEKIWI_URL` from the MCP connection or default to `http://localhost:3100`.
209
+ </HARD-RULE>
210
+
211
+ ### 3. Execute First Step + Auto-Advance Loop
118
212
 
119
213
  Read the first step's instruction as an **internal directive and execute immediately**.
120
214
 
@@ -127,19 +221,188 @@ Starting: {workflow title} ({n} steps)
127
221
  ━━━━━━━━━━━━━━━━━━━━━━━━━
128
222
  **1** → 2 → 3 → 4 → 5 → 6 → 7
129
223
  ━━━━━━━━━━━━━━━━━━━━━━━━━
224
+ 📺 Live: ${BLUEKIWI_URL}/tasks/${TASK_ID}
225
+ ━━━━━━━━━━━━━━━━━━━━━━━━━
130
226
  ```
131
227
 
132
- **auto_advance loop**: If the next step has `auto_advance: true`, continue executing without pausing.
228
+ **Auto-advance loop**: If execute_step returns no `next_action`, continue executing the next step without pausing.
133
229
 
134
230
  <HARD-RULE>
135
- After executing an auto_advance=true step, always proceed to the next step automatically.
231
+ After executing a step with no next_action, always proceed to the next step automatically.
136
232
  Show a brief inline update: "✅ [{title}] done → continuing to next step..."
137
- Repeat the loop until reaching an auto_advance=false step.
233
+ Repeat the loop until reaching a gate step or a hitl=true action step.
138
234
  </HARD-RULE>
139
235
 
140
236
  ### 4. When Pausing
141
237
 
142
- - **HITL** (execute_step returned `next_action: "wait_for_human_approval"`):
143
- Call `request_approval`, show "⏸ Waiting for approval ��� use /bk-approve when ready.", stop.
144
- - After completing an action step (auto_advance=false, no HITL): "Type `/bk-next` to proceed."
145
- - After showing a gate question: Wait for user response. Do not show `/bk-next` hint.
238
+ Check the `next_action` field in the `execute_step` response and handle accordingly:
239
+
240
+ #### HITL (next_action: "wait_for_human_approval")
241
+
242
+ Call `request_approval`, then immediately show the HITL approval AskUserQuestion (inline HITL approval). Do NOT stop and tell the user to type `/bk-approve`.
243
+
244
+ #### Gate step (no next_action, node_type=gate)
245
+
246
+ <HARD-RULE>
247
+ Write all VS content text (titles, descriptions, option labels, button text)
248
+ in the user's language. The frame UI (Submit button, status) is auto-localized,
249
+ but agent-authored content must match the user's locale.
250
+ </HARD-RULE>
251
+
252
+ - If `visual_selection: true`:
253
+ 1. Compose a VS content **fragment** using `bk-*` component classes. Write **only the inner HTML** - do not include `<html>`, `<head>`, or `<body>` tags. The frame (CSS, JS, submit button) is injected automatically by the web UI.
254
+
255
+ **Component quick reference:**
256
+ - Selection: `bk-options` (A/B/C cards, single), `bk-cards` (visual cards, single), `bk-checklist` (multi-select), `bk-code-compare` (code blocks, single)
257
+ - Input: `bk-slider` (numeric range), `bk-ranking` (drag reorder), `bk-matrix` (2x2 drag placement)
258
+ - Display: `bk-split`, `bk-pros-cons`, `bk-mockup`, `bk-timeline`
259
+ - Layout: `h2`, `.bk-subtitle`, `.bk-section`, `.bk-label`
260
+
261
+ Every selection/input element needs a `data-value` attribute. Example fragment:
262
+
263
+ ```html
264
+ <h2>Choose an approach</h2>
265
+ <p class="bk-subtitle">
266
+ Select the architecture that best fits your needs
267
+ </p>
268
+ <div class="bk-options">
269
+ <div class="bk-option" data-value="monolith" data-recommended>
270
+ <div class="bk-option-letter">A</div>
271
+ <div class="bk-option-body">
272
+ <h3>Monolith</h3>
273
+ <p>Simple deployment</p>
274
+ </div>
275
+ </div>
276
+ <div class="bk-option" data-value="microservices">
277
+ <div class="bk-option-letter">B</div>
278
+ <div class="bk-option-body">
279
+ <h3>Microservices</h3>
280
+ <p>Independent scaling</p>
281
+ </div>
282
+ </div>
283
+ </div>
284
+ ```
285
+
286
+ 2. Call `set_visual_html(task_id, node_id, html)` with the fragment.
287
+ 3. Open the VS deep link so the user sees the selection UI immediately:
288
+ ```bash
289
+ open "${BLUEKIWI_URL:-http://localhost:3100}/tasks/${TASK_ID}?step=${STEP_ORDER}&vs=true"
290
+ ```
291
+ 4. Poll `get_web_response(task_id)` every 3-5 seconds until a response arrives (max 120 seconds).
292
+ 5. The response is a **JSON object** (not a plain string). Parse it to read the user's choices:
293
+
294
+ ```json
295
+ {
296
+ "selections": ["monolith"],
297
+ "values": { "budget": 70 },
298
+ "ranking": ["security", "ux"]
299
+ }
300
+ ```
301
+
302
+ - `selections`: chosen option values (from bk-options, bk-cards, bk-checklist, bk-code-compare)
303
+ - `values`: numeric inputs (from bk-slider, keyed by data-name)
304
+ - `ranking`: ordered list (from bk-ranking)
305
+ - `matrix`: placement coordinates (from bk-matrix)
306
+ Only populated fields appear.
307
+
308
+ 6. Use the parsed response to form the gate answer and call `advance`.
309
+
310
+ - If `visual_selection: false` → present the gate question to the user via AskUserQuestion. Use the response as gate answer, call `execute_step` with the answer, then `advance`.
311
+
312
+ #### Attachments
313
+
314
+ <HARD-RULE>
315
+ When `advance` returns `node.attachments`:
316
+ 1. Review the list (filename, mime_type, size_bytes)
317
+ 2. Call `get_attachment(workflow_id, node_id, attachment_id)` for each text file the instruction references
318
+ 3. Use downloaded content as context when executing the instruction
319
+ 4. For binary files, note their existence but do not download unless explicitly required
320
+ </HARD-RULE>
321
+
322
+ #### Loop (next_action: "loop_back")
323
+
324
+ <HARD-RULE>
325
+ Loop nodes repeat until a termination condition is met. The instruction contains the termination condition.
326
+
327
+ **Execution flow:**
328
+
329
+ 1. Read the instruction and execute one iteration (e.g., ask one clarifying question).
330
+ 2. Present the result/question to the user via AskUserQuestion.
331
+ 3. Based on user response, decide: is the termination condition met?
332
+ - **NOT met** → call `execute_step(loop_continue=true)` → server creates a new pending log on the same node → re-execute the loop step (go back to step 1)
333
+ - **Met** → call `execute_step(loop_continue=false)` → loop ends → call `advance` to move to next step
334
+
335
+ **Example — "Clarifying Questions" loop (loop_back_to=self):**
336
+
337
+ ```
338
+ Iteration 1: "Who is the primary user of this feature?" → user answers → purpose clear, constraints unclear → loop_continue=true
339
+ Iteration 2: "Are there tech stack limitations?" → user answers → constraints clear, success criteria unclear → loop_continue=true
340
+ Iteration 3: "What defines completion?" → user answers → all items clear → loop_continue=false → advance
341
+ ```
342
+
343
+ **Example — "Design Section Presentation" loop:**
344
+
345
+ ```
346
+ Iteration 1: Present architecture section → user "looks good" → more sections remain → loop_continue=true
347
+ Iteration 2: Present data flow section → user "needs revision" → revise and re-present → loop_continue=true
348
+ Iteration 3: Present final section → user approves → all sections done → loop_continue=false → advance
349
+ ```
350
+
351
+ </HARD-RULE>
352
+
353
+ #### Loop + VS History Pattern
354
+
355
+ When a loop node uses `visual_selection: true`, each iteration presents a VS screen and collects a response. Use `get_web_response(task_id, node_id)` to access all previous iteration responses for that node:
356
+
357
+ ```json
358
+ {
359
+ "task_id": 19,
360
+ "node_id": 109,
361
+ "history": [
362
+ {
363
+ "iteration": 1,
364
+ "web_response": { "selections": ["a"] },
365
+ "created_at": "..."
366
+ },
367
+ {
368
+ "iteration": 2,
369
+ "web_response": { "selections": ["b"], "values": { "confidence": 80 } },
370
+ "created_at": "..."
371
+ }
372
+ ]
373
+ }
374
+ ```
375
+
376
+ Use the history to adapt subsequent VS screens - for example, pre-selecting the user's previous choice, adjusting slider defaults based on past values, or skipping already-confirmed items.
377
+
378
+ ## Graceful Interruption (중단 처리)
379
+
380
+ <HARD-RULE>
381
+ Whenever the user requests a stop mid-workflow — phrases like "stop", "pause", "cancel", "잠깐", "중단", "그만", "멈춰", or presses Ctrl+C — you MUST ask before exiting:
382
+
383
+ Ask via AskUserQuestion:
384
+
385
+ - header: "작업 중단"
386
+ - "현재 Step {N}에서 중단합니다. 어떻게 처리할까요?"
387
+ - options:
388
+ - "일시 중지 (나중에 이어서)" — leave task as `running`; it will auto-timeout after 2 hours of inactivity, and can be resumed next session
389
+ - "태스크 종료 (완전히 닫기)" — call `complete_task(task_id, summary="사용자 요청으로 중단됨")` to mark the task finished
390
+ - "계속 진행" — dismiss and continue the current step
391
+
392
+ If "일시 중지":
393
+
394
+ - Save the current progress to `context_snapshot` via `execute_step` (mark output as "작업이 일시 중지되었습니다. 다음 세션에서 이어서 진행 가능합니다.")
395
+ - Remind the user: "Task #{id}은 Step {N}에서 일시 중지되었습니다. 다음에 `/bk-start`를 실행하면 이어서 진행할 수 있습니다."
396
+
397
+ If "태스크 종료":
398
+
399
+ - Call `complete_task(task_id, summary="사용자 요청으로 중단됨. 마지막 완료 스텝: {N}")`.
400
+ - Remind the user: "태스크가 종료되었습니다. 처음부터 다시 시작하려면 `/bk-start`를 실행하세요."
401
+
402
+ **When not to prompt**: If ALL steps are already completed and `complete_task` is about to be called, skip this dialog — the workflow is naturally finishing.
403
+ </HARD-RULE>
404
+
405
+ ## Feedback Survey (before calling complete_task)
406
+
407
+ When the workflow finishes, run the feedback survey flow.
408
+ Follow the sequence: `save_feedback` → `complete_task` → suggest improvements.
@@ -8,27 +8,57 @@ user_invocable: true
8
8
 
9
9
  View the progress of active and completed tasks.
10
10
 
11
+ ## Argument Handling
12
+
13
+ - `/bk-status` → Show all running tasks. If none, show recent completed tasks.
14
+ - `/bk-status <task_id>` → Show detailed step-by-step log for that task.
15
+ - `/bk-status running` → Show only running tasks.
16
+ - `/bk-status completed` → Show only completed tasks.
17
+
11
18
  ## Execution Steps
12
19
 
13
20
  ### 1. Fetch Tasks
14
21
 
15
- Call `advance` with `peek: true` on any known active task, or check the server for running tasks.
22
+ Call `list_tasks` with optional filters:
23
+
24
+ - No argument: `list_tasks()` (all tasks) or `list_tasks(status="running")`
25
+ - With status filter: `list_tasks(status="running")` or `list_tasks(status="completed")`
26
+ - With task_id: `advance(task_id=<id>, peek=true)` for detailed view
16
27
 
17
- ### 2. Display Results
28
+ ### 2. Display Task List
18
29
 
19
30
  ```
20
31
  BlueKiwi Task Status
21
32
  ━━━━━━━━━━━━━━━━━━━━━━━━━
22
33
  #1 [completed] Feature Brainstorm 8/8 steps 2026-04-07
23
34
  #2 [running] PR Security Review 2/4 steps 2026-04-07 ← active
35
+ #3 [running] DB Migration Plan 5/7 steps 2026-04-08 ← active
24
36
  ━━━━━━━━━━━━━━━━━━━━━━━━━
37
+ Active: 2 tasks
25
38
  ```
26
39
 
27
40
  ### 3. Detailed View
28
41
 
29
- If a task number is given as an argument (`/bk-status 2`), show the step-by-step log for that task.
42
+ If a task number is given, call `advance(task_id=<id>, peek=true)` and show the step-by-step log:
43
+
44
+ ```
45
+ Task #2: PR Security Review (running)
46
+ ━━━━━━━━━━━━━━━━━━━━━━━━━
47
+ ✅ 1. Code Analysis completed 12s
48
+ ✅ 2. Dependency Check completed 8s
49
+ ⏸ 3. Security Review pending ← current (gate)
50
+ ○ 4. Report Generation —
51
+ ━━━━━━━━━━━━━━━━━━━━━━━━━
52
+ ```
53
+
54
+ ### 4. Offer Actions
55
+
56
+ After displaying, ask via AskUserQuestion:
57
+
58
+ - header: "Actions"
59
+ - options: ["Resume active task (/bk-start)", "View detailed task", "Done"]
30
60
 
31
61
  ## Notes
32
62
 
33
- - Web UI: http://localhost:3000/tasks
34
- - Real-time monitoring: Requires WebSocket Relay (`npm run ws`)
63
+ - `list_tasks` respects RBAC — only tasks on workflows the user can read are returned.
64
+ - For real-time monitoring, use the web UI: http://localhost:3000/tasks
@@ -11,7 +11,7 @@ export async function devLinkCommand() {
11
11
  for (const adapter of detectInstalledAdapters()) {
12
12
  const target = adapter.getSkillsDir();
13
13
  mkdirSync(target, { recursive: true });
14
- for (const skill of ["bk-start", "bk-next", "bk-status", "bk-rewind"]) {
14
+ for (const skill of ["bk-start", "bk-status", "bk-rewind"]) {
15
15
  const link = join(target, skill);
16
16
  if (existsSync(link)) {
17
17
  rmSync(link, { recursive: true, force: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bluekiwi",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "BlueKiwi CLI — install MCP client and skills into your agent runtime",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +0,0 @@
1
- export declare class BlueKiwiClient {
2
- private baseUrl;
3
- private apiKey;
4
- constructor(baseUrl: string, apiKey: string);
5
- request<T>(method: string, path: string, body?: unknown): Promise<T>;
6
- }
@@ -1,67 +0,0 @@
1
- import {
2
- BlueKiwiAuthError,
3
- BlueKiwiApiError,
4
- BlueKiwiNetworkError,
5
- } from "./errors.js";
6
- const RETRY_DELAYS_MS = [100, 500, 2000];
7
- export class BlueKiwiClient {
8
- baseUrl;
9
- apiKey;
10
- constructor(baseUrl, apiKey) {
11
- this.baseUrl = baseUrl;
12
- this.apiKey = apiKey;
13
- if (!baseUrl) throw new Error("BlueKiwiClient: baseUrl is required");
14
- if (!apiKey) throw new Error("BlueKiwiClient: apiKey is required");
15
- }
16
- async request(method, path, body) {
17
- const url = `${this.baseUrl.replace(/\/$/, "")}${path}`;
18
- const init = {
19
- method,
20
- headers: {
21
- "Content-Type": "application/json",
22
- Authorization: `Bearer ${this.apiKey}`,
23
- },
24
- body: body === undefined ? undefined : JSON.stringify(body),
25
- };
26
- let lastError;
27
- for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt++) {
28
- try {
29
- const res = await fetch(url, init);
30
- if (res.status === 401) {
31
- throw new BlueKiwiAuthError();
32
- }
33
- if (!res.ok) {
34
- const text = await res.text().catch(() => "");
35
- if (res.status >= 500 && attempt < RETRY_DELAYS_MS.length) {
36
- lastError = new BlueKiwiApiError(res.status, text);
37
- await sleep(RETRY_DELAYS_MS[attempt]);
38
- continue;
39
- }
40
- throw new BlueKiwiApiError(res.status, text);
41
- }
42
- const text = await res.text();
43
- return text ? JSON.parse(text) : null;
44
- } catch (err) {
45
- if (
46
- err instanceof BlueKiwiAuthError ||
47
- err instanceof BlueKiwiApiError
48
- ) {
49
- throw err;
50
- }
51
- lastError = err;
52
- if (attempt < RETRY_DELAYS_MS.length) {
53
- await sleep(RETRY_DELAYS_MS[attempt]);
54
- continue;
55
- }
56
- throw new BlueKiwiNetworkError(
57
- `Failed to reach ${url} after ${RETRY_DELAYS_MS.length + 1} attempts`,
58
- err,
59
- );
60
- }
61
- }
62
- throw new BlueKiwiNetworkError("Unreachable", lastError);
63
- }
64
- }
65
- function sleep(ms) {
66
- return new Promise((r) => setTimeout(r, ms));
67
- }
@@ -1,12 +0,0 @@
1
- export declare class BlueKiwiAuthError extends Error {
2
- constructor(message?: string);
3
- }
4
- export declare class BlueKiwiApiError extends Error {
5
- readonly status: number;
6
- readonly body: string;
7
- constructor(status: number, body: string);
8
- }
9
- export declare class BlueKiwiNetworkError extends Error {
10
- readonly cause?: unknown | undefined;
11
- constructor(message: string, cause?: unknown | undefined);
12
- }
@@ -1,24 +0,0 @@
1
- export class BlueKiwiAuthError extends Error {
2
- constructor(message = "Invalid or expired API key") {
3
- super(message);
4
- this.name = "BlueKiwiAuthError";
5
- }
6
- }
7
- export class BlueKiwiApiError extends Error {
8
- status;
9
- body;
10
- constructor(status, body) {
11
- super(`BlueKiwi API error ${status}: ${body}`);
12
- this.status = status;
13
- this.body = body;
14
- this.name = "BlueKiwiApiError";
15
- }
16
- }
17
- export class BlueKiwiNetworkError extends Error {
18
- cause;
19
- constructor(message, cause) {
20
- super(message);
21
- this.cause = cause;
22
- this.name = "BlueKiwiNetworkError";
23
- }
24
- }
@@ -1 +0,0 @@
1
- export {};