fix(pr4-review): address PR review issues and fix nightly build

- Fix HTTP-Referer URLs to point to DavinciDreams/Morse-Code
- Update src/package.json repository and homepage URLs
- Replace WeakRef<any>/setProvider(any) with typed TelemetryPropertiesProvider interface
- Remove dead AbortController boilerplate from RetryQueue.retryRequest
- Fix WebAuthService.initialize() to emit auth-state-changed event (fixes webview spinner)
- Fix nightly esbuild PKG_NAME to match stable so commands resolve correctly
- Fix .gitignore corruption (remove backtick-nnul, add proper 'nul' entry)
- Update .roo-hooks.json.example branding to Morse Code
- Update all stale test assertions (HTTP-Referer, X-Title, User-Agent)
- Add docs/cloud-backend.md — implementation plan for future cloud feature branch
- Add scripts/build-pr.js — PR test build tool with nightly slot support
- Update scripts/install-vsix.js branding

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
davincidreams 2026-04-27 03:23:15 -04:00
parent 11b0bdef9d
commit 272a6acfed
19 changed files with 396 additions and 83 deletions

4
.gitignore vendored
View file

@ -55,4 +55,6 @@ qdrant_storage/
plans/
roo-cli-*.tar.gz*
`nnul
# Windows null device artifact
nul

View file

@ -1,5 +1,5 @@
{
"_comment": "Example hooks configuration file for Roo Code. Rename to .roo-hooks.json to use.",
"_comment": "Example hooks configuration file for Morse Code. Rename to .roo-hooks.json to use.",
"_comment2": "Hooks are executed before (PreCompact) and after (PostCompact) context condensation.",
"_comment3": "Each hook can be a command (shell command) or http (HTTP POST request).",
"PreCompact": [

View file

@ -33,9 +33,9 @@ async function main() {
sourcesContent: false,
platform: "node",
define: {
"process.env.PKG_NAME": '"roo-code-nightly"',
"process.env.PKG_NAME": '"morse-code"',
"process.env.PKG_VERSION": `"${overrideJson.version}"`,
"process.env.PKG_OUTPUT_CHANNEL": '"Roo-Code-Nightly"',
"process.env.PKG_OUTPUT_CHANNEL": '"Morse-Code-Nightly"',
...(gitSha ? { "process.env.PKG_SHA": `"${gitSha}"` } : {}),
},
}

186
docs/cloud-backend.md Normal file
View file

@ -0,0 +1,186 @@
# Morse Code Cloud Backend — Implementation Plan
**Status:** Planned — future feature branch (`feature/morse-cloud-backend`)
The extension's cloud package (`packages/cloud/`) was disabled in the standalone fork (PR #4).
This document sketches what's needed to re-enable it against a Morse Code backend.
---
## Prerequisites
| Item | Status |
| ----------------------------------- | ----------------------------------------------- |
| Clerk account | ✓ Done |
| `CLERK_BASE_URL` in .env | ✓ Done (`closing-mantis-16.clerk.accounts.dev`) |
| Restore stubbed cloud code from git | Pending |
| Morse Code backend deployed | Pending |
| Sign-in landing page | Pending |
---
## Step 1 — Restore stubbed cloud code
Three files were gutted in PR #4 and need to be restored from git history:
```bash
git show 2fc779842^:packages/cloud/src/WebAuthService.ts > packages/cloud/src/WebAuthService.ts
git show 2fc779842^:packages/cloud/src/TelemetryClient.ts > packages/cloud/src/TelemetryClient.ts
git show 2fc779842^:packages/cloud/src/retry-queue/RetryQueue.ts > packages/cloud/src/retry-queue/RetryQueue.ts
```
Then update `packages/cloud/src/config.ts` to point at the Morse Code backend:
```ts
export const PRODUCTION_CLERK_BASE_URL = "https://closing-mantis-16.clerk.accounts.dev"
export const PRODUCTION_ROO_CODE_API_URL = "https://api.morse-code.com"
```
---
## Step 2 — Backend API (9 endpoints)
The extension calls two base URLs — Clerk (auth) and the Morse Code API (everything else).
### Clerk handles automatically (no backend code needed)
| Method | Path | Purpose |
| ------ | --------------------------------- | --------------------------- |
| `POST` | `/v1/client/sign_ins` | Exchange ticket → sessionId |
| `POST` | `/v1/client/sessions/{id}/tokens` | sessionId → JWT |
| `GET` | `/v1/me` | Current user info |
| `GET` | `/v1/me/organization_memberships` | Org list |
| `POST` | `/v1/client/sessions/{id}/remove` | Logout |
### Morse Code API to build
#### Auth flow glue
```
GET /extension/sign-in?state={csrf}
```
Landing page that starts the Clerk OAuth flow and redirects back to the extension
with `vscode://morse-code/auth?code={ticket}&state={csrf}`.
#### Settings
```
GET /api/extension-settings
Authorization: Bearer {jwt}
→ { organization: OrganizationSettings, user: UserSettingsData }
PATCH /api/user-settings
Authorization: Bearer {jwt}
Body: { settings: Partial<UserSettingsConfig>, version?: number }
→ UserSettingsData
```
`OrganizationSettings` shape (from `packages/types/src/`):
```ts
{
version: number
cloudSettings: { taskSyncEnabled: boolean }
defaultSettings: GlobalSettings
allowList: string[] // allowed shell commands
features: Record<string, boolean>
hiddenMcps: string[]
}
```
#### Telemetry
```
POST /api/events
Authorization: Bearer {jwt}
Body: { type: TelemetryEventName, properties: Record<string, unknown> }
POST /api/events/backfill
Authorization: Bearer {jwt}
Body: FormData { taskId, properties (JSON string), file (JSON array of ClineMessage[]) }
```
Return `429` with `Retry-After` header to rate-limit — the client will pause and retry automatically.
#### Task sharing
```
POST /api/extension/share
Authorization: Bearer {jwt}
Body: { taskId: string, visibility: "organization" | "public" }
→ { success: boolean, shareUrl?: string, isNewShare?: boolean, manageUrl?: string }
```
Return `404` if `taskId` is unknown — the extension will automatically backfill the message
history via `/api/events/backfill` and retry.
#### Optional / lower priority
```
GET /api/extension/credit-balance
→ { balance: number }
GET /api/extension/bridge/config
→ { userId: string, socketBridgeUrl: string, token: string }
```
---
## Step 3 — JWT verification
All `/api/*` routes must verify the Clerk JWT. Use the Clerk backend SDK:
```ts
import { createClerkClient } from "@clerk/backend"
const clerk = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY })
async function verifyToken(req) {
const token = req.headers.authorization?.replace("Bearer ", "")
const payload = await clerk.verifyToken(token)
return { userId: payload.sub, orgId: payload.org_id }
}
```
---
## Step 4 — Sign-in landing page
The simplest approach is a single Next.js route (or Express endpoint):
```
/extension/sign-in?state={csrf}&landingPageSlug={slug}
```
1. Start Clerk sign-in with `strategy: "oauth_google"` (or show a sign-in form)
2. On success, get the one-time ticket from Clerk
3. Redirect to `vscode://morse-code/auth?code={ticket}&state={csrf}`
The extension's `handleCallback()` takes it from there.
---
## Tech stack recommendation
| Layer | Recommendation |
| -------------- | --------------------------------------------------------- |
| Runtime | Node.js (Bun or Express) — Clerk SDK is Node-first |
| Auth | Clerk (already have account) |
| Database | Postgres (for settings, task shares, telemetry) |
| Hosting | Fly.io / Railway / Vercel (easy Postgres integration) |
| Telemetry sink | Write to DB initially; swap for ClickHouse later at scale |
---
## Environment variables summary
```bash
# Extension (.env at repo root)
CLERK_BASE_URL=https://closing-mantis-16.clerk.accounts.dev
ROO_CODE_API_URL=https://api.morse-code.com
# Backend server
CLERK_SECRET_KEY=sk_test_...
DATABASE_URL=postgres://...
```

View file

@ -21,6 +21,7 @@
"clean": "turbo clean --log-order grouped --output-logs new-only && rimraf dist out bin .vite-port .turbo",
"install:vsix": "pnpm install --frozen-lockfile && pnpm clean && pnpm vsix && node scripts/install-vsix.js",
"install:vsix:nightly": "pnpm install --frozen-lockfile && pnpm clean && pnpm vsix:nightly && node scripts/install-vsix.js --nightly",
"test:pr": "node scripts/build-pr.js",
"code-server:install": "node scripts/code-server.js",
"changeset:version": "cp CHANGELOG.md src/CHANGELOG.md && changeset version && cp -vf src/CHANGELOG.md .",
"knip": "knip --include files",

View file

@ -8,9 +8,12 @@ import {
TelemetryEventSubscription,
} from "@roo-code/types"
interface TelemetryPropertiesProvider {
getTelemetryProperties(): Promise<TelemetryEvent["properties"]>
}
abstract class BaseTelemetryClient implements TelemetryClient {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected providerRef: WeakRef<any> | null = null
protected providerRef: WeakRef<TelemetryPropertiesProvider> | null = null
protected telemetryEnabled: boolean = false
constructor(
@ -67,8 +70,7 @@ abstract class BaseTelemetryClient implements TelemetryClient {
public async captureException(_error: Error, _additionalProperties?: Record<string, unknown>): Promise<void> {}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public setProvider(provider: any): void {
public setProvider(provider: TelemetryPropertiesProvider): void {
this.providerRef = new WeakRef(provider)
}

View file

@ -36,7 +36,9 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
}
// Start in logged-out state — no credentials loading, no timer, no HTTP calls.
const previousState = this.state
this.state = "logged-out"
this.emit("auth-state-changed", { state: this.state, previousState })
this.log("[auth] Cloud features disabled — initialized in logged-out state")
}

View file

@ -1,4 +1,4 @@
import { EventEmitter } from "events"
import { EventEmitter } from "events"
import type { ExtensionContext } from "vscode"
import type { QueuedRequest, QueueStats, RetryQueueConfig, RetryQueueEvents } from "./types.js"
@ -70,7 +70,7 @@ export class RetryQueue extends EventEmitter<RetryQueueEvents> {
}
}
// Cloud features disabled — enqueue is a no-op to prevent any HTTP calls
// Cloud features disabled - enqueue is a no-op to prevent any HTTP calls
public async enqueue(
_url: string,
_options: RequestInit,
@ -173,47 +173,9 @@ export class RetryQueue extends EventEmitter<RetryQueueEvents> {
}
}
private async retryRequest(request: QueuedRequest): Promise<Response> {
this.log(`[RetryQueue] Retrying request: ${request.url}`)
// Cloud features disabled — skip actual HTTP fetch, return fake success
this.log(`[RetryQueue] Cloud features disabled - skipping request to ${request.url}`)
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), this.config.requestTimeout)
try {
// Return a fake successful response instead of making a real HTTP call
const response = new Response(JSON.stringify({ success: true }), {
status: 200,
statusText: "OK",
headers: { "Content-Type": "application/json" },
})
clearTimeout(timeoutId)
// Check for error status codes that should trigger retry
if (!response.ok) {
// Handle different status codes appropriately
if (response.status >= 500) {
// Server errors (5xx) should be retried
throw new Error(`Server error: ${response.status} ${response.statusText}`)
} else if (response.status === 429) {
// Rate limiting - return response to let caller handle Retry-After
return response
} else if (response.status >= 400 && response.status < 500) {
// Client errors (4xx including 401/403) should NOT be retried
// These errors indicate problems with the request itself that won't be fixed by retrying
this.log(`[RetryQueue] Non-retryable client error ${response.status}, removing from queue`)
return response
}
}
return response
} catch (error) {
clearTimeout(timeoutId)
throw error
}
private async retryRequest(_request: QueuedRequest): Promise<Response> {
// Cloud features disabled - retries never execute
throw new Error("Cloud features disabled")
}
private startRetryTimer(): void {

158
scripts/build-pr.js Normal file
View file

@ -0,0 +1,158 @@
#!/usr/bin/env node
/**
* Build and install a PR-stamped VSIX for local testing.
*
* Usage:
* node scripts/build-pr.js --pr=4 # stable slot: morse-code-3.52.1-pr.4.vsix
* node scripts/build-pr.js --pr=4 --nightly # nightly slot: morse-code-nightly-0.4.0.vsix (coexists with stable)
* node scripts/build-pr.js --pr=4 -y # non-interactive
* node scripts/build-pr.js --pr=4 --editor=cursor
* node scripts/build-pr.js --pr=4 --no-install # build only, skip install
*
* --nightly installs into the separate morse-code-nightly extension slot so it
* coexists with your stable morse-code build. Use this during PR testing.
*/
const { execSync } = require("child_process")
const fs = require("fs")
const path = require("path")
const readline = require("readline")
// ── Args ──────────────────────────────────────────────────────────────────────
const args = process.argv.slice(2)
const prArg = args.find((a) => a.startsWith("--pr="))
if (!prArg) {
console.error(
"Usage: node scripts/build-pr.js --pr=<number> [--nightly] [-y] [--editor=code|cursor] [--no-install]",
)
process.exit(1)
}
const prNumber = prArg.split("=")[1]
const isNightly = args.includes("--nightly")
const autoYes = args.includes("-y")
const noInstall = args.includes("--no-install")
const editorArg = args.find((a) => a.startsWith("--editor="))
const editor = editorArg ? editorArg.split("=")[1] : "code"
// ── Paths & metadata ──────────────────────────────────────────────────────────
const stablePkgPath = path.resolve(__dirname, "../src/package.json")
const nightlyPkgPath = path.resolve(__dirname, "../apps/vscode-nightly/package.nightly.json")
const stablePkg = JSON.parse(fs.readFileSync(stablePkgPath, "utf-8"))
let pkgPath, pkg, prVersion, vsixName, extensionId
if (isNightly) {
const nightlyPkg = JSON.parse(fs.readFileSync(nightlyPkgPath, "utf-8"))
pkgPath = nightlyPkgPath
pkg = nightlyPkg
// Use 0.<pr>.<build> so version increments clearly per PR
prVersion = `0.${prNumber}.0`
vsixName = `./bin/${nightlyPkg.name}-${prVersion}.vsix`
extensionId = `${stablePkg.publisher}.${nightlyPkg.name}`
} else {
pkgPath = stablePkgPath
pkg = stablePkg
const baseVersion = stablePkg.version.split("-")[0]
prVersion = `${baseVersion}-pr.${prNumber}`
vsixName = `./bin/${stablePkg.name}-${prVersion}.vsix`
extensionId = `${stablePkg.publisher}.${stablePkg.name}`
}
// ── Helpers ───────────────────────────────────────────────────────────────────
function patchVersion(version) {
const updated = { ...pkg, version }
fs.writeFileSync(pkgPath, JSON.stringify(updated, null, "\t") + "\n")
}
function restoreVersion() {
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, "\t") + "\n")
}
function run(cmd) {
execSync(cmd, { stdio: "inherit", shell: true })
}
// On Windows, the VS Code CLI is code.cmd not code.exe
function resolveEditor(name) {
if (process.platform !== "win32") return name
const { execSync: ex } = require("child_process")
try {
const p = ex(`where ${name}.cmd 2>nul`, { encoding: "utf8" }).trim().split("\n")[0].trim()
if (p) return `"${p}"`
} catch {}
return name
}
// ── Main ──────────────────────────────────────────────────────────────────────
async function main() {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
const ask = (q) => new Promise((r) => rl.question(q, r))
const resolvedEditor = resolveEditor(editor)
const slot = isNightly ? "nightly (coexists with stable)" : "stable"
console.log("\nMorse Code - PR Build Tool")
console.log("==========================")
console.log(` PR: #${prNumber}`)
console.log(` Slot: ${slot}`)
console.log(` Version: ${pkg.version} -> ${prVersion}`)
console.log(` VSIX: ${vsixName}`)
if (!noInstall) {
console.log(` Editor: ${editor}`)
console.log(` Extension: ${extensionId}`)
}
console.log()
if (!autoYes) {
const answer = await ask("Continue? (y/n): ")
if (answer.toLowerCase() !== "y") {
console.log("Cancelled.")
rl.close()
return
}
}
rl.close()
console.log(`\n-> Patching version to ${prVersion}...`)
patchVersion(prVersion)
try {
const buildCmd = isNightly ? "pnpm vsix:nightly" : "pnpm vsix"
console.log(`-> Building VSIX (${buildCmd})...`)
run(buildCmd)
console.log(`\nBuilt: ${vsixName}`)
if (!noInstall) {
if (!fs.existsSync(vsixName)) {
console.error(`\nVSIX not found at ${vsixName}`)
process.exit(1)
}
console.log(`-> Installing ${vsixName}...`)
run(`${resolvedEditor} --install-extension ${vsixName}`)
console.log(`\nInstalled PR #${prNumber} build (${prVersion}) into ${slot} slot`)
console.log("Restart VS Code to activate the updated extension.\n")
}
} finally {
console.log(`-> Restoring version to ${pkg.version}...`)
restoreVersion()
}
}
main().catch((err) => {
console.error("\nBuild failed:", err.message)
try {
restoreVersion()
} catch {}
process.exit(1)
})

View file

@ -49,10 +49,10 @@ async function main() {
const extensionId = `${publisher}.${name}`
const buildType = isNightly ? "Nightly" : "Regular"
console.log(`\n🚀 Roo Code VSIX Installer (${buildType})`)
console.log(`\n🚀 Morse Code VSIX Installer (${buildType})`)
console.log("========================")
console.log("\nThis script will:")
console.log("1. Uninstall any existing version of the Roo Code extension")
console.log("1. Uninstall any existing version of the Morse Code extension")
console.log("2. Install the newly built VSIX package")
console.log(`\nExtension: ${extensionId}`)
console.log(`VSIX file: ${vsixFileName}`)

View file

@ -11,16 +11,16 @@ describe("DEFAULT_HEADERS", () => {
})
it("should have correct HTTP-Referer value", () => {
expect(DEFAULT_HEADERS["HTTP-Referer"]).toBe("https://github.com/RooVetGit/Roo-Cline")
expect(DEFAULT_HEADERS["HTTP-Referer"]).toBe("https://github.com/DavinciDreams/Morse-Code")
})
it("should have correct X-Title value", () => {
expect(DEFAULT_HEADERS["X-Title"]).toBe("Roo Code")
expect(DEFAULT_HEADERS["X-Title"]).toBe("Morse Code")
})
it("should have correct User-Agent format", () => {
const userAgent = DEFAULT_HEADERS["User-Agent"]
expect(userAgent).toBe(`RooCode/${Package.version}`)
expect(userAgent).toBe(`MorseCode/${Package.version}`)
// Verify it follows the tool_name/version pattern
expect(userAgent).toMatch(/^[a-zA-Z-]+\/\d+\.\d+\.\d+$/)
@ -28,7 +28,7 @@ describe("DEFAULT_HEADERS", () => {
it("should have User-Agent with correct tool name", () => {
const userAgent = DEFAULT_HEADERS["User-Agent"]
expect(userAgent.startsWith("RooCode/")).toBe(true)
expect(userAgent.startsWith("MorseCode/")).toBe(true)
})
it("should have User-Agent with semantic version format", () => {

View file

@ -111,9 +111,9 @@ describe("OpenAiHandler", () => {
baseURL: expect.any(String),
apiKey: expect.any(String),
defaultHeaders: {
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
"X-Title": "Roo Code",
"User-Agent": `RooCode/${Package.version}`,
"HTTP-Referer": "https://github.com/DavinciDreams/Morse-Code",
"X-Title": "Morse Code",
"User-Agent": `MorseCode/${Package.version}`,
},
timeout: expect.any(Number),
})

View file

@ -100,9 +100,9 @@ describe("OpenRouterHandler", () => {
baseURL: "https://openrouter.ai/api/v1",
apiKey: mockOptions.openRouterApiKey,
defaultHeaders: {
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
"X-Title": "Roo Code",
"User-Agent": `RooCode/${Package.version}`,
"HTTP-Referer": "https://github.com/DavinciDreams/Morse-Code",
"X-Title": "Morse Code",
"User-Agent": `MorseCode/${Package.version}`,
},
})
})

View file

@ -58,9 +58,9 @@ describe("RequestyHandler", () => {
baseURL: "https://router.requesty.ai/v1",
apiKey: mockOptions.requestyApiKey,
defaultHeaders: {
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
"X-Title": "Roo Code",
"User-Agent": `RooCode/${Package.version}`,
"HTTP-Referer": "https://github.com/DavinciDreams/Morse-Code",
"X-Title": "Morse Code",
"User-Agent": `MorseCode/${Package.version}`,
},
})
})
@ -73,9 +73,9 @@ describe("RequestyHandler", () => {
baseURL: "https://custom.requesty.ai/v1",
apiKey: mockOptions.requestyApiKey,
defaultHeaders: {
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
"X-Title": "Roo Code",
"User-Agent": `RooCode/${Package.version}`,
"HTTP-Referer": "https://github.com/DavinciDreams/Morse-Code",
"X-Title": "Morse Code",
"User-Agent": `MorseCode/${Package.version}`,
},
})
})

View file

@ -96,9 +96,9 @@ describe("VercelAiGatewayHandler", () => {
baseURL: "https://ai-gateway.vercel.sh/v1",
apiKey: mockOptions.vercelAiGatewayApiKey,
defaultHeaders: expect.objectContaining({
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
"X-Title": "Roo Code",
"User-Agent": expect.stringContaining("RooCode/"),
"HTTP-Referer": "https://github.com/DavinciDreams/Morse-Code",
"X-Title": "Morse Code",
"User-Agent": expect.stringContaining("MorseCode/"),
}),
})
})

View file

@ -1,7 +1,7 @@
import { Package } from "../../shared/package"
export const DEFAULT_HEADERS = {
"HTTP-Referer": "https://github.com/moo-code/Moo-Code",
"X-Title": "Moo Code",
"User-Agent": `MooCode/${Package.version}`,
"HTTP-Referer": "https://github.com/DavinciDreams/Morse-Code",
"X-Title": "Morse Code",
"User-Agent": `MorseCode/${Package.version}`,
}

View file

@ -70,8 +70,8 @@ export async function generateImageWithProvider(options: ImageGenerationOptions)
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"HTTP-Referer": "https://github.com/moo-code/Moo-Code",
"X-Title": "Moo Code",
"HTTP-Referer": "https://github.com/DavinciDreams/Morse-Code",
"X-Title": "Morse Code",
},
body: JSON.stringify({
model,
@ -216,8 +216,8 @@ export async function generateImageWithImagesApi(options: ImagesApiOptions): Pro
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json",
"HTTP-Referer": "https://github.com/moo-code/Moo-Code",
"X-Title": "Moo Code",
"HTTP-Referer": "https://github.com/DavinciDreams/Morse-Code",
"X-Title": "Morse Code",
},
body: JSON.stringify(requestBody),
}

View file

@ -18,9 +18,9 @@
},
"repository": {
"type": "git",
"url": "https://github.com/DavinciDreams/Roo-Code"
"url": "https://github.com/DavinciDreams/Morse-Code"
},
"homepage": "https://github.com/DavinciDreams/Roo-Code",
"homepage": "https://github.com/DavinciDreams/Morse-Code",
"categories": [
"AI",
"Chat",

View file

@ -76,8 +76,8 @@ export class OpenRouterEmbedder implements IEmbedder {
baseURL: this.baseUrl,
apiKey: apiKey,
defaultHeaders: {
"HTTP-Referer": "https://github.com/moo-code/Moo-Code",
"X-Title": "Moo Code",
"HTTP-Referer": "https://github.com/DavinciDreams/Morse-Code",
"X-Title": "Morse Code",
},
})
} catch (error) {