Files
claw-code-parity/ts-worker/state/FeedStore.ts
T
Wylabb f7c23fb325
Build Claw Telegram / build (push) Successful in 5m26s
Build Claw Telegram / cleanup (push) Successful in 0s
Replace Rust worker with TS worker in single image
Add ts-worker/ with the Bun/TypeScript worker that replaces
claw-profile-worker. The Dockerfile now builds a single image
containing both the Rust gateway (claw-telegram) and the TS worker.

The image defaults to worker mode (bun run ts-worker/main.ts).
The gateway Unraid XML overrides with --entrypoint claw-telegram.
Worker containers use the same image with the default CMD.

- Add ts-worker/ (12 files): HTTP/SSE server, Anthropic SDK engine,
  approval broker, event translator, state stores
- Add package.json with @anthropic-ai/sdk dependency
- Rewrite Dockerfile: three-stage build (Rust + Bun + runtime)
- Revert CLAW_GATEWAY_WORKER_IMAGE to claw-telegram:latest
- Remove image pull from docker_worker_manager (same image, already local)
- Add ts-worker paths to CI trigger

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 23:44:57 +02:00

122 lines
3.2 KiB
TypeScript

/**
* FeedStore — manages feed items and their payload files.
*
* Feed items are non-text outputs from Claude (images, HTML previews,
* code files, diffs, etc.) that get surfaced in the Mini App.
*/
import { randomUUID } from 'crypto'
import { readFile, writeFile, mkdir, readdir, stat } from 'fs/promises'
import { join, basename } from 'path'
import type { FeedItemRecord, FeedItemKind, FeedItemSource } from './records.js'
export class FeedStore {
private items = new Map<string, FeedItemRecord>()
private stateRoot: string
private payloadDir: string
constructor(stateRoot: string) {
this.stateRoot = stateRoot
this.payloadDir = join(stateRoot, 'claw', 'feed')
}
async init(): Promise<void> {
await mkdir(this.payloadDir, { recursive: true })
// Load existing feed index if present
try {
const indexPath = join(this.payloadDir, 'index.json')
const data = await readFile(indexPath, 'utf8')
const records = JSON.parse(data) as FeedItemRecord[]
for (const r of records) {
this.items.set(r.feed_item_id, r)
}
} catch {
// No existing index — start fresh
}
}
async addItem(
title: string,
kind: FeedItemKind,
source: FeedItemSource,
payload?: Uint8Array,
options?: {
mediaType?: string
fileName?: string
linkedAppId?: string
previewText?: string
},
): Promise<FeedItemRecord> {
const id = randomUUID()
const now = Date.now()
let storedPath: string | undefined
if (payload) {
const ext = options?.fileName
? basename(options.fileName)
: id
storedPath = join(this.payloadDir, ext)
await writeFile(storedPath, payload)
}
const record: FeedItemRecord = {
feed_item_id: id,
title,
kind,
source,
linked_app_id: options?.linkedAppId,
media_type: options?.mediaType,
file_name: options?.fileName,
stored_path: storedPath,
preview_text: options?.previewText,
deleted: false,
created_at: now,
updated_at: now,
}
this.items.set(id, record)
await this.persistIndex()
return record
}
getItem(feedItemId: string): FeedItemRecord | undefined {
return this.items.get(feedItemId)
}
listItems(turnId?: string): FeedItemRecord[] {
let items = Array.from(this.items.values()).filter((i) => !i.deleted)
if (turnId) {
items = items.filter((i) => i.source.turn_id === turnId)
}
return items.sort((a, b) => b.created_at - a.created_at)
}
async getPayload(feedItemId: string): Promise<Uint8Array | null> {
const item = this.items.get(feedItemId)
if (!item?.stored_path) return null
try {
return await readFile(item.stored_path) as unknown as Uint8Array
} catch {
return null
}
}
deleteItem(feedItemId: string): boolean {
const item = this.items.get(feedItemId)
if (!item) return false
item.deleted = true
item.updated_at = Date.now()
return true
}
reset(): void {
this.items.clear()
}
private async persistIndex(): Promise<void> {
const indexPath = join(this.payloadDir, 'index.json')
const records = Array.from(this.items.values())
await writeFile(indexPath, JSON.stringify(records, null, 2))
}
}