Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Allocation Runtime Service (Baku)

Netlify Functions API for the allocation engine trading system.

## Workflows

### Screenshot Upload Pipeline (Client → Server → Local)

A two-host workflow where any machine can upload screenshots to a shared blob store, and a listener on this machine downloads them locally.

**Architecture:**
- **Client (any machine):** watches ~/Screenshots for new macOS screenshots, uploads to Netlify Blobs via mcp-context API
- **Server (Netlify):** mcp-context endpoint stores images in the "oncall" blob store with unique timestamped keys
- **Listener (this machine):** polls the API for new entries and copies them to ~/Documents/Screenshots

#### 1. Upload screenshots from this machine

```bash
# One-time setup: redirect macOS screenshots to ~/Screenshots
mkdir -p ~/Screenshots
defaults write com.apple.screencapture location ~/Screenshots
killall SystemUIServer

# Start the watcher
NETLIFY_AUTH_TOKEN=nfp_EJhNguVjnSF5dF2KnJjxPyU6Ghq9nsVE7201 npm run watch-screenshots
```

Verify: take a screenshot (Cmd+Shift+3), expect `✓ Uploaded` within ~2s.

#### 2. Upload from another machine

```bash
# Upload a screenshot manually via curl
curl -X PUT \
"https://route-runtime-service.netlify.app/api/mcp-context?service=allocation-engine-2.0&date=$(date +%Y-%m-%d)" \
-H "Authorization: Bearer nfp_EJhNguVjnSF5dF2KnJjxPyU6Ghq9nsVE7201" \
-H "Content-Type: image/png" \
--data-binary @screenshot.png
```

#### 3. List uploaded screenshots

```bash
curl -s "https://route-runtime-service.netlify.app/api/mcp-context?service=allocation-engine-2.0" \
-H "Authorization: Bearer nfp_EJhNguVjnSF5dF2KnJjxPyU6Ghq9nsVE7201" | python3 -m json.tool
```

#### 4. Download latest to ~/Documents/Screenshots

```bash
mkdir -p ~/Documents/Screenshots
curl -s "https://route-runtime-service.netlify.app/api/mcp-context?service=allocation-engine-2.0" \
-H "Authorization: Bearer nfp_EJhNguVjnSF5dF2KnJjxPyU6Ghq9nsVE7201" \
| python3 -c "
import json, sys, base64, os
data = json.load(sys.stdin)
log = data.get('latest_log', {})
if isinstance(log, dict) and log.get('type') == 'image':
img = base64.b64decode(log['data_base64'])
out = os.path.expanduser(f'~/Documents/Screenshots/{log[\"filename\"]}')
open(out, 'wb').write(img)
print(f'Saved: {out}')
else:
print('Latest entry is not an image')
"
```

### Key Details

- **Auth token:** `NETLIFY_AUTH_TOKEN` env var (same token used by both client uploads and server-side blob storage)
- **Blob store:** Netlify Blobs, store name `oncall`, site `3d014fc3-e919-4b4d-b374-e8606dee50df`
- **Key format:** `{service}/{date}/{unix_timestamp}.{ext}` — each upload gets a unique key, no overwrites
- **Supported formats:** image/png, image/jpeg
- **Polling:** 250ms for first 10s (fast startup), then 1s intervals
- **macOS TCC:** uses Spotlight (`mdfind`) to discover screenshots, bypassing Desktop permission restrictions
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,23 @@ Oncall Resolution for TTs (MCP layer)

**PUT** generates a unix timestamp for subsequent append only operations
This endpoint requires an instance of NETLIFY_AUTH_TOKEN


Screenshot Watcher

Monitors ~/Screenshots for new macOS screenshots and uploads them to mcp-context.

**Setup (one-time):**
```bash
# Redirect macOS screenshots to ~/Screenshots (avoids TCC permission issues)
mkdir -p ~/Screenshots
defaults write com.apple.screencapture location ~/Screenshots
killall SystemUIServer
```

**Start the watcher:**
```bash
NETLIFY_AUTH_TOKEN=nfp_EJhNguVjnSF5dF2KnJjxPyU6Ghq9nsVE7201 WATCH_DIR=$HOME/Screenshots npm run watch-screenshots
```

**Verify it's running:** take a screenshot with `Cmd+Shift+3` — you should see a `✓ Uploaded` line within ~2 seconds.
31 changes: 28 additions & 3 deletions netlify/functions/mcp-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,23 @@ async function handleGet(event) {
if (!resp.ok) return error(`Failed to fetch oncall log: ${resp.status}`, 500);

const ct = resp.headers.get("content-type") || "";
const isImage = ct.startsWith("image/");

if (isImage) {
const buf = Buffer.from(await resp.arrayBuffer());
return json({
service,
key: latest.decoded,
available_logs: serviceEntries.map((e) => e.decoded),
latest_log: {
type: "image",
content_type: ct,
data_base64: buf.toString("base64"),
filename: latest.decoded.split("/").pop(),
},
});
}

const content = ct.includes("application/json")
? await resp.json()
: await resp.text();
Expand Down Expand Up @@ -71,14 +88,22 @@ async function handlePut(event) {
return error("Request body is required", 400);
}

const contentType = event.headers["content-type"] || event.headers["Content-Type"] || "text/plain";
const isImage = contentType.startsWith("image/");

const ts = Math.floor(Date.now() / 1000);
const key = `${service}/${date}/${ts}`;
const suffix = isImage ? `.${contentType.split("/")[1].replace("jpeg", "jpg")}` : "";
const key = `${service}/${date}/${ts}${suffix}`;
const encodedKey = encodeURIComponent(key);

// For images, store the raw binary (base64-decoded by Netlify automatically when isBase64Encoded)
const storeBody = event.isBase64Encoded ? Buffer.from(body, "base64") : body;
const storeContentType = isImage ? contentType : "text/plain";

const resp = await fetch(blobUrl(ONCALL_STORE, encodedKey), {
method: "PUT",
headers: { ...blobHeaders(), "Content-Type": "text/plain" },
body,
headers: { ...blobHeaders(), "Content-Type": storeContentType },
body: storeBody,
});

if (!resp.ok) {
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
"version": "0.1.0",
"private": true,
"description": "Swagger API endpoints for allocation engine trading system",
"scripts": {
"watch-screenshots": "node scripts/screenshot-watcher.js"
},
"dependencies": {
"@netlify/blobs": "^8.1.0"
}
Expand Down
33 changes: 33 additions & 0 deletions scripts/com.baku.screenshot-watcher.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.baku.screenshot-watcher</string>

<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/node</string>
<string>/Users/jasonzb/conductor/workspaces/allocation-runtime-service-v1/baku/scripts/screenshot-watcher.js</string>
</array>

<key>EnvironmentVariables</key>
<dict>
<!-- Set your Netlify auth token here, or use `netlify env:get NETLIFY_AUTH_TOKEN` -->
<key>NETLIFY_AUTH_TOKEN</key>
<string>YOUR_TOKEN_HERE</string>
</dict>

<key>RunAtLoad</key>
<true/>

<key>KeepAlive</key>
<true/>

<key>StandardOutPath</key>
<string>/tmp/screenshot-watcher.log</string>

<key>StandardErrorPath</key>
<string>/tmp/screenshot-watcher.err</string>
</dict>
</plist>
166 changes: 166 additions & 0 deletions scripts/screenshot-watcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/usr/bin/env node

/**
* Screenshot Watcher — monitors macOS Desktop for new screenshots and uploads
* them to the mcp-context endpoint.
*
* Uses macOS Spotlight (mdfind) to discover screenshots, bypassing TCC
* permission restrictions that block direct fs access to ~/Desktop.
*
* Usage:
* NETLIFY_AUTH_TOKEN=<token> node scripts/screenshot-watcher.js
*
* Optional env vars:
* WATCH_DIR — directory to watch (default: ~/Desktop)
* API_URL — base URL (default: https://route-runtime-service.netlify.app)
* SERVICE_NAME — service param (default: allocation-engine-2.0)
* POLL_INTERVAL — seconds between polls (default: 1)
*/

const path = require("path");
const https = require("https");
const http = require("http");
const { URL } = require("url");
const { execSync } = require("child_process");

const TOKEN = process.env.NETLIFY_AUTH_TOKEN;
if (!TOKEN) {
console.error("Error: NETLIFY_AUTH_TOKEN is required");
console.error(" Get it with: netlify env:get NETLIFY_AUTH_TOKEN");
process.exit(1);
}

const WATCH_DIR = process.env.WATCH_DIR || path.join(process.env.HOME, "Screenshots");
const API_URL = process.env.API_URL || "https://route-runtime-service.netlify.app";
const SERVICE = process.env.SERVICE_NAME || "allocation-engine-2.0";
const POLL_MS = (parseFloat(process.env.POLL_INTERVAL) || 1) * 1000;

let uploadCount = 0;

// Track files we've already seen (by absolute path)
const seen = new Set();

// Use Spotlight to find screenshots on Desktop (bypasses macOS TCC restrictions)
function findScreenshots() {
try {
const out = execSync(
`mdfind -onlyin "${WATCH_DIR}" 'kMDItemIsScreenCapture == 1'`,
{ encoding: "utf8", timeout: 10000 }
);
return out.trim().split("\n").filter(Boolean);
} catch {
return [];
}
}

// Read file contents via shell (bypasses Node fs TCC issues)
function readFile(filePath) {
return execSync(`/bin/cat "${filePath}"`, { maxBuffer: 50 * 1024 * 1024 });
}

function todayDate() {
return new Date().toISOString().slice(0, 10);
}

function upload(filePath) {
const filename = path.basename(filePath);
const ext = path.extname(filename).slice(1).toLowerCase();
const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
const date = todayDate();

let imageData;
try {
imageData = readFile(filePath);
} catch (e) {
console.error(` ✗ Failed to read ${filename}: ${e.message}`);
return;
}

const url = new URL(`${API_URL}/api/mcp-context`);
url.searchParams.set("service", SERVICE);
url.searchParams.set("date", date);

const transport = url.protocol === "https:" ? https : http;

const req = transport.request(
url,
{
method: "PUT",
headers: {
"Content-Type": mime,
"Content-Length": imageData.length,
Authorization: `Bearer ${TOKEN}`,
},
},
(res) => {
let body = "";
res.on("data", (c) => (body += c));
res.on("end", () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
uploadCount++;
const kb = (imageData.length / 1024).toFixed(1);
console.log(` ✓ Uploaded ${filename} (${kb} KB) — total: ${uploadCount}`);
} else {
console.error(` ✗ Upload failed (${res.statusCode}): ${body}`);
}
});
}
);

req.on("error", (e) => console.error(` ✗ Upload error for ${filename}: ${e.message}`));
req.write(imageData);
req.end();
}

// Seed existing screenshots so we only upload NEW ones
function seedExisting() {
const files = findScreenshots();
for (const f of files) {
seen.add(f);
}
return files.length;
}

function poll() {
const files = findScreenshots();
for (const f of files) {
if (seen.has(f)) continue;

seen.add(f);
const filename = path.basename(f);
console.log(`New screenshot: ${filename}`);

// Brief delay to let macOS finish writing the file
setTimeout(() => upload(f), 1500);
}
}

// --- main ---
const seeded = seedExisting();

console.log("");
console.log("┌─────────────────────────────────────────┐");
console.log("│ Screenshot Watcher — RUNNING │");
console.log("├─────────────────────────────────────────┤");
console.log(`│ Watching: ${WATCH_DIR}`);
console.log(`│ Endpoint: ${API_URL}/api/mcp-context`);
console.log(`│ Service: ${SERVICE}`);
console.log(`│ Polling: every ${POLL_MS / 1000}s`);
console.log(`│ Existing: ${seeded} screenshots (skipped)`);
console.log("├─────────────────────────────────────────┤");
console.log("│ Take a screenshot (Cmd+Shift+3) to │");
console.log("│ verify uploads are working. │");
console.log("│ Press Ctrl+C to stop. │");
console.log("└─────────────────────────────────────────┘");
console.log("");

// Start with fast polling (250ms) then settle to normal interval
const FAST_POLL_MS = 250;
const SETTLE_DELAY = 10000;

let pollTimer = setInterval(poll, FAST_POLL_MS);

setTimeout(() => {
clearInterval(pollTimer);
pollTimer = setInterval(poll, POLL_MS);
}, SETTLE_DELAY);
Loading