Compare commits

...

6 Commits

Author SHA1 Message Date
Simon c0ce6e7d14 chore(version): bump version to 0.2.0 2026-01-15 20:47:39 +08:00
Simon 8f5f98f889 feat(agent)!: rename brain to reflection; fix undefined lines in panel 2026-01-15 20:45:40 +08:00
Simon ee4719cdcf feat: observation 2026-01-15 20:28:39 +08:00
Simon b4c82b7833 refactor: mv getBrowserState to Controller; simplify Agent 2026-01-15 20:17:50 +08:00
Simon aefc3cfb89 Merge pull request #111 from alibaba/feat/extend-agent-history-for-observation
feat: extend history type
2026-01-15 19:57:41 +08:00
Simon 7f2b8c2766 feat: extend history type 2026-01-15 19:18:25 +08:00
12 changed files with 224 additions and 142 deletions
+12 -12
View File
@@ -1,12 +1,12 @@
{ {
"name": "root", "name": "root",
"version": "0.1.0", "version": "0.2.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "root", "name": "root",
"version": "0.1.0", "version": "0.2.0",
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"packages/page-controller", "packages/page-controller",
@@ -8147,15 +8147,15 @@
}, },
"packages/cdn": { "packages/cdn": {
"name": "@page-agent/cdn", "name": "@page-agent/cdn",
"version": "0.1.0", "version": "0.2.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"page-agent": "0.1.0" "page-agent": "0.2.0"
} }
}, },
"packages/llms": { "packages/llms": {
"name": "@page-agent/llms", "name": "@page-agent/llms",
"version": "0.1.0", "version": "0.2.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chalk": "^5.6.2", "chalk": "^5.6.2",
@@ -8163,19 +8163,19 @@
} }
}, },
"packages/page-agent": { "packages/page-agent": {
"version": "0.1.0", "version": "0.2.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@page-agent/llms": "0.1.0", "@page-agent/llms": "0.2.0",
"@page-agent/page-controller": "0.1.0", "@page-agent/page-controller": "0.2.0",
"@page-agent/ui": "0.1.0", "@page-agent/ui": "0.2.0",
"chalk": "^5.6.2", "chalk": "^5.6.2",
"zod": "^4.3.5" "zod": "^4.3.5"
} }
}, },
"packages/page-controller": { "packages/page-controller": {
"name": "@page-agent/page-controller", "name": "@page-agent/page-controller",
"version": "0.1.0", "version": "0.2.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ai-motion": "^0.4.8" "ai-motion": "^0.4.8"
@@ -8183,12 +8183,12 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@page-agent/ui", "name": "@page-agent/ui",
"version": "0.1.0", "version": "0.2.0",
"license": "MIT" "license": "MIT"
}, },
"packages/website": { "packages/website": {
"name": "@page-agent/website", "name": "@page-agent/website",
"version": "0.1.0", "version": "0.2.0",
"dependencies": { "dependencies": {
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-separator": "^1.1.8",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "root", "name": "root",
"private": true, "private": true,
"version": "0.1.0", "version": "0.2.0",
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
"packages/page-controller", "packages/page-controller",
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "@page-agent/cdn", "name": "@page-agent/cdn",
"private": false, "private": false,
"version": "0.1.0", "version": "0.2.0",
"type": "module", "type": "module",
"files": [ "files": [
"dist/" "dist/"
@@ -18,6 +18,6 @@
"build": "vite build && vite build --mode demo" "build": "vite build && vite build --mode demo"
}, },
"dependencies": { "dependencies": {
"page-agent": "0.1.0" "page-agent": "0.2.0"
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@page-agent/llms", "name": "@page-agent/llms",
"version": "0.1.0", "version": "0.2.0",
"type": "module", "type": "module",
"main": "./dist/lib/page-agent-llms.js", "main": "./dist/lib/page-agent-llms.js",
"module": "./dist/lib/page-agent-llms.js", "module": "./dist/lib/page-agent-llms.js",
+4 -4
View File
@@ -1,7 +1,7 @@
{ {
"name": "page-agent", "name": "page-agent",
"private": false, "private": false,
"version": "0.1.0", "version": "0.2.0",
"type": "module", "type": "module",
"main": "./dist/esm/page-agent.js", "main": "./dist/esm/page-agent.js",
"module": "./dist/esm/page-agent.js", "module": "./dist/esm/page-agent.js",
@@ -46,8 +46,8 @@
"dependencies": { "dependencies": {
"chalk": "^5.6.2", "chalk": "^5.6.2",
"zod": "^4.3.5", "zod": "^4.3.5",
"@page-agent/llms": "0.1.0", "@page-agent/llms": "0.2.0",
"@page-agent/page-controller": "0.1.0", "@page-agent/page-controller": "0.2.0",
"@page-agent/ui": "0.1.0" "@page-agent/ui": "0.2.0"
} }
} }
+140 -78
View File
@@ -16,7 +16,7 @@ import { normalizeResponse, trimLines, uid, waitUntil } from './utils'
import { assert } from './utils/assert' import { assert } from './utils/assert'
/** /**
* Agent brain state - the reflection-before-action model * Agent reflection state - the reflection-before-action model
* *
* Every tool call must first reflect on: * Every tool call must first reflect on:
* - evaluation_previous_goal: How well did the previous action achieve its goal? * - evaluation_previous_goal: How well did the previous action achieve its goal?
@@ -50,8 +50,12 @@ export interface MacroToolResult {
export type { PageAgentConfig } export type { PageAgentConfig }
export { tool, type PageAgentTool } from './tools' export { tool, type PageAgentTool } from './tools'
export interface AgentHistory { /**
brain: Partial<AgentReflection> * A single agent step with reflection and action
*/
export interface AgentStep {
type: 'step'
reflection: Partial<AgentReflection>
action: { action: {
name: string name: string
input: any input: any
@@ -66,10 +70,33 @@ export interface AgentHistory {
} }
} }
/**
* Persistent observation event (stays in memory)
*/
export interface ObservationEvent {
type: 'observation'
content: string
}
/**
* User takeover event
*/
export interface UserTakeoverEvent {
type: 'user_takeover'
}
/**
* Union type for all history events
*/
export type HistoryEvent = AgentStep | ObservationEvent | UserTakeoverEvent
/** @deprecated Use AgentStep instead */
export type AgentHistory = AgentStep
export interface ExecutionResult { export interface ExecutionResult {
success: boolean success: boolean
data: string data: string
history: AgentHistory[] history: HistoryEvent[]
} }
export class PageAgent extends EventTarget { export class PageAgent extends EventTarget {
@@ -83,7 +110,6 @@ export class PageAgent extends EventTarget {
taskId = '' taskId = ''
#llm: LLM #llm: LLM
#totalWaitTime = 0
#abortController = new AbortController() #abortController = new AbortController()
#llmRetryListener: ((e: Event) => void) | null = null #llmRetryListener: ((e: Event) => void) | null = null
#llmErrorListener: ((e: Event) => void) | null = null #llmErrorListener: ((e: Event) => void) | null = null
@@ -92,8 +118,16 @@ export class PageAgent extends EventTarget {
/** PageController for DOM operations */ /** PageController for DOM operations */
pageController: PageController pageController: PageController
/** History records */ /** Runtime states for tracking across steps */
history: AgentHistory[] = [] states = {
/** Accumulated wait time in seconds, used by wait tool */
totalWaitTime: 0,
/** Last known URL for detecting navigation */
lastURL: '',
}
/** History event stream */
history: HistoryEvent[] = []
constructor(config: PageAgentConfig) { constructor(config: PageAgentConfig) {
super() super()
@@ -150,6 +184,14 @@ export class PageAgent extends EventTarget {
window.addEventListener('beforeunload', this.#beforeUnloadListener) window.addEventListener('beforeunload', this.#beforeUnloadListener)
} }
/**
* Push a persistent observation to the history event stream.
* This will be visible in <agent_history> and remain in memory across steps.
*/
pushObservation(content: string): void {
this.history.push({ type: 'observation', content })
}
async execute(task: string): Promise<ExecutionResult> { async execute(task: string): Promise<ExecutionResult> {
if (!task) throw new Error('Task is required') if (!task) throw new Error('Task is required')
this.task = task this.task = task
@@ -177,10 +219,18 @@ export class PageAgent extends EventTarget {
this.history = [] this.history = []
// Reset states
this.states = {
totalWaitTime: 0,
lastURL: '',
}
try { try {
let step = 0 let step = 0
while (true) { while (true) {
await this.#generateObservations(step)
await onBeforeStep.call(this, step) await onBeforeStep.call(this, step)
console.group(`step: ${step}`) console.group(`step: ${step}`)
@@ -216,23 +266,24 @@ export class PageAgent extends EventTarget {
const macroResult = result.toolResult as MacroToolResult const macroResult = result.toolResult as MacroToolResult
const input = macroResult.input const input = macroResult.input
const output = macroResult.output const output = macroResult.output
const brain = { const reflection: Partial<AgentReflection> = {
evaluation_previous_goal: input.evaluation_previous_goal || '', evaluation_previous_goal: input.evaluation_previous_goal,
memory: input.memory || '', memory: input.memory,
next_goal: input.next_goal || '', next_goal: input.next_goal,
} }
const actionName = Object.keys(input.action)[0] const actionName = Object.keys(input.action)[0]
const action = { const action: AgentStep['action'] = {
name: actionName, name: actionName,
input: input.action[actionName], input: input.action[actionName],
output: output, output: output,
} }
this.history.push({ this.history.push({
brain, type: 'step',
reflection,
action, action,
usage: result.usage, usage: result.usage,
}) } as AgentStep)
console.log(chalk.green('Step finished:'), actionName) console.log(chalk.green('Step finished:'), actionName)
console.groupEnd() console.groupEnd()
@@ -319,13 +370,20 @@ export class PageAgent extends EventTarget {
const toolName = Object.keys(action)[0] const toolName = Object.keys(action)[0]
const toolInput = action[toolName] const toolInput = action[toolName]
const brain = trimLines(`✅: ${input.evaluation_previous_goal}
💾: ${input.memory}
🎯: ${input.next_goal}
`)
console.log(brain) // Build reflection text, only include non-empty fields
this.panel.update({ type: 'thinking', text: brain }) const reflectionLines: string[] = []
if (input.evaluation_previous_goal)
reflectionLines.push(`✅: ${input.evaluation_previous_goal}`)
if (input.memory) reflectionLines.push(`💾: ${input.memory}`)
if (input.next_goal) reflectionLines.push(`🎯: ${input.next_goal}`)
const reflectionText = reflectionLines.length > 0 ? reflectionLines.join('\n') : ''
if (reflectionText) {
console.log(reflectionText)
this.panel.update({ type: 'thinking', text: reflectionText })
}
// Find the corresponding tool // Find the corresponding tool
const tool = tools.get(toolName) const tool = tools.get(toolName)
@@ -337,20 +395,14 @@ export class PageAgent extends EventTarget {
const startTime = Date.now() const startTime = Date.now()
// Execute tool, bind `this` to PageAgent // Execute tool, bind `this` to PageAgent
let result = await tool.execute.bind(this)(toolInput) const result = await tool.execute.bind(this)(toolInput)
const duration = Date.now() - startTime const duration = Date.now() - startTime
console.log(chalk.green.bold(`Tool (${toolName}) executed for ${duration}ms`), result) console.log(chalk.green.bold(`Tool (${toolName}) executed for ${duration}ms`), result)
if (toolName === 'wait') { // Reset wait time for non-wait tools
this.#totalWaitTime += Math.round(toolInput.seconds + duration / 1000) if (toolName !== 'wait') {
result += `\n<sys> You have waited ${this.#totalWaitTime} seconds accumulatively.` this.states.totalWaitTime = 0
if (this.#totalWaitTime >= 3)
result += '\nDo NOT wait any longer unless you have a good reason.\n'
result += '</sys>'
} else {
// For other tools, reset wait time
this.#totalWaitTime = 0
} }
// Briefly display execution result // Briefly display execution result
@@ -427,6 +479,34 @@ export class PageAgent extends EventTarget {
return result return result
} }
/**
* Generate observations before each step
* - URL change detection
* - Too many steps warning
* @todo loop detection
* @todo console error
*/
async #generateObservations(stepCount: number): Promise<void> {
// Detect URL change
const currentURL = await this.pageController.getCurrentUrl()
if (currentURL !== this.states.lastURL) {
this.pushObservation(`Page navigated to → ${currentURL}`)
this.states.lastURL = currentURL
}
// Warn about remaining steps
const remaining = MAX_STEPS - stepCount
if (remaining === 5) {
this.pushObservation(
`⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.`
)
} else if (remaining === 2) {
this.pushObservation(
`⚠️ Critical: Only ${remaining} steps left! You must finish the task or call done immediately.`
)
}
}
async #assembleUserPrompt(): Promise<string> { async #assembleUserPrompt(): Promise<string> {
let prompt = '' let prompt = ''
@@ -438,31 +518,42 @@ export class PageAgent extends EventTarget {
// - <step_info> // - <step_info>
// <agent_state> // <agent_state>
const stepCount = this.history.filter((e) => e.type === 'step').length
prompt += `<agent_state> prompt += `<agent_state>
<user_request> <user_request>
${this.task} ${this.task}
</user_request> </user_request>
<step_info> <step_info>
Step ${this.history.length + 1} of ${MAX_STEPS} max possible steps Step ${stepCount + 1} of ${MAX_STEPS} max possible steps
Current date and time: ${new Date().toISOString()} Current date and time: ${new Date().toISOString()}
</step_info> </step_info>
</agent_state> </agent_state>
` `
// <agent_history> // <agent_history>
// - <step_> // - <step_N> for steps
// - <sys> for observations and system messages
prompt += '\n<agent_history>\n' prompt += '\n<agent_history>\n'
this.history.forEach((history, index) => { let stepIndex = 0
prompt += `<step_${index + 1}> for (const event of this.history) {
Evaluation of Previous Step: ${history.brain.evaluation_previous_goal} if (event.type === 'step') {
Memory: ${history.brain.memory} stepIndex++
Next Goal: ${history.brain.next_goal} prompt += `<step_${stepIndex}>
Action Results: ${history.action.output} Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}
</step_${index + 1}> Memory: ${event.reflection.memory}
Next Goal: ${event.reflection.next_goal}
Action Results: ${event.action.output}
</step_${stepIndex}>
` `
}) } else if (event.type === 'observation') {
prompt += `<sys>${event.content}</sys>\n`
} else if (event.type === 'user_takeover') {
prompt += `<sys>User took over control and made changes to the page.</sys>\n`
}
}
prompt += '</agent_history>\n\n' prompt += '</agent_history>\n\n'
@@ -492,51 +583,22 @@ export class PageAgent extends EventTarget {
} }
async #getBrowserState(): Promise<string> { async #getBrowserState(): Promise<string> {
const pageUrl = await this.pageController.getCurrentUrl() const state = await this.pageController.getBrowserState()
const pageTitle = await this.pageController.getPageTitle()
const pi = await this.pageController.getPageInfo()
const viewportExpansion = await this.pageController.getViewportExpansion()
await this.pageController.updateTree()
let simplifiedHTML = await this.pageController.getSimplifiedHTML()
let content = state.content
if (this.config.transformPageContent) { if (this.config.transformPageContent) {
simplifiedHTML = await this.config.transformPageContent(simplifiedHTML) content = await this.config.transformPageContent(content)
} }
let prompt = trimLines(`<browser_state> return trimLines(`<browser_state>
Current Page: [${pageTitle}](${pageUrl}) Current Page: [${state.title}](${state.url})
Page info: ${pi.viewport_width}x${pi.viewport_height}px viewport, ${pi.page_width}x${pi.page_height}px total page size, ${pi.pages_above.toFixed(1)} pages above, ${pi.pages_below.toFixed(1)} pages below, ${pi.total_pages.toFixed(1)} total pages, at ${(pi.current_page_position * 100).toFixed(0)}% of page ${state.header}
${content}
${viewportExpansion === -1 ? 'Interactive elements from top layer of the current page (full page):' : 'Interactive elements from top layer of the current page inside the viewport:'} ${state.footer}
</browser_state>
`) `)
// Page header info
const has_content_above = pi.pixels_above > 4
if (has_content_above && viewportExpansion !== -1) {
prompt += `... ${pi.pixels_above} pixels above (${pi.pages_above.toFixed(1)} pages) - scroll to see more ...\n`
} else {
prompt += `[Start of page]\n`
}
// Current viewport info
prompt += simplifiedHTML
prompt += `\n`
// Page footer info
const has_content_below = pi.pixels_below > 4
if (has_content_below && viewportExpansion !== -1) {
prompt += `... ${pi.pixels_below} pixels below (${pi.pages_below.toFixed(1)} pages) - scroll to see more ...\n`
} else {
prompt += `[End of page]\n`
}
prompt += `</browser_state>\n`
return prompt
} }
dispose(reason?: string) { dispose(reason?: string) {
+2 -2
View File
@@ -2,7 +2,7 @@ import type { LLMConfig } from '@page-agent/llms'
import type { PageControllerConfig } from '@page-agent/page-controller' import type { PageControllerConfig } from '@page-agent/page-controller'
import type { SupportedLanguage } from '@page-agent/ui' import type { SupportedLanguage } from '@page-agent/ui'
import type { AgentHistory, ExecutionResult, PageAgent } from '../PageAgent' import type { ExecutionResult, HistoryEvent, PageAgent } from '../PageAgent'
import type { PageAgentTool } from '../tools' import type { PageAgentTool } from '../tools'
export type { LLMConfig } export type { LLMConfig }
@@ -65,7 +65,7 @@ export interface AgentConfig {
// @todo: remove `this` binding, pass agent as explicit parameter instead // @todo: remove `this` binding, pass agent as explicit parameter instead
onBeforeStep?: (this: PageAgent, stepCnt: number) => Promise<void> | void onBeforeStep?: (this: PageAgent, stepCnt: number) => Promise<void> | void
onAfterStep?: (this: PageAgent, stepCnt: number, history: AgentHistory[]) => Promise<void> | void onAfterStep?: (this: PageAgent, stepCnt: number, history: HistoryEvent[]) => Promise<void> | void
onBeforeTask?: (this: PageAgent) => Promise<void> | void onBeforeTask?: (this: PageAgent) => Promise<void> | void
onAfterTask?: (this: PageAgent, result: ExecutionResult) => Promise<void> | void onAfterTask?: (this: PageAgent, result: ExecutionResult) => Promise<void> | void
+9
View File
@@ -57,6 +57,15 @@ tools.set(
const actualWaitTime = Math.max(0, input.seconds - (Date.now() - lastTimeUpdate) / 1000) const actualWaitTime = Math.max(0, input.seconds - (Date.now() - lastTimeUpdate) / 1000)
console.log(`actualWaitTime: ${actualWaitTime} seconds`) console.log(`actualWaitTime: ${actualWaitTime} seconds`)
await waitFor(actualWaitTime) await waitFor(actualWaitTime)
this.states.totalWaitTime += input.seconds
if (this.states.totalWaitTime >= 3) {
this.pushObservation(
`You have waited ${this.states.totalWaitTime} seconds accumulatively. Do NOT wait any longer unless you have a good reason.`
)
}
return `✅ Waited for ${input.seconds} seconds.` return `✅ Waited for ${input.seconds} seconds.`
}, },
}) })
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@page-agent/page-controller", "name": "@page-agent/page-controller",
"version": "0.1.0", "version": "0.2.0",
"type": "module", "type": "module",
"main": "./dist/lib/page-controller.js", "main": "./dist/lib/page-controller.js",
"module": "./dist/lib/page-controller.js", "module": "./dist/lib/page-controller.js",
+50 -39
View File
@@ -32,6 +32,20 @@ export interface PageControllerConfig extends dom.DomConfig {
enableMask?: boolean enableMask?: boolean
} }
/**
* Structured browser state for LLM consumption
*/
export interface BrowserState {
url: string
title: string
/** Page info + scroll position hint (e.g. "Page info: 1920x1080px...\n[Start of page]") */
header: string
/** Simplified HTML of interactive elements */
content: string
/** Page footer hint (e.g. "... 300 pixels below ..." or "[End of page]") */
footer: string
}
interface ActionResult { interface ActionResult {
success: boolean success: boolean
message: string message: string
@@ -93,42 +107,6 @@ export class PageController extends EventTarget {
return window.location.href return window.location.href
} }
/**
* Get current page title
*/
async getPageTitle(): Promise<string> {
return document.title
}
/**
* Get page scroll and size info
*/
async getPageInfo() {
return getPageInfo()
}
/**
* Get the simplified HTML representation of the page.
* This is used by LLM to understand the page structure.
*/
async getSimplifiedHTML(): Promise<string> {
return this.simplifiedHTML
}
/**
* Get text description for an element by index
*/
async getElementText(index: number): Promise<string | undefined> {
return this.elementTextMap.get(index)
}
/**
* Get total number of indexed interactive elements
*/
async getElementCount(): Promise<number> {
return this.selectorMap.size
}
/** /**
* Get last tree update timestamp * Get last tree update timestamp
*/ */
@@ -137,10 +115,43 @@ export class PageController extends EventTarget {
} }
/** /**
* Get the viewport expansion setting * Get structured browser state for LLM consumption.
* Automatically calls updateTree() to refresh the DOM state.
*/ */
async getViewportExpansion(): Promise<number> { async getBrowserState(): Promise<BrowserState> {
return this.config.viewportExpansion ?? VIEWPORT_EXPANSION const url = window.location.href
const title = document.title
const pi = getPageInfo()
const viewportExpansion = this.config.viewportExpansion ?? VIEWPORT_EXPANSION
await this.updateTree()
const content = this.simplifiedHTML
// Build header: page info + scroll position hint
const pageInfoLine = `Page info: ${pi.viewport_width}x${pi.viewport_height}px viewport, ${pi.page_width}x${pi.page_height}px total page size, ${pi.pages_above.toFixed(1)} pages above, ${pi.pages_below.toFixed(1)} pages below, ${pi.total_pages.toFixed(1)} total pages, at ${(pi.current_page_position * 100).toFixed(0)}% of page`
const elementsLabel =
viewportExpansion === -1
? 'Interactive elements from top layer of the current page (full page):'
: 'Interactive elements from top layer of the current page inside the viewport:'
const hasContentAbove = pi.pixels_above > 4
const scrollHintAbove =
hasContentAbove && viewportExpansion !== -1
? `... ${pi.pixels_above} pixels above (${pi.pages_above.toFixed(1)} pages) - scroll to see more ...`
: '[Start of page]'
const header = `${pageInfoLine}\n\n${elementsLabel}\n\n${scrollHintAbove}`
// Build footer: scroll position hint
const hasContentBelow = pi.pixels_below > 4
const footer =
hasContentBelow && viewportExpansion !== -1
? `... ${pi.pixels_below} pixels below (${pi.pages_below.toFixed(1)} pages) - scroll to see more ...`
: '[End of page]'
return { url, title, header, content, footer }
} }
// ======= DOM Tree Operations ======= // ======= DOM Tree Operations =======
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@page-agent/ui", "name": "@page-agent/ui",
"version": "0.1.0", "version": "0.2.0",
"type": "module", "type": "module",
"main": "./dist/lib/page-agent-ui.js", "main": "./dist/lib/page-agent-ui.js",
"module": "./dist/lib/page-agent-ui.js", "module": "./dist/lib/page-agent-ui.js",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@page-agent/website", "name": "@page-agent/website",
"private": true, "private": true,
"version": "0.1.0", "version": "0.2.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0", "dev": "vite --host 0.0.0.0",