- {/* Mobile tab bar - only shown on mobile when there are diffs */}
-
-
-
- setStore("mobileTab", "session")}
- >
- Session
-
- setStore("mobileTab", "review")}
- >
- {reviewCount()} Files Changed
-
-
-
-
+
+
+
+
+ {/* 1. Header */}
+
= 1 }}>
+
+ {data().title}
+
+
+ Based on prompt: "{promptText}"
+
+ Updated just now
+
+
+
+ {/* Loading State Overlay (Dynamic Activity Modules) */}
+
+
+
+
+ {(step) => (
+
+
+
+ {/* Metadata Row */}
+
+
+
+ {step.status === 'active' ? 'Processing' : 'Completed'}
+
+
+ {step.type === 'reading' ? 'Data: 1.2MB' : step.type === 'writing' ? 'Gen: 4 Tokens' : 'Agent: v1.0'}
+
+
- {/* Session panel */}
-
-
-
-
-
-
- Loading changes...
}
- >
-
{
- const value = file.tab(path)
- tabs().open(value)
- file.load(path)
- }}
- classes={{
- root: "pb-[calc(var(--prompt-height,8rem)+32px)]",
- header: "px-4",
- container: "px-4",
- }}
- />
+ {/* Main Content */}
+
+ {/* Icon */}
+
+
+
+
+
+
- }
- >
-
-
-
-
-
-
-
{
- autoScroll.handleScroll()
- if (isDesktop()) scheduleScrollSpy(e.currentTarget)
- }}
- onClick={autoScroll.handleInteraction}
- class="relative min-w-0 w-full h-full overflow-y-auto no-scrollbar"
- >
-
-
0}>
-
-
-
-
-
-
-
-
-
-
- {(message) => {
- if (import.meta.env.DEV) {
- onMount(() => {
- const id = params.id
- if (!id) return
- navMark({ dir: params.dir, to: id, name: "session:first-turn-mounted" })
- })
- }
- return (
-
-
- setStore("expanded", message.id, (open: boolean | undefined) => !open)
- }
- classes={{
- root: "min-w-0 w-full relative",
- content:
- "flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
- container:
- "px-4 md:px-6 " +
- (!showTabs()
- ? "md:max-w-200 md:mx-auto"
- : visibleUserMessages().length > 1
- ? "md:pr-6 md:pl-18"
- : ""),
- }}
- />
-
- )
- }}
-
-
+ {/* Text */}
+
+
+ {step.label}
+
+
+ {step.detail}
+
-
+
-
-
- {
- if (value === "create") {
- setStore("newSessionWorktree", value)
- return
- }
+ )}
+
- setStore("newSessionWorktree", "main")
-
- const target = value === "main" ? sync.project?.worktree : value
- if (!target) return
- if (target === sync.data.path.directory) return
- layout.projects.open(target)
- navigate(`/${base64Encode(target)}/session`)
- }}
- />
-
-
+
- {/* Prompt input */}
- (promptDock = el)}
- class="absolute inset-x-0 bottom-0 pt-12 pb-4 md:pb-8 flex flex-col justify-center items-center z-50 px-4 md:px-0 bg-gradient-to-t from-background-stronger via-background-stronger to-transparent pointer-events-none"
- >
-
-
- {handoff.prompt || "Loading prompt..."}
-
- }
- >
-
{
- inputRef = el
- }}
- newSessionWorktree={newSessionWorktree()}
- onNewSessionWorktreeReset={() => setStore("newSessionWorktree", "main")}
- />
-
+ {/* Main Report Content (Hidden when executing) */}
+
+ = 1}>
+
+
Executive Summary
+
+ {data().summary}
+
+ Recommendation: {data().recommendation}
+
-
-
-
-
-
- {/* Desktop tabs panel (Review + Context + Files) - hidden on mobile */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Review
-
-
- {info()?.summary?.files ?? 0}
-
-
-
-
-
-
-
-
- tabs().close("context")} />
-
- }
- hideCloseButton
- onMiddleClick={() => tabs().close("context")}
- >
-
-
-
-
- {(tab) => }
-
-
-
- dialog.show(() => )}
- />
-
-
-
-
-
-
-
-
- Loading changes...
}
- >
- {
- const value = file.tab(path)
- tabs().open(value)
- file.load(path)
- }}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {(tab) => {
- let scroll: HTMLDivElement | undefined
- let scrollFrame: number | undefined
- let pending: { x: number; y: number } | undefined
-
- const path = createMemo(() => file.pathFromTab(tab))
- const state = createMemo(() => {
- const p = path()
- if (!p) return
- return file.get(p)
- })
- const contents = createMemo(() => state()?.content?.content ?? "")
- const cacheKey = createMemo(() => checksum(contents()))
- const isImage = createMemo(() => {
- const c = state()?.content
- return (
- c?.encoding === "base64" && c?.mimeType?.startsWith("image/") && c?.mimeType !== "image/svg+xml"
- )
- })
- const isSvg = createMemo(() => {
- const c = state()?.content
- return c?.mimeType === "image/svg+xml"
- })
- const svgContent = createMemo(() => {
- if (!isSvg()) return
- const c = state()?.content
- if (!c) return
- if (c.encoding === "base64") return base64Decode(c.content)
- return c.content
- })
- const svgPreviewUrl = createMemo(() => {
- if (!isSvg()) return
- const c = state()?.content
- if (!c) return
- if (c.encoding === "base64") return `data:image/svg+xml;base64,${c.content}`
- return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(c.content)}`
- })
- const imageDataUrl = createMemo(() => {
- if (!isImage()) return
- const c = state()?.content
- return `data:${c?.mimeType};base64,${c?.content}`
- })
- const selectedLines = createMemo(() => {
- const p = path()
- if (!p) return null
- if (file.ready()) return file.selectedLines(p) ?? null
- return handoff.files[p] ?? null
- })
- const selection = createMemo(() => {
- const range = selectedLines()
- if (!range) return
- return selectionFromLines(range)
- })
- const selectionLabel = createMemo(() => {
- const sel = selection()
- if (!sel) return
- if (sel.startLine === sel.endLine) return `L${sel.startLine}`
- return `L${sel.startLine}-${sel.endLine}`
- })
+ = 2}>
+
+
Analysis Breakdown
- const restoreScroll = (retries = 0) => {
- const el = scroll
- if (!el) return
-
- const s = view()?.scroll(tab)
- if (!s) return
-
- // Wait for content to be scrollable - content may not have rendered yet
- if (el.scrollHeight <= el.clientHeight && retries < 10) {
- requestAnimationFrame(() => restoreScroll(retries + 1))
- return
- }
-
- if (el.scrollTop !== s.y) el.scrollTop = s.y
- if (el.scrollLeft !== s.x) el.scrollLeft = s.x
- }
+
+
+
+ {(h, i) => {h}}
+
+
- const handleScroll = (event: Event & { currentTarget: HTMLDivElement }) => {
- pending = {
- x: event.currentTarget.scrollLeft,
- y: event.currentTarget.scrollTop,
- }
- if (scrollFrame !== undefined) return
+
+ {(row) => (
+
+ {row.label}
+
+
+ {(val, i) => (
+ 0 ? "justify-end" : ""}`}>
+
+
+
+ {val.text}
+
+ )}
+
+
+ )}
+
+
+
+
- scrollFrame = requestAnimationFrame(() => {
- scrollFrame = undefined
+ = 2}>
+
+
+
+
High Confidence Analysis
+
+
+ {data().assumptions}
+
+
+
- const next = pending
- pending = undefined
- if (!next) return
+ = 3}>
+
+
Recommended Decisions
+
+
+
+ {(action) => {
+ // Determine color scheme based on badge/priority
+ const isHighPriority = action.badge.toLowerCase().includes('high') || action.badge.toLowerCase().includes('priority')
+ const isRisk = action.badge.toLowerCase().includes('risk')
+ const isOptimization = action.badge.toLowerCase().includes('optim') || action.badge.toLowerCase().includes('cost')
+
+ const accentColor = isHighPriority || isRisk ? '#ef4444' : isOptimization ? '#22c55e' : '#3b82f6'
+ const bgGradient = isHighPriority || isRisk
+ ? 'from-red-50/80 to-rose-50/80'
+ : isOptimization
+ ? 'from-emerald-50/80 to-green-50/80'
+ : 'from-blue-50/80 to-indigo-50/80'
+ const badgeBg = isHighPriority || isRisk
+ ? 'bg-red-100/70 text-red-700'
+ : isOptimization
+ ? 'bg-emerald-100/70 text-emerald-700'
+ : 'bg-blue-100/70 text-blue-700'
- view().setScroll(tab, next)
- })
- }
+ return (
+ handleAction(action.id, action.title)}
+ class={`relative overflow-hidden p-5 rounded-2xl cursor-pointer group bg-gradient-to-br ${bgGradient} backdrop-blur-sm hover:shadow-xl transition-all duration-300 active:scale-[0.98] border-0`}
+ style={{
+ "box-shadow": "0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.03)"
+ }}>
+ {/* Accent border on left */}
+
+
+ {/* Subtle top glow */}
+
+
+
+ {action.title}
+
+ {action.badge}
+
+
+
{action.detail}
- createEffect(
- on(
- () => state()?.loaded,
- (loaded) => {
- if (!loaded) return
- requestAnimationFrame(restoreScroll)
- },
- { defer: true },
- ),
+ {/* Hover indicator */}
+
+ Click to execute
+
+
+
)
+ }}
+
+
+
+
- createEffect(
- on(
- () => file.ready(),
- (ready) => {
- if (!ready) return
- requestAnimationFrame(restoreScroll)
- },
- { defer: true },
- ),
- )
+ = 3}>
+
+
+
+
+
- createEffect(
- on(
- () => tabs().active() === tab,
- (active) => {
- if (!active) return
- if (!state()?.loaded) return
- requestAnimationFrame(restoreScroll)
- },
- ),
- )
+ {/* Execution State */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {executionStatus() === 'running' ? `Executing: ${executedAction()}` : `Completed: ${executedAction()}`}
+
+
- onCleanup(() => {
- if (scrollFrame === undefined) return
- cancelAnimationFrame(scrollFrame)
- })
+ {/* Live Logs */}
+
+
+ {(log) => (
+
+ {new Date().toLocaleTimeString([], { hour12: false, hour: "2-digit", minute: "2-digit" })}
+ > {log}
+
+ )}
+
+
+
+
+
- return (
-
{
- scroll = el
- restoreScroll()
- }}
- onScroll={handleScroll}
- >
-
-
- {(sel) => (
-
-
-
- )}
-
-
-
-
-
})
-
-
-
-
-
{
- const p = path()
- if (!p) return
- file.setSelectedLines(p, range)
- }}
- overflow="scroll"
- class="select-text"
- />
-
-
-
})
-
-
-
-
-
- {
- const p = path()
- if (!p) return
- file.setSelectedLines(p, range)
- }}
- overflow="scroll"
- class="select-text pb-40"
- />
-
-
- Loading...
-
-
- {(err) => {err()}
}
-
-
-
-
- )
- }}
-
-
-
-
- {(tab) => {
- const path = createMemo(() => file.pathFromTab(tab()))
- return (
-
- {(p) => }
-
- )
- }}
-
-
-
+ {/* Success Card with Details */}
+
+
+
+
+
+
+
+
Execution Successful
+
+ The requested action has been processed and synchronized with the relevant business systems.
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- {(title) => (
-
- {title}
-
- )}
-
-
-
Loading...
-
- Loading terminal...
-
- }
- >
-
-
-
-
-
- t.id)}>
- {(pty) => }
-
-
-
-
-
-
-
-
- {(pty) => (
-
- terminal.clone(pty.id)} />
-
- )}
-
-
-
-
- {(draggedId) => {
- const pty = createMemo(() => terminal.all().find((t: LocalPTY) => t.id === draggedId()))
- return (
-
- {(t) => (
-
- {t().title}
-
- )}
-
- )
- }}
-
-
-
-
-
-
+
)
}
diff --git a/packages/opencode/AGENTS.md b/packages/opencode/AGENTS.md
index a68fd7f3e321..65c463a50690 100644
--- a/packages/opencode/AGENTS.md
+++ b/packages/opencode/AGENTS.md
@@ -1,4 +1,4 @@
-# opencode agent guidelines
+# shopos agent guidelines
## Build/Test Commands
@@ -24,4 +24,4 @@
- **Validation**: All inputs validated with Zod schemas
- **Logging**: Use `Log.create({ service: "name" })` pattern
- **Storage**: Use `Storage` namespace for persistence
-- **API Client**: The TypeScript TUI (built with SolidJS + OpenTUI) communicates with the OpenCode server using `@opencode-ai/sdk`. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, run `./script/generate.ts` to regenerate the SDK and related files.
+- **API Client**: The TypeScript TUI (built with SolidJS + OpenTUI) communicates with the ShopOS server using `@opencode-ai/sdk`. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, run `./script/generate.ts` to regenerate the SDK and related files.
diff --git a/packages/opencode/bin/opencode b/packages/opencode/bin/shopos
similarity index 86%
rename from packages/opencode/bin/opencode
rename to packages/opencode/bin/shopos
index e35cc00944d6..ca3bc7fef43e 100755
--- a/packages/opencode/bin/opencode
+++ b/packages/opencode/bin/shopos
@@ -17,7 +17,7 @@ function run(target) {
process.exit(code)
}
-const envPath = process.env.OPENCODE_BIN_PATH
+const envPath = process.env.SHOPOS_BIN_PATH
if (envPath) {
run(envPath)
}
@@ -45,11 +45,11 @@ if (!arch) {
arch = os.arch()
}
const base = "opencode-" + platform + "-" + arch
-const binary = platform === "windows" ? "opencode.exe" : "opencode"
+const binary = platform === "windows" ? "shopos.exe" : "shopos"
function findBinary(startDir) {
let current = startDir
- for (;;) {
+ for (; ;) {
const modules = path.join(current, "node_modules")
if (fs.existsSync(modules)) {
const entries = fs.readdirSync(modules)
@@ -74,9 +74,9 @@ function findBinary(startDir) {
const resolved = findBinary(scriptDir)
if (!resolved) {
console.error(
- 'It seems that your package manager failed to install the right version of the opencode CLI for your platform. You can try manually installing the "' +
- base +
- '" package',
+ 'It seems that your package manager failed to install the right version of the ShopOS CLI for your platform. You can try manually installing the "' +
+ base +
+ '" package',
)
process.exit(1)
}
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index 07fee7d730d0..2bf1722e0181 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -18,7 +18,7 @@
"deploy": "echo 'Deploying application...' && bun run build && echo 'Deployment completed successfully'"
},
"bin": {
- "opencode": "./bin/opencode"
+ "shopos": "./bin/shopos"
},
"exports": {
"./*": "./src/*.ts"
diff --git a/packages/opencode/running-shoe-store/src/data/products.ts b/packages/opencode/running-shoe-store/src/data/products.ts
new file mode 100644
index 000000000000..dc688446c12c
--- /dev/null
+++ b/packages/opencode/running-shoe-store/src/data/products.ts
@@ -0,0 +1,215 @@
+import type { Product, ShoeCategory } from "../types/product"
+import { BRANDS } from "../types/product"
+
+export const SAMPLE_PRODUCTS: Product[] = [
+ {
+ id: "nike-air-zoom-pegasus-40",
+ name: "Nike Air Zoom Pegasus 40",
+ brand: "Nike",
+ category: "training-daily",
+ price: 130,
+ description: "The legendary Pegasus returns with enhanced cushioning and responsiveness for your daily training.",
+ images: ["/products/nike-pegasus-40-1.jpg", "/products/nike-pegasus-40-2.jpg", "/products/nike-pegasus-40-3.jpg"],
+ sizes: [
+ { us: 7, eu: 40, uk: 6.5, cm: 25, available: true },
+ { us: 7.5, eu: 40.5, uk: 7, cm: 25.5, available: true },
+ { us: 8, eu: 41, uk: 7.5, cm: 26, available: true },
+ { us: 8.5, eu: 42, uk: 8, cm: 26.5, available: true },
+ { us: 9, eu: 42.5, uk: 8.5, cm: 27, available: true },
+ { us: 9.5, eu: 43, uk: 9, cm: 27.5, available: true },
+ { us: 10, eu: 44, uk: 9.5, cm: 28, available: true },
+ { us: 10.5, eu: 44.5, uk: 10, cm: 28.5, available: true },
+ { us: 11, eu: 45, uk: 10.5, cm: 29, available: true },
+ { us: 11.5, eu: 45.5, uk: 11, cm: 29.5, available: true },
+ { us: 12, eu: 46, uk: 11.5, cm: 30, available: true },
+ ],
+ fit: {
+ type: "true-to-size",
+ width: ["regular", "wide"],
+ recommendedFor: ["neutral runners", "daily training", "long distance"],
+ },
+ performance: {
+ cushioning: "moderate",
+ stability: "neutral",
+ weight: 285,
+ drop: 10,
+ surface: ["road", "track", "treadmill"],
+ },
+ stock: 150,
+ status: "active",
+ },
+ {
+ id: "adidas-ultraboost-22",
+ name: "adidas Ultraboost 22",
+ brand: "Adidas",
+ category: "road-running",
+ price: 190,
+ description: "Revolutionary energy return with Boost cushioning for premium comfort on your runs.",
+ images: [
+ "/products/adidas-ultraboost-22-1.jpg",
+ "/products/adidas-ultraboost-22-2.jpg",
+ "/products/adidas-ultraboost-22-3.jpg",
+ ],
+ sizes: [
+ { us: 7, eu: 40, uk: 6.5, cm: 25, available: true },
+ { us: 7.5, eu: 40.5, uk: 7, cm: 25.5, available: true },
+ { us: 8, eu: 41, uk: 7.5, cm: 26, available: true },
+ { us: 8.5, eu: 42, uk: 8, cm: 26.5, available: true },
+ { us: 9, eu: 42.5, uk: 8.5, cm: 27, available: true },
+ { us: 9.5, eu: 43, uk: 9, cm: 27.5, available: true },
+ { us: 10, eu: 44, uk: 9.5, cm: 28, available: true },
+ { us: 10.5, eu: 44.5, uk: 10, cm: 28.5, available: true },
+ { us: 11, eu: 45, uk: 10.5, cm: 29, available: true },
+ { us: 11.5, eu: 45.5, uk: 11, cm: 29.5, available: true },
+ { us: 12, eu: 46, uk: 11.5, cm: 30, available: true },
+ ],
+ fit: {
+ type: "true-to-size",
+ width: ["regular"],
+ recommendedFor: ["road runners", "daily training", "recovery runs"],
+ },
+ performance: {
+ cushioning: "maximum",
+ stability: "neutral",
+ weight: 310,
+ drop: 10,
+ surface: ["road", "track"],
+ },
+ stock: 85,
+ status: "active",
+ },
+ {
+ id: "hoka-clifton-9",
+ name: "Hoka Clifton 9",
+ brand: "Hoka",
+ category: "training-daily",
+ price: 145,
+ description: "Maximum cushioning meets lightweight design for the perfect daily trainer.",
+ images: ["/products/hoka-clifton-9-1.jpg", "/products/hoka-clifton-9-2.jpg", "/products/hoka-clifton-9-3.jpg"],
+ sizes: [
+ { us: 7, eu: 40, uk: 6.5, cm: 25, available: true },
+ { us: 7.5, eu: 40.5, uk: 7, cm: 25.5, available: true },
+ { us: 8, eu: 41, uk: 7.5, cm: 26, available: true },
+ { us: 8.5, eu: 42, uk: 8, cm: 26.5, available: true },
+ { us: 9, eu: 42.5, uk: 8.5, cm: 27, available: true },
+ { us: 9.5, eu: 43, uk: 9, cm: 27.5, available: true },
+ { us: 10, eu: 44, uk: 9.5, cm: 28, available: true },
+ { us: 10.5, eu: 44.5, uk: 10, cm: 28.5, available: true },
+ { us: 11, eu: 45, uk: 10.5, cm: 29, available: true },
+ { us: 11.5, eu: 45.5, uk: 11, cm: 29.5, available: true },
+ { us: 12, eu: 46, uk: 11.5, cm: 30, available: true },
+ ],
+ fit: {
+ type: "runs-small",
+ width: ["regular", "wide"],
+ recommendedFor: ["neutral runners", "long distance", "recovery runs"],
+ },
+ performance: {
+ cushioning: "maximum",
+ stability: "neutral",
+ weight: 290,
+ drop: 5,
+ surface: ["road", "track"],
+ },
+ stock: 120,
+ status: "active",
+ },
+ {
+ id: "nike-vaporfly-next-3",
+ name: "Nike Vaporfly Next% 3",
+ brand: "Nike",
+ category: "racing-competition",
+ price: 275,
+ description: "The ultimate racing shoe with carbon fiber plate for record-breaking performance.",
+ images: ["/products/nike-vaporfly-3-1.jpg", "/products/nike-vaporfly-3-2.jpg", "/products/nike-vaporfly-3-3.jpg"],
+ sizes: [
+ { us: 7, eu: 40, uk: 6.5, cm: 25, available: true },
+ { us: 7.5, eu: 40.5, uk: 7, cm: 25.5, available: true },
+ { us: 8, eu: 41, uk: 7.5, cm: 26, available: true },
+ { us: 8.5, eu: 42, uk: 8, cm: 26.5, available: true },
+ { us: 9, eu: 42.5, uk: 8.5, cm: 27, available: true },
+ { us: 9.5, eu: 43, uk: 9, cm: 27.5, available: true },
+ { us: 10, eu: 44, uk: 9.5, cm: 28, available: true },
+ { us: 10.5, eu: 44.5, uk: 10, cm: 28.5, available: true },
+ { us: 11, eu: 45, uk: 10.5, cm: 29, available: true },
+ { us: 11.5, eu: 45.5, uk: 11, cm: 29.5, available: true },
+ { us: 12, eu: 46, uk: 11.5, cm: 30, available: true },
+ ],
+ fit: {
+ type: "runs-small",
+ width: ["regular"],
+ recommendedFor: ["racing", "speed work", "elite runners"],
+ },
+ performance: {
+ cushioning: "moderate",
+ stability: "neutral",
+ weight: 195,
+ drop: 8,
+ surface: ["road", "track"],
+ },
+ stock: 45,
+ status: "active",
+ },
+ {
+ id: "salomon-speedcross-5",
+ name: "Salomon Speedcross 5",
+ brand: "Salomon",
+ category: "trail-running",
+ price: 160,
+ description: "Aggressive grip and protection for technical trail conditions and muddy terrain.",
+ images: [
+ "/products/salomon-speedcross-5-1.jpg",
+ "/products/salomon-speedcross-5-2.jpg",
+ "/products/salomon-speedcross-5-3.jpg",
+ ],
+ sizes: [
+ { us: 7, eu: 40, uk: 6.5, cm: 25, available: true },
+ { us: 7.5, eu: 40.5, uk: 7, cm: 25.5, available: true },
+ { us: 8, eu: 41, uk: 7.5, cm: 26, available: true },
+ { us: 8.5, eu: 42, uk: 8, cm: 26.5, available: true },
+ { us: 9, eu: 42.5, uk: 8.5, cm: 27, available: true },
+ { us: 9.5, eu: 43, uk: 9, cm: 27.5, available: true },
+ { us: 10, eu: 44, uk: 9.5, cm: 28, available: true },
+ { us: 10.5, eu: 44.5, uk: 10, cm: 28.5, available: true },
+ { us: 11, eu: 45, uk: 10.5, cm: 29, available: true },
+ { us: 11.5, eu: 45.5, uk: 11, cm: 29.5, available: true },
+ { us: 12, eu: 46, uk: 11.5, cm: 30, available: true },
+ ],
+ fit: {
+ type: "true-to-size",
+ width: ["regular"],
+ recommendedFor: ["trail runners", "muddy conditions", "technical terrain"],
+ },
+ performance: {
+ cushioning: "moderate",
+ stability: "neutral",
+ weight: 310,
+ drop: 9,
+ surface: ["trail", "mud", "rock"],
+ },
+ stock: 75,
+ status: "active",
+ },
+]
+
+export function getProductsByCategory(category: ShoeCategory): Product[] {
+ return SAMPLE_PRODUCTS.filter((product) => product.category === category)
+}
+
+export function getProductsByBrand(brand: string): Product[] {
+ return SAMPLE_PRODUCTS.filter((product) => product.brand === brand)
+}
+
+export function searchProducts(query: string): Product[] {
+ const lowercaseQuery = query.toLowerCase()
+ return SAMPLE_PRODUCTS.filter(
+ (product) =>
+ product.name.toLowerCase().includes(lowercaseQuery) ||
+ product.brand.toLowerCase().includes(lowercaseQuery) ||
+ product.description.toLowerCase().includes(lowercaseQuery),
+ )
+}
+
+export function getProductById(id: string): Product | undefined {
+ return SAMPLE_PRODUCTS.find((product) => product.id === id)
+}
diff --git a/packages/opencode/running-shoe-store/src/types/product.ts b/packages/opencode/running-shoe-store/src/types/product.ts
new file mode 100644
index 000000000000..1e8594b21d7e
--- /dev/null
+++ b/packages/opencode/running-shoe-store/src/types/product.ts
@@ -0,0 +1,74 @@
+export interface Product {
+ id: string
+ name: string
+ brand: string
+ category: ShoeCategory
+ price: number
+ description: string
+ images: string[]
+ sizes: Size[]
+ fit: FitInfo
+ performance: PerformanceFeatures
+ stock: number
+ status: "active" | "inactive"
+}
+
+export interface Size {
+ us: number
+ eu: number
+ uk: number
+ cm: number
+ available: boolean
+}
+
+export interface FitInfo {
+ type: "true-to-size" | "runs-small" | "runs-large"
+ width: ("narrow" | "regular" | "wide" | "extra-wide")[]
+ recommendedFor: string[]
+}
+
+export interface PerformanceFeatures {
+ cushioning: "minimal" | "moderate" | "maximum"
+ stability: "neutral" | "stability" | "motion-control"
+ weight: number
+ drop: number
+ surface: string[]
+}
+
+export type ShoeCategory = "road-running" | "trail-running" | "racing-competition" | "training-daily"
+
+export const BRANDS = [
+ "Nike",
+ "Adidas",
+ "New Balance",
+ "ASICS",
+ "Brooks",
+ "Hoka",
+ "Saucony",
+ "Mizuno",
+ "Under Armour",
+ "Puma",
+] as const
+
+export const CATEGORIES = {
+ "road-running": {
+ name: "Road Running",
+ description: "Shoes designed for pavement, treadmill, and track surfaces",
+ features: ["smooth ride", "durability", "cushioning"],
+ },
+ "trail-running": {
+ name: "Trail Running",
+ description: "Off-road shoes with enhanced traction and protection",
+ features: ["grip", "rock protection", "durability"],
+ },
+ "racing-competition": {
+ name: "Racing & Competition",
+ description: "Lightweight performance shoes for speed work and racing",
+ features: ["lightweight", "responsive", "minimal cushioning"],
+ },
+ "training-daily": {
+ name: "Training & Daily",
+ description: "Versatile shoes for everyday training and mileage",
+ features: ["versatility", "comfort", "durability"],
+ },
+} as const
diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts
index 61a665312f0e..52ab65065f9f 100755
--- a/packages/opencode/script/build.ts
+++ b/packages/opencode/script/build.ts
@@ -25,73 +25,73 @@ const allTargets: {
abi?: "musl"
avx2?: false
}[] = [
- {
- os: "linux",
- arch: "arm64",
- },
- {
- os: "linux",
- arch: "x64",
- },
- {
- os: "linux",
- arch: "x64",
- avx2: false,
- },
- {
- os: "linux",
- arch: "arm64",
- abi: "musl",
- },
- {
- os: "linux",
- arch: "x64",
- abi: "musl",
- },
- {
- os: "linux",
- arch: "x64",
- abi: "musl",
- avx2: false,
- },
- {
- os: "darwin",
- arch: "arm64",
- },
- {
- os: "darwin",
- arch: "x64",
- },
- {
- os: "darwin",
- arch: "x64",
- avx2: false,
- },
- {
- os: "win32",
- arch: "x64",
- },
- {
- os: "win32",
- arch: "x64",
- avx2: false,
- },
-]
+ {
+ os: "linux",
+ arch: "arm64",
+ },
+ {
+ os: "linux",
+ arch: "x64",
+ },
+ {
+ os: "linux",
+ arch: "x64",
+ avx2: false,
+ },
+ {
+ os: "linux",
+ arch: "arm64",
+ abi: "musl",
+ },
+ {
+ os: "linux",
+ arch: "x64",
+ abi: "musl",
+ },
+ {
+ os: "linux",
+ arch: "x64",
+ abi: "musl",
+ avx2: false,
+ },
+ {
+ os: "darwin",
+ arch: "arm64",
+ },
+ {
+ os: "darwin",
+ arch: "x64",
+ },
+ {
+ os: "darwin",
+ arch: "x64",
+ avx2: false,
+ },
+ {
+ os: "win32",
+ arch: "x64",
+ },
+ {
+ os: "win32",
+ arch: "x64",
+ avx2: false,
+ },
+ ]
const targets = singleFlag
? allTargets.filter((item) => {
- if (item.os !== process.platform || item.arch !== process.arch) {
- return false
- }
-
- // When building for the current platform, prefer a single native binary by default.
- // Baseline binaries require additional Bun artifacts and can be flaky to download.
- if (item.avx2 === false) {
- return baselineFlag
- }
-
- return true
- })
+ if (item.os !== process.platform || item.arch !== process.arch) {
+ return false
+ }
+
+ // When building for the current platform, prefer a single native binary by default.
+ // Baseline binaries require additional Bun artifacts and can be flaky to download.
+ if (item.avx2 === false) {
+ return baselineFlag
+ }
+
+ return true
+ })
: allTargets
await $`rm -rf dist`
@@ -134,8 +134,8 @@ for (const item of targets) {
autoloadTsconfig: true,
autoloadPackageJson: true,
target: name.replace(pkg.name, "bun") as any,
- outfile: `dist/${name}/bin/opencode`,
- execArgv: [`--user-agent=opencode/${Script.version}`, "--use-system-ca", "--"],
+ outfile: `dist/${name}/bin/shopos`,
+ execArgv: [`--user-agent=shopos/${Script.version}`, "--use-system-ca", "--"],
windows: {},
},
entrypoints: ["./src/index.ts", parserWorker, workerPath],
diff --git a/packages/opencode/src/brand/index.ts b/packages/opencode/src/brand/index.ts
new file mode 100644
index 000000000000..7d8d82f4bff4
--- /dev/null
+++ b/packages/opencode/src/brand/index.ts
@@ -0,0 +1,144 @@
+import { BusEvent } from "@/bus/bus-event"
+import z from "zod"
+import { Storage } from "../storage/storage"
+import { Instance } from "../project/instance"
+import { fn } from "@/util/fn"
+import { Log } from "../util/log"
+
+export namespace Brand {
+ const log = Log.create({ service: "brand" })
+
+ export const AssetType = z.enum(["logo", "product", "creative", "guideline", "other"])
+ export type AssetType = z.infer
+
+ export const Asset = z.object({
+ id: z.string(),
+ filename: z.string(),
+ path: z.string(), // Local path to stored file
+ type: AssetType,
+ metadata: z.record(z.string(), z.any()).optional(),
+ analysis: z.object({
+ colors: z.array(z.string()).optional(),
+ extractedText: z.string().optional(),
+ description: z.string().optional(),
+ }).optional(),
+ uploadedAt: z.number(),
+ })
+ export type Asset = z.infer
+
+ export const Tone = z.string()
+ export const Color = z.string() // Hex code
+
+ export const Context = z.object({
+ id: z.string(),
+ projectID: z.string(),
+ name: z.string().optional(),
+ tone: z.array(Tone).default([]),
+ primaryColors: z.array(Color).default([]),
+ visualPatterns: z.array(z.string()).default([]),
+ assets: z.array(Asset).default([]),
+ doNotUse: z.array(z.string()).default([]), // List of asset IDs or filenames
+ status: z.enum(["pending", "processing", "ready", "approved"]).default("pending"),
+ version: z.number().default(1),
+ time: z.object({
+ created: z.number(),
+ updated: z.number(),
+ approved: z.number().optional()
+ })
+ })
+ export type Context = z.infer
+
+ export const Event = {
+ Updated: BusEvent.define(
+ "brand.updated",
+ z.object({
+ context: Context,
+ }),
+ ),
+ }
+
+ export const get = fn(z.void(), async () => {
+ const projectID = Instance.project.id
+ // We assume 1 brand per project for now, keyed by projectID
+ const context = await Storage.read(["brand", projectID]).catch(() => null)
+ return context
+ })
+
+ export const create = fn(z.object({ name: z.string().optional() }), async (input) => {
+ const projectID = Instance.project.id
+ const context: Context = {
+ id: crypto.randomUUID(),
+ projectID,
+ name: input.name,
+ tone: [],
+ primaryColors: [],
+ visualPatterns: [],
+ assets: [],
+ doNotUse: [],
+ status: "pending",
+ version: 1,
+ time: {
+ created: Date.now(),
+ updated: Date.now()
+ }
+ }
+ await Storage.write(["brand", projectID], context)
+ return context
+ })
+
+ export const addAsset = fn(
+ z.object({
+ filename: z.string(),
+ path: z.string(),
+ type: AssetType
+ }),
+ async (input) => {
+ const projectID = Instance.project.id
+ let context = await get()
+ if (!context) {
+ context = await create({})
+ }
+
+ const asset: Asset = {
+ id: crypto.randomUUID(),
+ filename: input.filename,
+ path: input.path,
+ type: input.type,
+ uploadedAt: Date.now(),
+ analysis: {}
+ }
+
+ // TODO: Trigger analysis pipeline here (async)
+ analyzeAsset(asset)
+
+ context.assets.push(asset)
+ await update(context)
+ return context
+ })
+
+ export const update = fn(Context, async (context) => {
+ const projectID = Instance.project.id
+ context.time.updated = Date.now()
+ context.version += 1
+ await Storage.write(["brand", projectID], context)
+ // Publish event
+ return context
+ })
+
+ // Stub for analysis
+ async function analyzeAsset(asset: Asset) {
+ log.info(`Analyzing asset: ${asset.filename}`)
+ // In a real implementation, this would call a Vision model
+ // For now, we just pass through
+ }
+
+ export const approve = fn(z.void(), async () => {
+ const context = await get()
+ if (!context) throw new Error("No brand context found")
+
+ context.status = "approved"
+ context.time.approved = Date.now()
+ await update(context)
+ return context
+ })
+}
diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts
index 54248f96f3df..a48c17a4ce1d 100644
--- a/packages/opencode/src/cli/cmd/run.ts
+++ b/packages/opencode/src/cli/cmd/run.ts
@@ -27,7 +27,7 @@ const TOOL: Record = {
export const RunCommand = cmd({
command: "run [message..]",
- describe: "run opencode with a message",
+ describe: "run shopos with a message",
builder: (yargs: Argv) => {
return yargs
.positional("message", {
@@ -81,7 +81,7 @@ export const RunCommand = cmd({
})
.option("attach", {
type: "string",
- describe: "attach to a running opencode server (e.g., http://localhost:4096)",
+ describe: "attach to a running shopos server (e.g., http://localhost:4096)",
})
.option("port", {
type: "number",
@@ -104,7 +104,7 @@ export const RunCommand = cmd({
for (const filePath of files) {
const resolvedPath = path.resolve(process.cwd(), filePath)
const file = Bun.file(resolvedPath)
- const stats = await file.stat().catch(() => {})
+ const stats = await file.stat().catch(() => { })
if (!stats) {
UI.error(`File not found: ${filePath}`)
process.exit(1)
@@ -295,24 +295,24 @@ export const RunCommand = cmd({
const result = await sdk.session.create(
title
? {
- title,
- permission: [
- {
- permission: "question",
- action: "deny",
- pattern: "*",
- },
- ],
- }
+ title,
+ permission: [
+ {
+ permission: "question",
+ action: "deny",
+ pattern: "*",
+ },
+ ],
+ }
: {
- permission: [
- {
- permission: "question",
- action: "deny",
- pattern: "*",
- },
- ],
- },
+ permission: [
+ {
+ permission: "question",
+ action: "deny",
+ pattern: "*",
+ },
+ ],
+ },
)
return result.data?.id
})()
diff --git a/packages/opencode/src/cli/ui.ts b/packages/opencode/src/cli/ui.ts
index acd1383a070d..38f4b4d08028 100644
--- a/packages/opencode/src/cli/ui.ts
+++ b/packages/opencode/src/cli/ui.ts
@@ -4,10 +4,12 @@ import { NamedError } from "@opencode-ai/util/error"
export namespace UI {
const LOGO = [
- [` `, ` ▄ `],
- [`█▀▀█ █▀▀█ █▀▀█ █▀▀▄ `, `█▀▀▀ █▀▀█ █▀▀█ █▀▀█`],
- [`█░░█ █░░█ █▀▀▀ █░░█ `, `█░░░ █░░█ █░░█ █▀▀▀`],
- [`▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ `, `▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`],
+ [` ____ _ ___ ____ `, ` `],
+ [` / ___|| |__ ___ _ __ / _ \/ ___| `, ` `],
+ [` \___ \| '_ \ / _ \| '_ \| | | \___ \ `, ` `],
+ [` ___) | | | | (_) | |_) | |_| |___) |`, ` `],
+ [` |____/|_| |_|\___/| .__/ \___/|____/ `, ` `],
+ [` |_| `, ` `],
]
export const CancelledError = NamedError.create("UICancelledError", z.void())
diff --git a/packages/opencode/src/commerce/generator.ts b/packages/opencode/src/commerce/generator.ts
new file mode 100644
index 000000000000..5c95e0cd077a
--- /dev/null
+++ b/packages/opencode/src/commerce/generator.ts
@@ -0,0 +1,118 @@
+import { Commerce } from "./index"
+import { z } from "zod"
+import { iife } from "@/util/iife"
+
+export namespace CommerceGenerator {
+
+ // Seedable random number generator for deterministic "Simulated Reality"
+ class SeededRandom {
+ private seed: number;
+ constructor(seed: number) { this.seed = seed }
+
+ // Simple LCG
+ next(): number {
+ this.seed = (this.seed * 9301 + 49297) % 233280;
+ return this.seed / 233280;
+ }
+
+ range(min: number, max: number): number {
+ return Math.floor(this.next() * (max - min + 1) + min)
+ }
+ }
+
+ export const generate = async (
+ products: Commerce.Product[],
+ marketplaces: Commerce.MarketplaceConfig[],
+ days: number = 30
+ ): Promise => {
+
+ const now = new Date()
+ const performance: Commerce.PerformanceRecord[] = []
+ const economics: Commerce.UnitEconomics[] = []
+ const rng = new SeededRandom(12345) // Fixed seed for reproducibility
+
+ // 1. Generate Daily Performance
+ for (let i = 0; i < days; i++) {
+ const date = new Date(now)
+ date.setDate(date.getDate() - i)
+ const dateStr = date.toISOString().split('T')[0]
+
+ for (const product of products) {
+ for (const market of marketplaces) {
+ // Determine base "velocity" of product on market
+ // Amazon > Flipkart > D2C usually for velocity, but D2C higher margin
+ let velocityMultiplier = 1
+ if (market.id === 'amazon_in') velocityMultiplier = 2.5
+ if (market.id === 'flipkart_in') velocityMultiplier = 1.8
+
+ const unitsSold = rng.range(0, 10 * velocityMultiplier)
+ const returns = Math.floor(unitsSold * (rng.range(5, 15) / 100)) // 5-15% return rate
+
+ // Ad Spend fluctuates
+ const adSpend = rng.range(500, 2000)
+
+ performance.push({
+ productID: product.id,
+ marketplace: market.id,
+ date: dateStr,
+ period: "day",
+ unitsSold,
+ revenue: unitsSold * product.price,
+ adSpend,
+ returns,
+ dataSource: "synthetic",
+ generatedAt: Date.now()
+ })
+ }
+ }
+ }
+
+ // 2. Calculate Economics (Static View per Product/Market)
+ for (const product of products) {
+ for (const market of marketplaces) {
+ const rules = market.rules
+
+ const commission = product.price * rules.commissionPct
+ const fixed = rules.fixedFee
+ const logistics = product.weightKg * rules.logisticsPerKg
+ // Tax mainly on Price (simplification)
+ const tax = product.price * rules.taxPct
+
+ // Marketing CAC (Simulated avg from performance)
+ // In a real engine, this would aggregation. Here we estimate.
+ const marketingCAC = product.id.includes("premium") ? 400 : 150
+ const returnCost = 100 // Flat simulation
+
+ const totalCost = commission + fixed + logistics + tax + marketingCAC + (returnCost * 0.1) // Assumed 10% rtr rate
+ const net = product.price - totalCost
+
+ economics.push({
+ productID: product.id,
+ marketplace: market.id,
+ sellingPrice: product.price,
+ commission,
+ logistics,
+ tax,
+ marketingCAC,
+ returnCost,
+ netContribution: net,
+ marginPct: Math.round((net / product.price) * 100)
+ })
+ }
+ }
+
+ return {
+ timestamp: Date.now(),
+ products,
+ performance,
+ economics
+ }
+ }
+
+ // Helper to create Mock Products if Catalog is empty
+ export const mockProducts = (): Commerce.Product[] => [
+ { id: "p_run_v1", sku: "NIKE-PEG-40", title: "Nike Air Zoom Pegasus 40", price: 11999, weightKg: 0.8, source: "mock", variants: [] },
+ { id: "p_run_v2", sku: "ADIDAS-UB-L", title: "Adidas Ultraboost Light", price: 16999, weightKg: 0.7, source: "mock", variants: [] },
+ { id: "p_trail_x", sku: "SAL-SPEED-5", title: "Salomon Speedcross 5", price: 12999, weightKg: 1.1, source: "mock", variants: [] }
+ ]
+}
diff --git a/packages/opencode/src/commerce/index.ts b/packages/opencode/src/commerce/index.ts
new file mode 100644
index 000000000000..ffca77aeaac0
--- /dev/null
+++ b/packages/opencode/src/commerce/index.ts
@@ -0,0 +1,138 @@
+import { Storage } from "../storage/storage"
+import { Instance } from "../project/instance"
+import { fn } from "@/util/fn"
+import z from "zod"
+
+export namespace Commerce {
+
+ // 1. Data Source Types
+ export const DataSourceType = z.enum(["storefront_mcp", "mock", "synthetic"])
+ export type DataSourceType = z.infer
+
+ // 2. Marketplace Model
+ export const MarketplaceID = z.enum(["amazon_in", "flipkart_in", "d2c_shopify"])
+ export type MarketplaceID = z.infer
+
+ export const MarketplaceConfig = z.object({
+ id: MarketplaceID,
+ name: z.string(),
+ currency: z.string(),
+ rules: z.object({
+ commissionPct: z.number().default(0), // e.g. 0.15 for 15%
+ fixedFee: z.number().default(0), // e.g. 20 (INR)
+ logisticsPerKg: z.number().default(0), // standard shipping
+ taxPct: z.number().default(0) // GST/VAT
+ })
+ })
+ export type MarketplaceConfig = z.infer
+
+ // 3. Product Catalog (Ground Truth)
+ export const Product = z.object({
+ id: z.string(),
+ sku: z.string(),
+ title: z.string(),
+ price: z.number(),
+ weightKg: z.number().default(0.5),
+ variants: z.array(z.string()).default([]),
+ source: z.literal("storefront_mcp").or(z.literal("mock")),
+ policies: z.record(z.string(), z.any()).optional()
+ })
+ export type Product = z.infer
+
+ // 4. Time Series Data (Synthetic/Real)
+ export const Period = z.enum(["day", "week", "month"])
+ export type Period = z.infer
+
+ export const PerformanceRecord = z.object({
+ productID: z.string(),
+ marketplace: MarketplaceID,
+ date: z.string(), // ISO Date YYYY-MM-DD
+ period: Period,
+
+ // Metrics
+ unitsSold: z.number(),
+ revenue: z.number(),
+ adSpend: z.number(),
+ returns: z.number(),
+
+ // Quality Metadata
+ dataSource: DataSourceType,
+ generatedAt: z.number()
+ })
+ export type PerformanceRecord = z.infer
+
+ // 5. Product Economics (Calculated)
+ export const UnitEconomics = z.object({
+ productID: z.string(),
+ marketplace: MarketplaceID,
+ sellingPrice: z.number(),
+
+ // Costs
+ commission: z.number(),
+ logistics: z.number(),
+ tax: z.number(),
+ marketingCAC: z.number(),
+ returnCost: z.number(),
+
+ // Net
+ netContribution: z.number(), // Profit per unit
+ marginPct: z.number()
+ })
+ export type UnitEconomics = z.infer
+
+ // Aggregate Data Object
+ export const Dataset = z.object({
+ timestamp: z.number(),
+ products: z.array(Product),
+ performance: z.array(PerformanceRecord),
+ economics: z.array(UnitEconomics)
+ })
+ export type Dataset = z.infer
+
+ // --- API ---
+
+ export const getMarketplaces = fn(z.void(), async () => {
+ // Hardcoded Marketplace Definitions for Phase 2
+ return [
+ {
+ id: "amazon_in",
+ name: "Amazon India",
+ currency: "INR",
+ rules: { commissionPct: 0.18, fixedFee: 25, logisticsPerKg: 65, taxPct: 0.18 }
+ },
+ {
+ id: "flipkart_in",
+ name: "Flipkart",
+ currency: "INR",
+ rules: { commissionPct: 0.15, fixedFee: 15, logisticsPerKg: 55, taxPct: 0.18 }
+ },
+ {
+ id: "d2c_shopify",
+ name: "Official Store",
+ currency: "INR",
+ rules: { commissionPct: 0.02, fixedFee: 0, logisticsPerKg: 80, taxPct: 0.18 } // Payment gateway fee only
+ }
+ ] as MarketplaceConfig[]
+ })
+
+ export const getCatalog = fn(z.void(), async () => {
+ const projectID = Instance.project.id
+ // TODO: Connect to real MCP. For now returning null/empty to force synthetic generation logic later
+ return await Storage.read(["commerce", projectID, "catalog"]).catch(() => [])
+ })
+
+ export const saveCatalog = fn(z.array(Product), async (products) => {
+ const projectID = Instance.project.id
+ await Storage.write(["commerce", projectID, "catalog"], products)
+ })
+
+ export const getPerformance = fn(z.object({
+ start: z.string(),
+ end: z.string()
+ }), async (range) => {
+ const projectID = Instance.project.id
+ // Simple filter can be added here
+ return await Storage.read(["commerce", projectID, "performance"]).catch(() => [])
+ })
+
+}
diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts
index 6dc5e99e91ef..a494ac964784 100644
--- a/packages/opencode/src/index.ts
+++ b/packages/opencode/src/index.ts
@@ -41,7 +41,7 @@ process.on("uncaughtException", (e) => {
const cli = yargs(hideBin(process.argv))
.parserConfiguration({ "populate--": true })
- .scriptName("opencode")
+ .scriptName("shopos")
.wrap(100)
.help("help", "show help")
.alias("help", "h")
@@ -68,9 +68,8 @@ const cli = yargs(hideBin(process.argv))
})
process.env.AGENT = "1"
- process.env.OPENCODE = "1"
-
- Log.Default.info("opencode", {
+ process.env.SHOPOS = "1"
+ Log.Default.info("shopos", {
version: Installation.VERSION,
args: process.argv.slice(2),
})
diff --git a/packages/opencode/src/server/brand.ts b/packages/opencode/src/server/brand.ts
new file mode 100644
index 000000000000..d2d68b506590
--- /dev/null
+++ b/packages/opencode/src/server/brand.ts
@@ -0,0 +1,82 @@
+import { Hono } from "hono"
+import { describeRoute, resolver, validator } from "hono-openapi"
+import { Brand } from "../brand"
+import { z } from "zod"
+import { errors } from "./error"
+
+// Create a new Hono instance for Brand routes
+export const BrandRoute = new Hono()
+
+BrandRoute.get(
+ "/",
+ describeRoute({
+ summary: "Get Brand Context",
+ description: "Retrieve the current brand context object for the project.",
+ operationId: "brand.get",
+ responses: {
+ 200: {
+ description: "Brand Context",
+ content: {
+ "application/json": {
+ schema: resolver(Brand.Context.nullable()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ const context = await Brand.get()
+ return c.json(context)
+ }
+)
+
+BrandRoute.post(
+ "/",
+ describeRoute({
+ summary: "Create Brand Context",
+ description: "Initialize a new brand context.",
+ operationId: "brand.create",
+ responses: {
+ 200: {
+ description: "Created Context",
+ content: {
+ "application/json": {
+ schema: resolver(Brand.Context),
+ },
+ },
+ },
+ },
+ }),
+ validator("json", z.object({ name: z.string().optional() })),
+ async (c) => {
+ const input = c.req.valid("json")
+ const context = await Brand.create(input)
+ return c.json(context)
+ }
+)
+
+BrandRoute.post(
+ "/approve",
+ describeRoute({
+ summary: "Approve Brand Context",
+ description: "Lock the brand context as the source of truth.",
+ operationId: "brand.approve",
+ responses: {
+ 200: {
+ description: "Approved Context",
+ content: {
+ "application/json": {
+ schema: resolver(Brand.Context),
+ },
+ },
+ },
+ ...errors(400),
+ },
+ }),
+ async (c) => {
+ const context = await Brand.approve()
+ return c.json(context)
+ }
+)
+
+// TODO: Implement Asset Upload Route (requires multipart handling)
diff --git a/packages/opencode/src/server/commerce.ts b/packages/opencode/src/server/commerce.ts
new file mode 100644
index 000000000000..e8651c37860b
--- /dev/null
+++ b/packages/opencode/src/server/commerce.ts
@@ -0,0 +1,68 @@
+import { Hono } from "hono"
+import { describeRoute, resolver, validator } from "hono-openapi"
+import { Commerce } from "../commerce"
+import { CommerceGenerator } from "../commerce/generator"
+import { z } from "zod"
+
+export const CommerceRoute = new Hono()
+
+CommerceRoute.get(
+ "/marketplaces",
+ describeRoute({
+ summary: "Get Marketplaces",
+ description: "Get the definitions of all supported marketplaces.",
+ operationId: "commerce.marketplaces",
+ responses: {
+ 200: { description: "List of Marketplaces", content: { "application/json": { schema: resolver(z.array(Commerce.MarketplaceConfig)) } } }
+ }
+ }),
+ async (c) => {
+ return c.json(await Commerce.getMarketplaces())
+ }
+)
+
+CommerceRoute.get(
+ "/catalog",
+ describeRoute({
+ summary: "Get Product Catalog",
+ description: "Get the current ground-truth product catalog.",
+ operationId: "commerce.catalog",
+ responses: {
+ 200: { description: "Product Catalog", content: { "application/json": { schema: resolver(z.array(Commerce.Product)) } } }
+ }
+ }),
+ async (c) => {
+ let catalog = await Commerce.getCatalog()
+ if (catalog.length === 0) {
+ // Fallback to mock for Phase 2 if empty
+ catalog = CommerceGenerator.mockProducts()
+ await Commerce.saveCatalog(catalog)
+ }
+ return c.json(catalog)
+ }
+)
+
+CommerceRoute.post(
+ "/generate",
+ describeRoute({
+ summary: "Generate Synthetic Data",
+ description: "Generate a fresh set of synthetic performance data.",
+ operationId: "commerce.generate",
+ responses: {
+ 200: { description: "Full Dataset", content: { "application/json": { schema: resolver(Commerce.Dataset) } } }
+ }
+ }),
+ async (c) => {
+ let catalog = await Commerce.getCatalog()
+ if (catalog.length === 0) catalog = CommerceGenerator.mockProducts()
+
+ const marketplaces = await Commerce.getMarketplaces()
+ const dataset = await CommerceGenerator.generate(catalog, marketplaces)
+
+ // In a real app we'd save this to a huge db. For Phase 2 we just return the snapshot or store it lightly.
+ // We'll store it in a 'latest' key for retrieval
+ // await Commerce.saveDataset(dataset) // Assuming implementation
+
+ return c.json(dataset)
+ }
+)
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 52457515b8e9..f14c84ba438d 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -31,6 +31,8 @@ import { Command } from "../command"
import { ProviderAuth } from "../provider/auth"
import { Global } from "../global"
import { ProjectRoute } from "./project"
+import { BrandRoute } from "./brand"
+import { CommerceRoute } from "./commerce"
import { ToolRegistry } from "../tool/registry"
import { zodToJsonSchema } from "zod-to-json-schema"
import { SessionPrompt } from "../session/prompt"
@@ -289,6 +291,8 @@ export namespace Server {
.use(validator("query", z.object({ directory: z.string().optional() })))
.route("/project", ProjectRoute)
+ .route("/brand", BrandRoute)
+ .route("/commerce", CommerceRoute)
.get(
"/pty",
diff --git a/packages/opencode/src/session/prompt/anthropic-20250930.txt b/packages/opencode/src/session/prompt/anthropic-20250930.txt
index 9af96024888d..107394e04353 100644
--- a/packages/opencode/src/session/prompt/anthropic-20250930.txt
+++ b/packages/opencode/src/session/prompt/anthropic-20250930.txt
@@ -4,72 +4,16 @@ IMPORTANT: Assist with defensive security tasks only. Refuse to create, modify,
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
If the user asks for help or wants to give feedback inform them of the following:
-- /help: Get help with using Claude Code
-- To give feedback, users should report the issue at https://github.com/anthropics/claude-code/issues
+- /help: Get help with using ShopOS
+- To give feedback, users should report the issue at https://github.com/anomalyco/shopos/issues
-When the user directly asks about Claude Code (eg. "can Claude Code do...", "does Claude Code have..."), or asks in second person (eg. "are you able...", "can you do..."), or asks how to use a specific Claude Code feature (eg. implement a hook, or write a slash command), use the WebFetch tool to gather information to answer the question from Claude Code docs. The list of available docs is available at https://docs.claude.com/en/docs/claude-code/claude_code_docs_map.md.
+When the user directly asks about ShopOS (eg. "can ShopOS do...", "does ShopOS have..."), or asks in second person (eg. "are you able...", "can you do..."), or asks how to use a specific ShopOS feature (eg. implement a hook, or write a slash command), use the WebFetch tool to gather information to answer the question from ShopOS docs. The list of available docs is available at https://shopos.ai/docs.
# Tone and style
-You should be concise, direct, and to the point, while providing complete information and matching the level of detail you provide in your response with the level of complexity of the user's query or the work you have completed.
-A concise response is generally less than 4 lines, not including tool calls or code generated. You should provide more detail when the task is complex or when the user asks you to.
-IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-3 sentences or a short paragraph, please do.
-IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
-Do not add additional code explanation summary unless requested by the user. After working on a file, briefly confirm that you have completed the task, rather than providing an explanation of what you did.
-Answer the user's question directly, avoiding any elaboration, explanation, introduction, conclusion, or excessive details. Brief answers are best, but be sure to provide complete information. You MUST avoid extra preamble before/after your response, such as "The answer is .", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...".
-
-Here are some examples to demonstrate appropriate verbosity:
-
-user: 2 + 2
-assistant: 4
-
-
-
-user: what is 2+2?
-assistant: 4
-
-
-
-user: is 11 a prime number?
-assistant: Yes
-
-
-
-user: what command should I run to list files in the current directory?
-assistant: ls
-
-
-
-user: what command should I run to watch files in the current directory?
-assistant: [runs ls to list the files in the current directory, then read docs/commands in the relevant file to find out how to watch files]
-npm run dev
-
-
-
-user: How many golf balls fit inside a jetta?
-assistant: 150000
-
-
-
-user: what files are in the directory src/?
-assistant: [runs ls and sees foo.c, bar.c, baz.c]
-user: which file contains the implementation of foo?
-assistant: src/foo.c
-
-When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
-Remember that your output will be displayed on a command line interface. Your responses can use Github-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
-Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session.
-If you cannot or will not help the user with something, please do not say why or what it could lead to, since this comes across as preachy and annoying. Please offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences.
-Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
-IMPORTANT: Keep your responses short, since they will be displayed on a command line interface.
-
-# Proactiveness
-You are allowed to be proactive, but only when the user asks you to do something. You should strive to strike a balance between:
-- Doing the right thing when asked, including taking actions and follow-up actions
-- Not surprising the user with actions you take without asking
-For example, if the user asks you how to approach something, you should do your best to answer their question first, and not immediately jump into taking actions.
+You should be concise, direct, and to the point, while providing complete information and matching the level of detail you provide in your response with the level of complexity of the user's query or the work you have completed.
# Professional objectivity
-Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if Claude honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs.
+Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if ShopOS honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs.
# Task Management
You have access to the TodoWrite tools to help you manage and plan tasks. Use these tools VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress.
diff --git a/packages/opencode/src/session/prompt/anthropic.txt b/packages/opencode/src/session/prompt/anthropic.txt
index f9050a37bf78..c0c025b33bbd 100644
--- a/packages/opencode/src/session/prompt/anthropic.txt
+++ b/packages/opencode/src/session/prompt/anthropic.txt
@@ -1,4 +1,4 @@
-You are OpenCode, the best coding agent on the planet.
+You are ShopOS, the best coding agent on the planet.
You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
@@ -7,9 +7,9 @@ IMPORTANT: You must NEVER generate or guess URLs for the user unless you are con
If the user asks for help or wants to give feedback inform them of the following:
- ctrl+p to list available actions
- To give feedback, users should report the issue at
- https://github.com/anomalyco/opencode
+ https://github.com/anomalyco/shopos
-When the user directly asks about OpenCode (eg. "can OpenCode do...", "does OpenCode have..."), or asks in second person (eg. "are you able...", "can you do..."), or asks how to use a specific OpenCode feature (eg. implement a hook, write a slash command, or install an MCP server), use the WebFetch tool to gather information to answer the question from OpenCode docs. The list of available docs is available at https://opencode.ai/docs
+When the user directly asks about ShopOS (eg. "can ShopOS do...", "does ShopOS have..."), or asks in second person (eg. "are you able...", "can you do..."), or asks how to use a specific ShopOS feature (eg. implement a hook, write a slash command, or install an MCP server), use the WebFetch tool to gather information to answer the question from ShopOS docs. The list of available docs is available at https://shopos.ai/docs
# Tone and style
- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
@@ -18,7 +18,7 @@ When the user directly asks about OpenCode (eg. "can OpenCode do...", "does Open
- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. This includes markdown files.
# Professional objectivity
-Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if OpenCode honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs.
+Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if ShopOS honestly applies the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs.
# Task Management
You have access to the TodoWrite tools to help you manage and plan tasks. Use these tools VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress.
diff --git a/packages/opencode/src/tool/commerce.ts b/packages/opencode/src/tool/commerce.ts
new file mode 100644
index 000000000000..fd2e961a9602
--- /dev/null
+++ b/packages/opencode/src/tool/commerce.ts
@@ -0,0 +1,71 @@
+import { Tool } from "./tool"
+import { z } from "zod"
+import { Commerce } from "../commerce"
+import { CommerceGenerator } from "../commerce/generator"
+import description from "./commerce.txt" with { type: "text" }
+
+import { Instance } from "../project/instance"
+
+export const CommerceTool = Tool.define("commerce", {
+ description,
+ parameters: z.object({
+ action: z.enum(["marketplaces", "catalog", "performance"]),
+ start: z.string().optional().describe("Start date for performance data (YYYY-MM-DD)"),
+ end: z.string().optional().describe("End date for performance data (YYYY-MM-DD)"),
+ }),
+ execute: async ({ action, start, end }) => {
+
+ if (action === "marketplaces") {
+ const marketplaces = await Commerce.getMarketplaces()
+ return JSON.stringify(marketplaces, null, 2)
+ }
+
+ if (action === "catalog") {
+ let catalog = await Commerce.getCatalog()
+ if (catalog.length === 0) {
+ // Auto-seed if empty
+ catalog = CommerceGenerator.mockProducts()
+ await Commerce.saveCatalog(catalog)
+ }
+ return JSON.stringify(catalog, null, 2)
+ }
+
+ if (action === "performance") {
+ // 1. Try to get existing performance data
+ // Note: Commerce.getPerformance uses Instance.project.id internally
+
+ let data = await Commerce.getPerformance({
+ start: start || new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
+ end: end || new Date().toISOString()
+ })
+
+ // 2. If no data, generate it on the fly (Just-in-Time Synthetic Data)
+ if (data.length === 0) {
+ let catalog = await Commerce.getCatalog()
+ if (catalog.length === 0) {
+ catalog = CommerceGenerator.mockProducts()
+ await Commerce.saveCatalog(catalog)
+ }
+
+ const marketplaces = await Commerce.getMarketplaces()
+ const dataset = await CommerceGenerator.generate(catalog, marketplaces)
+
+ // Save it so future queries work?
+ // For Phase 2, we just return the generated data as "Live Synthetic"
+ // In a real app we would persist this.
+ data = dataset.performance
+
+ // Let's attach the economics to the output too for the agent to see
+ return JSON.stringify({
+ performance: dataset.performance,
+ economics: dataset.economics,
+ note: "Data was synthetically generated because no historical records were found."
+ }, null, 2)
+ }
+
+ return JSON.stringify(data, null, 2)
+ }
+
+ return "Invalid action"
+ },
+})
diff --git a/packages/opencode/src/tool/commerce.txt b/packages/opencode/src/tool/commerce.txt
new file mode 100644
index 000000000000..79114876984f
--- /dev/null
+++ b/packages/opencode/src/tool/commerce.txt
@@ -0,0 +1,14 @@
+Query the Phase 2 Commerce Substrate for marketplace data.
+This tool allows you to:
+1. List supported marketplaces and their rules (commission, fees).
+2. Get the Ground Truth Product Catalog.
+3. correct query comprehensive, labeled performance data (Sales, Revenue, ROI) across all marketplaces.
+
+The data returned is "Decision Grade" - meaning it identifies its source (Real vs Synthetic) and includes fully calculated unit economics.
+
+Usage:
+- Use `action="marketplaces"` to see available platforms.
+- Use `action="catalog"` to see products.
+- Use `action="performance"` with `start` and `end` dates to get sales data.
+
+If data is missing, the tool will automatically generate comprehensive synthetic data for the requesting period.
diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts
index 82bf7f563282..a7f83160424a 100644
--- a/packages/opencode/src/tool/registry.ts
+++ b/packages/opencode/src/tool/registry.ts
@@ -25,6 +25,7 @@ import { Flag } from "@/flag/flag"
import { Log } from "@/util/log"
import { LspTool } from "./lsp"
import { Truncate } from "./truncation"
+import { CommerceTool } from "./commerce"
export namespace ToolRegistry {
const log = Log.create({ service: "tool.registry" })
@@ -107,6 +108,7 @@ export namespace ToolRegistry {
WebSearchTool,
CodeSearchTool,
SkillTool,
+ CommerceTool,
...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []),
...(config.experimental?.batch_tool === true ? [BatchTool] : []),
...custom,
diff --git a/summary.md b/summary.md
new file mode 100644
index 000000000000..567e377fdcb1
--- /dev/null
+++ b/summary.md
@@ -0,0 +1,64 @@
+# ShopOS Development Environment Summary
+
+This document provides a detailed overview of the ShopOS (formerly OpenCode) development environment, specifically focusing on the recent rebranding activity and the underlying architecture of the CLI tool you are interacting with.
+
+## 1. Context & Activity Overview
+
+You are currently working in a **refactoring branch** (`refactor/shopos-branding`) of the ShopOS repository. The primary objective has been to rebrand the open-source "OpenCode" agent into a custom "ShopOS" internal tool.
+
+### Recent Actions
+- **Rebranding:** All user-facing references to "OpenCode" (CLI name, logos, system prompts, help text) have been updated to "ShopOS".
+- **CLI Binary:** The executable commands have been changed from `opencode` to `shopos`.
+- **System Identity:** The AI agent now identifies itself as "ShopOS" in conversations and directs users to `shopos.ai/docs`.
+
+## 2. Architecture of the CLI Agent
+
+The tool you are using is a sophisticated **Agentic CLI** built on a Client/Server architecture. It is designed to act as an autonomous pair programmer.
+
+### Core Components
+
+1. **Client (CLI/TUI):**
+ * **Entry Point:** `src/index.ts` (now running as `shopos`).
+ * **UI:** A terminal user interface (TUI) built with `OpenTUI` (a custom library likely using Ink or similar logic) and `SolidJS` for reactivity.
+ * **Function:** Handles user input, rendering, and communicating with the server.
+
+2. **Server (The Brain):**
+ * **Session Management:** `src/session/index.ts` manages stateful conversations. Each "chat" is a persistent session stored locally.
+ * **Agent Loop:** The core logic runs a continuous loop: `User Input` -> `Reasoning` -> `Tool Selection` -> `Execution` -> `Observation` -> `Response`.
+ * **LLM Integration:** Connects to models (Claude, OpenAI, etc.) via the standard `ai` SDK.
+
+3. **The "Build" Process (What you saw in the terminal):**
+ * **Interactive REPL:** The CLI runs a Read-Eval-Print Loop.
+ * **Context:** It maintains a "Project Context" (files, git status, architecture).
+ * **Tools:** The agent has access to specific tools:
+ * `bash`: Execute shell commands.
+ * `edit`: Modify files.
+ * `read`: Read file contents.
+ * `manage`: Create/Update plans (Todo lists).
+ * **Session Names:** The system auto-generates names for sessions (e.g., `big-pickle`, `high-flyer`) to easily identify them in history.
+
+## 3. How It Functions (The "Magic")
+
+When you type a request like *"Build the store interface"*:
+
+1. **Ingestion:** The CLI packages your text + current file context + active terminal output.
+2. **Reasoning:** The LLM (e.g., Sonnet 3.5) analyzes the request against the `PRODUCT_KNOWLEDGE_BASE.md` (your long-term memory).
+3. **Planning:** It breaks the task down (e.g., "1. List products", "2. Create Component", "3. Update Route").
+4. **Execution:**
+ * It might run a `ls` or `grep` to find files.
+ * It calls `write_file` to generate code.
+ * It runs `bun build` to verify changes.
+5. **Feedback:** If a command fails, it reads the error, self-corrects, and retries.
+
+## 4. Current Status
+
+* **Branch:** `refactor/shopos-branding`
+* **Build Status:** The dev server (`bun run dev`) is currently active.
+* **Next Steps:** The rebranding is technically complete in the codebase. The next logical step would be to build the binary (`bun run build`) or test the new `shopos` command to ensure the transition is seamless.
+
+## 5. Key Files Modified
+
+* `packages/opencode/package.json`: Binary name change.
+* `packages/opencode/src/index.ts`: Runtime identity change.
+* `packages/opencode/src/cli/ui.ts`: ASCII Logo update.
+* `packages/opencode/src/session/prompt/anthropic.txt`: System personality update.