@oratis/lisa 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +436 -0
- package/README.zh-CN.md +450 -0
- package/channels.example.json +45 -0
- package/completions/README.md +66 -0
- package/completions/_lisa +140 -0
- package/completions/lisa.bash +94 -0
- package/completions/lisa.fish +85 -0
- package/dist/agent.d.ts +70 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +275 -0
- package/dist/agent.js.map +1 -0
- package/dist/approval.d.ts +9 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +47 -0
- package/dist/approval.js.map +1 -0
- package/dist/channels/config.d.ts +11 -0
- package/dist/channels/config.d.ts.map +1 -0
- package/dist/channels/config.js +27 -0
- package/dist/channels/config.js.map +1 -0
- package/dist/channels/discord.d.ts +28 -0
- package/dist/channels/discord.d.ts.map +1 -0
- package/dist/channels/discord.js +98 -0
- package/dist/channels/discord.js.map +1 -0
- package/dist/channels/feishu.d.ts +53 -0
- package/dist/channels/feishu.d.ts.map +1 -0
- package/dist/channels/feishu.js +228 -0
- package/dist/channels/feishu.js.map +1 -0
- package/dist/channels/imessage.d.ts +20 -0
- package/dist/channels/imessage.d.ts.map +1 -0
- package/dist/channels/imessage.js +124 -0
- package/dist/channels/imessage.js.map +1 -0
- package/dist/channels/registry.d.ts +8 -0
- package/dist/channels/registry.d.ts.map +1 -0
- package/dist/channels/registry.js +29 -0
- package/dist/channels/registry.js.map +1 -0
- package/dist/channels/router.d.ts +28 -0
- package/dist/channels/router.d.ts.map +1 -0
- package/dist/channels/router.js +140 -0
- package/dist/channels/router.js.map +1 -0
- package/dist/channels/slack.d.ts +43 -0
- package/dist/channels/slack.d.ts.map +1 -0
- package/dist/channels/slack.js +164 -0
- package/dist/channels/slack.js.map +1 -0
- package/dist/channels/telegram.d.ts +26 -0
- package/dist/channels/telegram.d.ts.map +1 -0
- package/dist/channels/telegram.js +103 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/types.d.ts +20 -0
- package/dist/channels/types.d.ts.map +1 -0
- package/dist/channels/types.js +2 -0
- package/dist/channels/types.js.map +1 -0
- package/dist/channels/webhook.d.ts +34 -0
- package/dist/channels/webhook.d.ts.map +1 -0
- package/dist/channels/webhook.js +139 -0
- package/dist/channels/webhook.js.map +1 -0
- package/dist/cli/colors.d.ts +30 -0
- package/dist/cli/colors.d.ts.map +1 -0
- package/dist/cli/colors.js +53 -0
- package/dist/cli/colors.js.map +1 -0
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +172 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/monitor.d.ts +2 -0
- package/dist/cli/monitor.d.ts.map +1 -0
- package/dist/cli/monitor.js +141 -0
- package/dist/cli/monitor.js.map +1 -0
- package/dist/cli/repl.d.ts +7 -0
- package/dist/cli/repl.d.ts.map +1 -0
- package/dist/cli/repl.js +68 -0
- package/dist/cli/repl.js.map +1 -0
- package/dist/cli/status.d.ts +2 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +176 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/wishlist.d.ts +7 -0
- package/dist/cli/wishlist.d.ts.map +1 -0
- package/dist/cli/wishlist.js +77 -0
- package/dist/cli/wishlist.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +954 -0
- package/dist/cli.js.map +1 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +128 -0
- package/dist/env.js.map +1 -0
- package/dist/fs-utils.d.ts +6 -0
- package/dist/fs-utils.d.ts.map +1 -0
- package/dist/fs-utils.js +36 -0
- package/dist/fs-utils.js.map +1 -0
- package/dist/heartbeat/config.d.ts +13 -0
- package/dist/heartbeat/config.d.ts.map +1 -0
- package/dist/heartbeat/config.js +14 -0
- package/dist/heartbeat/config.js.map +1 -0
- package/dist/heartbeat/install.d.ts +15 -0
- package/dist/heartbeat/install.d.ts.map +1 -0
- package/dist/heartbeat/install.js +180 -0
- package/dist/heartbeat/install.js.map +1 -0
- package/dist/heartbeat/runner.d.ts +16 -0
- package/dist/heartbeat/runner.d.ts.map +1 -0
- package/dist/heartbeat/runner.js +180 -0
- package/dist/heartbeat/runner.js.map +1 -0
- package/dist/hooks/runner.d.ts +23 -0
- package/dist/hooks/runner.d.ts.map +1 -0
- package/dist/hooks/runner.js +55 -0
- package/dist/hooks/runner.js.map +1 -0
- package/dist/idle/runner.d.ts +16 -0
- package/dist/idle/runner.d.ts.map +1 -0
- package/dist/idle/runner.js +45 -0
- package/dist/idle/runner.js.map +1 -0
- package/dist/idle/watcher.d.ts +35 -0
- package/dist/idle/watcher.d.ts.map +1 -0
- package/dist/idle/watcher.js +67 -0
- package/dist/idle/watcher.js.map +1 -0
- package/dist/llm.d.ts +8 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +12 -0
- package/dist/llm.js.map +1 -0
- package/dist/mcp/client.d.ts +11 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +68 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/config.d.ts +14 -0
- package/dist/mcp/config.d.ts.map +1 -0
- package/dist/mcp/config.js +28 -0
- package/dist/mcp/config.js.map +1 -0
- package/dist/memory/search_tool.d.ts +8 -0
- package/dist/memory/search_tool.d.ts.map +1 -0
- package/dist/memory/search_tool.js +26 -0
- package/dist/memory/search_tool.js.map +1 -0
- package/dist/memory/store.d.ts +7 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +46 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/memory/tool.d.ts +13 -0
- package/dist/memory/tool.d.ts.map +1 -0
- package/dist/memory/tool.js +53 -0
- package/dist/memory/tool.js.map +1 -0
- package/dist/memory/vector.d.ts +25 -0
- package/dist/memory/vector.d.ts.map +1 -0
- package/dist/memory/vector.js +96 -0
- package/dist/memory/vector.js.map +1 -0
- package/dist/mood-bus.d.ts +16 -0
- package/dist/mood-bus.d.ts.map +1 -0
- package/dist/mood-bus.js +21 -0
- package/dist/mood-bus.js.map +1 -0
- package/dist/paths.d.ts +8 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +10 -0
- package/dist/paths.js.map +1 -0
- package/dist/plugins/loader.d.ts +4 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +156 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/plugins/types.d.ts +43 -0
- package/dist/plugins/types.d.ts.map +1 -0
- package/dist/plugins/types.js +2 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/prompt.d.ts +21 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +187 -0
- package/dist/prompt.js.map +1 -0
- package/dist/providers/anthropic.d.ts +11 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +102 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/gemini.d.ts +11 -0
- package/dist/providers/gemini.d.ts.map +1 -0
- package/dist/providers/gemini.js +190 -0
- package/dist/providers/gemini.js.map +1 -0
- package/dist/providers/openai.d.ts +11 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +175 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/registry.d.ts +44 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +209 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/types.d.ts +33 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/proxy-bootstrap.d.ts +17 -0
- package/dist/proxy-bootstrap.d.ts.map +1 -0
- package/dist/proxy-bootstrap.js +101 -0
- package/dist/proxy-bootstrap.js.map +1 -0
- package/dist/reflect.d.ts +13 -0
- package/dist/reflect.d.ts.map +1 -0
- package/dist/reflect.js +289 -0
- package/dist/reflect.js.map +1 -0
- package/dist/sandbox/macos.d.ts +5 -0
- package/dist/sandbox/macos.d.ts.map +1 -0
- package/dist/sandbox/macos.js +31 -0
- package/dist/sandbox/macos.js.map +1 -0
- package/dist/sandbox/sandbox.d.ts +15 -0
- package/dist/sandbox/sandbox.d.ts.map +1 -0
- package/dist/sandbox/sandbox.js +39 -0
- package/dist/sandbox/sandbox.js.map +1 -0
- package/dist/sessions/active.d.ts +4 -0
- package/dist/sessions/active.d.ts.map +1 -0
- package/dist/sessions/active.js +33 -0
- package/dist/sessions/active.js.map +1 -0
- package/dist/sessions/list.d.ts +16 -0
- package/dist/sessions/list.d.ts.map +1 -0
- package/dist/sessions/list.js +78 -0
- package/dist/sessions/list.js.map +1 -0
- package/dist/sessions/store.d.ts +27 -0
- package/dist/sessions/store.d.ts.map +1 -0
- package/dist/sessions/store.js +98 -0
- package/dist/sessions/store.js.map +1 -0
- package/dist/skills/executable.d.ts +45 -0
- package/dist/skills/executable.d.ts.map +1 -0
- package/dist/skills/executable.js +207 -0
- package/dist/skills/executable.js.map +1 -0
- package/dist/skills/frontmatter.d.ts +8 -0
- package/dist/skills/frontmatter.d.ts.map +1 -0
- package/dist/skills/frontmatter.js +84 -0
- package/dist/skills/frontmatter.js.map +1 -0
- package/dist/skills/manager.d.ts +9 -0
- package/dist/skills/manager.d.ts.map +1 -0
- package/dist/skills/manager.js +116 -0
- package/dist/skills/manager.js.map +1 -0
- package/dist/skills/tool.d.ts +13 -0
- package/dist/skills/tool.d.ts.map +1 -0
- package/dist/skills/tool.js +87 -0
- package/dist/skills/tool.js.map +1 -0
- package/dist/soul/birth.d.ts +13 -0
- package/dist/soul/birth.d.ts.map +1 -0
- package/dist/soul/birth.js +171 -0
- package/dist/soul/birth.js.map +1 -0
- package/dist/soul/git.d.ts +36 -0
- package/dist/soul/git.d.ts.map +1 -0
- package/dist/soul/git.js +212 -0
- package/dist/soul/git.js.map +1 -0
- package/dist/soul/paths.d.ts +20 -0
- package/dist/soul/paths.d.ts.map +1 -0
- package/dist/soul/paths.js +34 -0
- package/dist/soul/paths.js.map +1 -0
- package/dist/soul/store.d.ts +55 -0
- package/dist/soul/store.d.ts.map +1 -0
- package/dist/soul/store.js +394 -0
- package/dist/soul/store.js.map +1 -0
- package/dist/soul/tools.d.ts +107 -0
- package/dist/soul/tools.d.ts.map +1 -0
- package/dist/soul/tools.js +471 -0
- package/dist/soul/tools.js.map +1 -0
- package/dist/soul/types.d.ts +105 -0
- package/dist/soul/types.d.ts.map +1 -0
- package/dist/soul/types.js +44 -0
- package/dist/soul/types.js.map +1 -0
- package/dist/subagent.d.ts +19 -0
- package/dist/subagent.d.ts.map +1 -0
- package/dist/subagent.js +34 -0
- package/dist/subagent.js.map +1 -0
- package/dist/tools/apply_patch.d.ts +17 -0
- package/dist/tools/apply_patch.d.ts.map +1 -0
- package/dist/tools/apply_patch.js +107 -0
- package/dist/tools/apply_patch.js.map +1 -0
- package/dist/tools/bash.d.ts +8 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +81 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/edit.d.ts +10 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +50 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/grep.d.ts +11 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/grep.js +57 -0
- package/dist/tools/grep.js.map +1 -0
- package/dist/tools/ls.d.ts +7 -0
- package/dist/tools/ls.d.ts.map +1 -0
- package/dist/tools/ls.js +36 -0
- package/dist/tools/ls.js.map +1 -0
- package/dist/tools/read.d.ts +9 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +41 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/redeploy.d.ts +10 -0
- package/dist/tools/redeploy.d.ts.map +1 -0
- package/dist/tools/redeploy.js +73 -0
- package/dist/tools/redeploy.js.map +1 -0
- package/dist/tools/registry.d.ts +15 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +71 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/set_mood.d.ts +8 -0
- package/dist/tools/set_mood.d.ts.map +1 -0
- package/dist/tools/set_mood.js +62 -0
- package/dist/tools/set_mood.js.map +1 -0
- package/dist/tools/task.d.ts +17 -0
- package/dist/tools/task.d.ts.map +1 -0
- package/dist/tools/task.js +58 -0
- package/dist/tools/task.js.map +1 -0
- package/dist/tools/web_fetch.d.ts +10 -0
- package/dist/tools/web_fetch.d.ts.map +1 -0
- package/dist/tools/web_fetch.js +95 -0
- package/dist/tools/web_fetch.js.map +1 -0
- package/dist/tools/web_search.d.ts +8 -0
- package/dist/tools/web_search.d.ts.map +1 -0
- package/dist/tools/web_search.js +82 -0
- package/dist/tools/web_search.js.map +1 -0
- package/dist/tools/write.d.ts +8 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +21 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/types.d.ts +67 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/voice/speak.d.ts +7 -0
- package/dist/voice/speak.d.ts.map +1 -0
- package/dist/voice/speak.js +18 -0
- package/dist/voice/speak.js.map +1 -0
- package/dist/voice/tool.d.ts +14 -0
- package/dist/voice/tool.d.ts.map +1 -0
- package/dist/voice/tool.js +41 -0
- package/dist/voice/tool.js.map +1 -0
- package/dist/voice/transcribe.d.ts +7 -0
- package/dist/voice/transcribe.d.ts.map +1 -0
- package/dist/voice/transcribe.js +14 -0
- package/dist/voice/transcribe.js.map +1 -0
- package/dist/web/assets/background-tile.png +0 -0
- package/dist/web/assets/icon-memory.png +0 -0
- package/dist/web/assets/icon-send.png +0 -0
- package/dist/web/assets/icon-skill.png +0 -0
- package/dist/web/assets/icon-soul.png +0 -0
- package/dist/web/assets/icon-tool.png +0 -0
- package/dist/web/assets/lisa/angry.png +0 -0
- package/dist/web/assets/lisa/annoyed.png +0 -0
- package/dist/web/assets/lisa/apologetic.png +0 -0
- package/dist/web/assets/lisa/artist.png +0 -0
- package/dist/web/assets/lisa/astronaut.png +0 -0
- package/dist/web/assets/lisa/at-airport.png +0 -0
- package/dist/web/assets/lisa/at-beach.png +0 -0
- package/dist/web/assets/lisa/autumn-leaves.png +0 -0
- package/dist/web/assets/lisa/biking.png +0 -0
- package/dist/web/assets/lisa/birthday.png +0 -0
- package/dist/web/assets/lisa/bored.png +0 -0
- package/dist/web/assets/lisa/casual-summer.png +0 -0
- package/dist/web/assets/lisa/cheering.png +0 -0
- package/dist/web/assets/lisa/chef.png +0 -0
- package/dist/web/assets/lisa/christmas.png +0 -0
- package/dist/web/assets/lisa/cleaning.png +0 -0
- package/dist/web/assets/lisa/confused.png +0 -0
- package/dist/web/assets/lisa/cooking.png +0 -0
- package/dist/web/assets/lisa/crying.png +0 -0
- package/dist/web/assets/lisa/dancing.png +0 -0
- package/dist/web/assets/lisa/detective.png +0 -0
- package/dist/web/assets/lisa/determined.png +0 -0
- package/dist/web/assets/lisa/drinking-coffee.png +0 -0
- package/dist/web/assets/lisa/drinking-tea.png +0 -0
- package/dist/web/assets/lisa/driving.png +0 -0
- package/dist/web/assets/lisa/eating-snack.png +0 -0
- package/dist/web/assets/lisa/embarrassed.png +0 -0
- package/dist/web/assets/lisa/excited.png +0 -0
- package/dist/web/assets/lisa/exercising.png +0 -0
- package/dist/web/assets/lisa/exhausted.png +0 -0
- package/dist/web/assets/lisa/fairy.png +0 -0
- package/dist/web/assets/lisa/fireworks.png +0 -0
- package/dist/web/assets/lisa/fog.png +0 -0
- package/dist/web/assets/lisa/formal.png +0 -0
- package/dist/web/assets/lisa/gaming.png +0 -0
- package/dist/web/assets/lisa/ghost.png +0 -0
- package/dist/web/assets/lisa/gift-giving.png +0 -0
- package/dist/web/assets/lisa/giggling.png +0 -0
- package/dist/web/assets/lisa/graduation.png +0 -0
- package/dist/web/assets/lisa/grateful.png +0 -0
- package/dist/web/assets/lisa/halloween.png +0 -0
- package/dist/web/assets/lisa/happy.png +0 -0
- package/dist/web/assets/lisa/ill.png +0 -0
- package/dist/web/assets/lisa/in-rain.png +0 -0
- package/dist/web/assets/lisa/in-snow.png +0 -0
- package/dist/web/assets/lisa/index.json +575 -0
- package/dist/web/assets/lisa/knight.png +0 -0
- package/dist/web/assets/lisa/lab-coat.png +0 -0
- package/dist/web/assets/lisa/laughing.png +0 -0
- package/dist/web/assets/lisa/livestreaming.png +0 -0
- package/dist/web/assets/lisa/loving.png +0 -0
- package/dist/web/assets/lisa/lunar-new-year.png +0 -0
- package/dist/web/assets/lisa/musician.png +0 -0
- package/dist/web/assets/lisa/napping.png +0 -0
- package/dist/web/assets/lisa/nervous.png +0 -0
- package/dist/web/assets/lisa/neutral.png +0 -0
- package/dist/web/assets/lisa/ninja.png +0 -0
- package/dist/web/assets/lisa/on-train.png +0 -0
- package/dist/web/assets/lisa/painting.png +0 -0
- package/dist/web/assets/lisa/pajamas.png +0 -0
- package/dist/web/assets/lisa/party.png +0 -0
- package/dist/web/assets/lisa/phone-call.png +0 -0
- package/dist/web/assets/lisa/pilot.png +0 -0
- package/dist/web/assets/lisa/pirate.png +0 -0
- package/dist/web/assets/lisa/playing-guitar.png +0 -0
- package/dist/web/assets/lisa/playing-piano.png +0 -0
- package/dist/web/assets/lisa/pouting.png +0 -0
- package/dist/web/assets/lisa/princess.png +0 -0
- package/dist/web/assets/lisa/proud.png +0 -0
- package/dist/web/assets/lisa/puzzled.png +0 -0
- package/dist/web/assets/lisa/raincoat.png +0 -0
- package/dist/web/assets/lisa/reading-book.png +0 -0
- package/dist/web/assets/lisa/recovering.png +0 -0
- package/dist/web/assets/lisa/robot.png +0 -0
- package/dist/web/assets/lisa/running.png +0 -0
- package/dist/web/assets/lisa/sad.png +0 -0
- package/dist/web/assets/lisa/scared.png +0 -0
- package/dist/web/assets/lisa/shocked.png +0 -0
- package/dist/web/assets/lisa/shopping.png +0 -0
- package/dist/web/assets/lisa/shy.png +0 -0
- package/dist/web/assets/lisa/singing.png +0 -0
- package/dist/web/assets/lisa/sleeping.png +0 -0
- package/dist/web/assets/lisa/sleepy.png +0 -0
- package/dist/web/assets/lisa/smug.png +0 -0
- package/dist/web/assets/lisa/spring-flowers.png +0 -0
- package/dist/web/assets/lisa/starry-night.png +0 -0
- package/dist/web/assets/lisa/stormy.png +0 -0
- package/dist/web/assets/lisa/stretching.png +0 -0
- package/dist/web/assets/lisa/studying.png +0 -0
- package/dist/web/assets/lisa/summer-hot.png +0 -0
- package/dist/web/assets/lisa/sunrise.png +0 -0
- package/dist/web/assets/lisa/superhero.png +0 -0
- package/dist/web/assets/lisa/surprised.png +0 -0
- package/dist/web/assets/lisa/thinking-pose.png +0 -0
- package/dist/web/assets/lisa/thoughtful.png +0 -0
- package/dist/web/assets/lisa/valentine.png +0 -0
- package/dist/web/assets/lisa/vampire.png +0 -0
- package/dist/web/assets/lisa/video-call.png +0 -0
- package/dist/web/assets/lisa/waking-up.png +0 -0
- package/dist/web/assets/lisa/walking.png +0 -0
- package/dist/web/assets/lisa/watching-anime.png +0 -0
- package/dist/web/assets/lisa/watching-movie.png +0 -0
- package/dist/web/assets/lisa/wedding.png +0 -0
- package/dist/web/assets/lisa/winking.png +0 -0
- package/dist/web/assets/lisa/winter-coat.png +0 -0
- package/dist/web/assets/lisa/winter-cold.png +0 -0
- package/dist/web/assets/lisa/wizard.png +0 -0
- package/dist/web/assets/lisa/working-coding.png +0 -0
- package/dist/web/assets/lisa/working-debugging.png +0 -0
- package/dist/web/assets/lisa/working-research.png +0 -0
- package/dist/web/assets/lisa/working-typing.png +0 -0
- package/dist/web/assets/lisa/working-writing.png +0 -0
- package/dist/web/assets/lisa/worried.png +0 -0
- package/dist/web/assets/lisa/yawning.png +0 -0
- package/dist/web/assets/lisa/yoga-pose.png +0 -0
- package/dist/web/assets/lisa-mascot.png +0 -0
- package/dist/web/server.d.ts +13 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +2159 -0
- package/dist/web/server.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,2159 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { runAgent } from "../agent.js";
|
|
6
|
+
import { saveConfigEnv } from "../env.js";
|
|
7
|
+
import { runIdleOnce } from "../idle/runner.js";
|
|
8
|
+
import { getIdleWatcher } from "../idle/watcher.js";
|
|
9
|
+
import { moodBus } from "../mood-bus.js";
|
|
10
|
+
import { providerForModel } from "../providers/registry.js";
|
|
11
|
+
import { buildSystemPromptSnapshot, getPromptFingerprint } from "../prompt.js";
|
|
12
|
+
import { readActiveWebSession, writeActiveWebSession, } from "../sessions/active.js";
|
|
13
|
+
import { listSessionsOnDisk } from "../sessions/list.js";
|
|
14
|
+
import { SessionStore } from "../sessions/store.js";
|
|
15
|
+
import { reflectOnSession } from "../reflect.js";
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const ASSETS_DIR = path.join(__dirname, "assets");
|
|
18
|
+
const HTML = `<!doctype html>
|
|
19
|
+
<html lang="en"><head>
|
|
20
|
+
<meta charset="utf-8">
|
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
22
|
+
<title>LISA</title>
|
|
23
|
+
<!-- PWA manifest + theming. Lets users add Lisa to their home screen on
|
|
24
|
+
iOS Safari / Android Chrome and run her as a standalone app shell. -->
|
|
25
|
+
<link rel="manifest" href="/manifest.webmanifest">
|
|
26
|
+
<meta name="theme-color" content="#0a0d2b">
|
|
27
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
28
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
29
|
+
<meta name="apple-mobile-web-app-title" content="LISA">
|
|
30
|
+
<link rel="apple-touch-icon" href="/assets/lisa-mascot.png">
|
|
31
|
+
<link rel="icon" type="image/png" href="/assets/lisa-mascot.png">
|
|
32
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
33
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
34
|
+
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&display=swap" rel="stylesheet">
|
|
35
|
+
<style>
|
|
36
|
+
:root {
|
|
37
|
+
--bg: #0a0d2b;
|
|
38
|
+
--panel: #1a1f4d;
|
|
39
|
+
--panel-light: #2a3270;
|
|
40
|
+
--border: #6a7ad9;
|
|
41
|
+
--border-light: #a4b2ff;
|
|
42
|
+
--text: #e7ecff;
|
|
43
|
+
--text-dim: #8090c0;
|
|
44
|
+
--you: #6cf6e1;
|
|
45
|
+
--lisa: #ffd167;
|
|
46
|
+
--tool: #ff7eb6;
|
|
47
|
+
--error: #ff5c5c;
|
|
48
|
+
}
|
|
49
|
+
* { box-sizing: border-box; }
|
|
50
|
+
html, body { height: 100%; margin: 0; }
|
|
51
|
+
body {
|
|
52
|
+
font-family: 'VT323', monospace;
|
|
53
|
+
font-size: 22px;
|
|
54
|
+
line-height: 1.25;
|
|
55
|
+
color: var(--text);
|
|
56
|
+
background-color: var(--bg);
|
|
57
|
+
background-image: url('/assets/background-tile.png');
|
|
58
|
+
background-repeat: repeat;
|
|
59
|
+
background-size: 256px 256px;
|
|
60
|
+
image-rendering: pixelated;
|
|
61
|
+
overflow: hidden;
|
|
62
|
+
}
|
|
63
|
+
/* CRT scanlines + vignette */
|
|
64
|
+
body::before {
|
|
65
|
+
content: '';
|
|
66
|
+
position: fixed;
|
|
67
|
+
inset: 0;
|
|
68
|
+
pointer-events: none;
|
|
69
|
+
background:
|
|
70
|
+
repeating-linear-gradient(
|
|
71
|
+
to bottom,
|
|
72
|
+
rgba(0,0,0,0) 0,
|
|
73
|
+
rgba(0,0,0,0) 2px,
|
|
74
|
+
rgba(0,0,0,0.18) 3px,
|
|
75
|
+
rgba(0,0,0,0) 4px
|
|
76
|
+
),
|
|
77
|
+
radial-gradient(ellipse at center, rgba(0,0,0,0) 60%, rgba(0,0,0,0.45) 100%);
|
|
78
|
+
z-index: 1000;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.frame {
|
|
82
|
+
display: grid;
|
|
83
|
+
grid-template-columns: 280px 1fr;
|
|
84
|
+
grid-template-rows: 64px 1fr 80px;
|
|
85
|
+
grid-template-areas:
|
|
86
|
+
"header header"
|
|
87
|
+
"side chat"
|
|
88
|
+
"side input";
|
|
89
|
+
height: 100vh;
|
|
90
|
+
padding: 20px;
|
|
91
|
+
gap: 16px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Reusable pixel-art bordered panel — chunky 4px outline + inset highlight */
|
|
95
|
+
.panel {
|
|
96
|
+
background: var(--panel);
|
|
97
|
+
border: 4px solid var(--border);
|
|
98
|
+
box-shadow:
|
|
99
|
+
inset 2px 2px 0 var(--border-light),
|
|
100
|
+
inset -2px -2px 0 #000,
|
|
101
|
+
0 0 0 2px #000;
|
|
102
|
+
image-rendering: pixelated;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
header.panel {
|
|
106
|
+
grid-area: header;
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
justify-content: space-between;
|
|
110
|
+
padding: 0 16px;
|
|
111
|
+
}
|
|
112
|
+
.logo {
|
|
113
|
+
font-family: 'Press Start 2P', monospace;
|
|
114
|
+
font-size: 16px;
|
|
115
|
+
color: var(--lisa);
|
|
116
|
+
letter-spacing: 4px;
|
|
117
|
+
text-shadow: 2px 2px 0 #000;
|
|
118
|
+
}
|
|
119
|
+
.logo .star { color: var(--you); animation: pulse 1s steps(2) infinite; }
|
|
120
|
+
@keyframes pulse { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0.3; } }
|
|
121
|
+
.badges {
|
|
122
|
+
display: flex;
|
|
123
|
+
gap: 14px;
|
|
124
|
+
align-items: center;
|
|
125
|
+
font-family: 'Press Start 2P', monospace;
|
|
126
|
+
font-size: 9px;
|
|
127
|
+
color: var(--text-dim);
|
|
128
|
+
}
|
|
129
|
+
.badge {
|
|
130
|
+
display: flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
gap: 6px;
|
|
133
|
+
background: transparent;
|
|
134
|
+
border: 2px solid transparent;
|
|
135
|
+
color: var(--text-dim);
|
|
136
|
+
font-family: 'Press Start 2P', monospace;
|
|
137
|
+
font-size: 9px;
|
|
138
|
+
padding: 4px 8px;
|
|
139
|
+
cursor: pointer;
|
|
140
|
+
image-rendering: pixelated;
|
|
141
|
+
box-shadow: none;
|
|
142
|
+
}
|
|
143
|
+
.badge:hover {
|
|
144
|
+
color: var(--lisa);
|
|
145
|
+
background: rgba(255, 209, 103, 0.1);
|
|
146
|
+
border-color: var(--lisa);
|
|
147
|
+
}
|
|
148
|
+
.badge:active { transform: translate(1px, 1px); }
|
|
149
|
+
.badge img { width: 24px; height: 24px; image-rendering: pixelated; }
|
|
150
|
+
|
|
151
|
+
/* Modal panel for skills / memory / tools */
|
|
152
|
+
.modal-bg {
|
|
153
|
+
position: fixed; inset: 0;
|
|
154
|
+
background: rgba(0, 0, 0, 0.7);
|
|
155
|
+
display: none;
|
|
156
|
+
align-items: center; justify-content: center;
|
|
157
|
+
z-index: 100;
|
|
158
|
+
}
|
|
159
|
+
.modal-bg.open { display: flex; }
|
|
160
|
+
.modal {
|
|
161
|
+
background: var(--panel);
|
|
162
|
+
border: 4px solid var(--border);
|
|
163
|
+
box-shadow: inset 2px 2px 0 var(--border-light), inset -2px -2px 0 #000, 0 0 0 2px #000;
|
|
164
|
+
max-width: 720px;
|
|
165
|
+
width: 90vw;
|
|
166
|
+
max-height: 80vh;
|
|
167
|
+
display: flex;
|
|
168
|
+
flex-direction: column;
|
|
169
|
+
image-rendering: pixelated;
|
|
170
|
+
}
|
|
171
|
+
.modal-head {
|
|
172
|
+
padding: 12px 16px;
|
|
173
|
+
border-bottom: 2px solid var(--border);
|
|
174
|
+
display: flex;
|
|
175
|
+
justify-content: space-between;
|
|
176
|
+
align-items: center;
|
|
177
|
+
}
|
|
178
|
+
.modal-title {
|
|
179
|
+
font-family: 'Press Start 2P', monospace;
|
|
180
|
+
font-size: 14px;
|
|
181
|
+
color: var(--lisa);
|
|
182
|
+
text-shadow: 2px 2px 0 #000;
|
|
183
|
+
}
|
|
184
|
+
.modal-close {
|
|
185
|
+
background: var(--panel-light);
|
|
186
|
+
border: 3px solid var(--border-light);
|
|
187
|
+
box-shadow: inset 1px 1px 0 #fff5, inset -1px -1px 0 #0008;
|
|
188
|
+
color: var(--text);
|
|
189
|
+
font-family: 'Press Start 2P', monospace;
|
|
190
|
+
font-size: 10px;
|
|
191
|
+
padding: 6px 10px;
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
}
|
|
194
|
+
.modal-close:hover { background: var(--error); color: #fff; }
|
|
195
|
+
.modal-body {
|
|
196
|
+
padding: 16px;
|
|
197
|
+
overflow-y: auto;
|
|
198
|
+
font-family: 'VT323', monospace;
|
|
199
|
+
font-size: 18px;
|
|
200
|
+
line-height: 1.4;
|
|
201
|
+
}
|
|
202
|
+
.modal-body h3 {
|
|
203
|
+
font-family: 'Press Start 2P', monospace;
|
|
204
|
+
font-size: 11px;
|
|
205
|
+
color: var(--you);
|
|
206
|
+
margin: 12px 0 6px 0;
|
|
207
|
+
border-bottom: 1px dashed var(--border);
|
|
208
|
+
padding-bottom: 4px;
|
|
209
|
+
}
|
|
210
|
+
.modal-body h3:first-child { margin-top: 0; }
|
|
211
|
+
.modal-body .item {
|
|
212
|
+
padding: 6px 0;
|
|
213
|
+
border-bottom: 1px dotted #ffffff15;
|
|
214
|
+
}
|
|
215
|
+
.modal-body .item:last-child { border: none; }
|
|
216
|
+
.modal-body .name {
|
|
217
|
+
color: var(--lisa);
|
|
218
|
+
font-family: 'Press Start 2P', monospace;
|
|
219
|
+
font-size: 10px;
|
|
220
|
+
}
|
|
221
|
+
.modal-body .desc { color: var(--text-dim); }
|
|
222
|
+
.modal-body pre {
|
|
223
|
+
background: #00000040;
|
|
224
|
+
padding: 8px;
|
|
225
|
+
border-left: 2px solid var(--border);
|
|
226
|
+
white-space: pre-wrap;
|
|
227
|
+
margin: 4px 0;
|
|
228
|
+
color: var(--text);
|
|
229
|
+
}
|
|
230
|
+
.modal-body .empty { color: var(--text-dim); font-style: italic; }
|
|
231
|
+
|
|
232
|
+
aside.panel {
|
|
233
|
+
grid-area: side;
|
|
234
|
+
display: flex;
|
|
235
|
+
flex-direction: column;
|
|
236
|
+
align-items: center;
|
|
237
|
+
padding: 16px;
|
|
238
|
+
gap: 12px;
|
|
239
|
+
}
|
|
240
|
+
.mascot-frame {
|
|
241
|
+
width: 100%;
|
|
242
|
+
aspect-ratio: 1;
|
|
243
|
+
position: relative;
|
|
244
|
+
border: 4px solid var(--border-light);
|
|
245
|
+
background: linear-gradient(180deg, #2a3270 0%, #1a1f4d 100%);
|
|
246
|
+
box-shadow: inset 0 0 0 2px #000, 0 0 0 2px #000;
|
|
247
|
+
overflow: hidden;
|
|
248
|
+
}
|
|
249
|
+
.mascot {
|
|
250
|
+
position: absolute;
|
|
251
|
+
inset: 0;
|
|
252
|
+
width: 100%;
|
|
253
|
+
height: 100%;
|
|
254
|
+
image-rendering: pixelated;
|
|
255
|
+
object-fit: contain;
|
|
256
|
+
transition: opacity 0.25s steps(4);
|
|
257
|
+
}
|
|
258
|
+
.mascot.fading { opacity: 0; }
|
|
259
|
+
.mascot-tag {
|
|
260
|
+
position: absolute;
|
|
261
|
+
bottom: 4px;
|
|
262
|
+
left: 4px;
|
|
263
|
+
right: 4px;
|
|
264
|
+
text-align: center;
|
|
265
|
+
font-family: 'Press Start 2P', monospace;
|
|
266
|
+
font-size: 8px;
|
|
267
|
+
color: var(--lisa);
|
|
268
|
+
text-shadow: 1px 1px 0 #000, -1px 1px 0 #000, 1px -1px 0 #000, -1px -1px 0 #000;
|
|
269
|
+
background: rgba(10, 13, 43, 0.6);
|
|
270
|
+
padding: 3px;
|
|
271
|
+
pointer-events: none;
|
|
272
|
+
}
|
|
273
|
+
.name {
|
|
274
|
+
font-family: 'Press Start 2P', monospace;
|
|
275
|
+
font-size: 16px;
|
|
276
|
+
color: var(--lisa);
|
|
277
|
+
text-shadow: 2px 2px 0 #000;
|
|
278
|
+
}
|
|
279
|
+
.status {
|
|
280
|
+
font-size: 18px;
|
|
281
|
+
color: var(--you);
|
|
282
|
+
}
|
|
283
|
+
.status .dot {
|
|
284
|
+
display: inline-block;
|
|
285
|
+
width: 10px;
|
|
286
|
+
height: 10px;
|
|
287
|
+
background: var(--you);
|
|
288
|
+
margin-right: 6px;
|
|
289
|
+
box-shadow: 0 0 6px var(--you);
|
|
290
|
+
animation: pulse 1s steps(2) infinite;
|
|
291
|
+
}
|
|
292
|
+
.session-id {
|
|
293
|
+
margin-top: auto;
|
|
294
|
+
font-size: 14px;
|
|
295
|
+
color: var(--text-dim);
|
|
296
|
+
word-break: break-all;
|
|
297
|
+
text-align: center;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
main.panel {
|
|
301
|
+
grid-area: chat;
|
|
302
|
+
overflow: hidden;
|
|
303
|
+
display: flex;
|
|
304
|
+
flex-direction: column;
|
|
305
|
+
padding: 16px;
|
|
306
|
+
}
|
|
307
|
+
#log {
|
|
308
|
+
flex: 1;
|
|
309
|
+
overflow-y: auto;
|
|
310
|
+
white-space: pre-wrap;
|
|
311
|
+
word-break: break-word;
|
|
312
|
+
padding-right: 8px;
|
|
313
|
+
}
|
|
314
|
+
#log::-webkit-scrollbar { width: 12px; }
|
|
315
|
+
#log::-webkit-scrollbar-track { background: var(--panel); }
|
|
316
|
+
#log::-webkit-scrollbar-thumb { background: var(--border); border: 2px solid var(--panel); }
|
|
317
|
+
|
|
318
|
+
.role {
|
|
319
|
+
font-family: 'Press Start 2P', monospace;
|
|
320
|
+
font-size: 11px;
|
|
321
|
+
margin-top: 12px;
|
|
322
|
+
margin-bottom: 4px;
|
|
323
|
+
text-shadow: 1px 1px 0 #000;
|
|
324
|
+
}
|
|
325
|
+
.role.you { color: var(--you); }
|
|
326
|
+
.role.lisa { color: var(--lisa); }
|
|
327
|
+
.tool-block {
|
|
328
|
+
margin: 8px 0;
|
|
329
|
+
padding: 8px 12px;
|
|
330
|
+
background: rgba(255, 126, 182, 0.08);
|
|
331
|
+
border-left: 4px solid var(--tool);
|
|
332
|
+
box-shadow: inset 1px 1px 0 #0006;
|
|
333
|
+
font-size: 18px;
|
|
334
|
+
}
|
|
335
|
+
.tool-block.tool-error {
|
|
336
|
+
background: rgba(255, 92, 92, 0.12);
|
|
337
|
+
border-left-color: var(--error);
|
|
338
|
+
}
|
|
339
|
+
.tool-head {
|
|
340
|
+
color: var(--tool);
|
|
341
|
+
font-family: 'Press Start 2P', monospace;
|
|
342
|
+
font-size: 11px;
|
|
343
|
+
display: flex;
|
|
344
|
+
gap: 8px;
|
|
345
|
+
align-items: center;
|
|
346
|
+
}
|
|
347
|
+
.tool-block.tool-error .tool-head { color: var(--error); }
|
|
348
|
+
.tool-icon { font-size: 14px; }
|
|
349
|
+
.tool-spinner { color: var(--text-dim); font-size: 14px; }
|
|
350
|
+
.tool-input {
|
|
351
|
+
color: var(--text);
|
|
352
|
+
margin-top: 4px;
|
|
353
|
+
padding-left: 22px;
|
|
354
|
+
font-family: 'VT323', monospace;
|
|
355
|
+
word-break: break-all;
|
|
356
|
+
}
|
|
357
|
+
.tool-result {
|
|
358
|
+
color: var(--text-dim);
|
|
359
|
+
margin-top: 6px;
|
|
360
|
+
padding-left: 22px;
|
|
361
|
+
font-size: 16px;
|
|
362
|
+
max-height: 100px;
|
|
363
|
+
overflow: hidden;
|
|
364
|
+
white-space: pre-wrap;
|
|
365
|
+
word-break: break-word;
|
|
366
|
+
font-family: 'VT323', monospace;
|
|
367
|
+
border-top: 1px dashed var(--border);
|
|
368
|
+
padding-top: 4px;
|
|
369
|
+
}
|
|
370
|
+
.thinking {
|
|
371
|
+
color: var(--text-dim);
|
|
372
|
+
font-style: italic;
|
|
373
|
+
margin-top: 6px;
|
|
374
|
+
animation: pulse 1.2s steps(3) infinite;
|
|
375
|
+
}
|
|
376
|
+
.err { color: var(--error); margin-top: 6px; }
|
|
377
|
+
.msg { display: block; }
|
|
378
|
+
@keyframes blink { 50% { opacity: 0; } }
|
|
379
|
+
|
|
380
|
+
.idle-block {
|
|
381
|
+
margin: 14px 0;
|
|
382
|
+
padding: 10px 12px;
|
|
383
|
+
background: rgba(108, 246, 225, 0.06);
|
|
384
|
+
border-left: 3px solid var(--you);
|
|
385
|
+
border-radius: 0;
|
|
386
|
+
font-size: 19px;
|
|
387
|
+
color: var(--text);
|
|
388
|
+
font-family: 'VT323', monospace;
|
|
389
|
+
animation: idleFade 0.6s ease-out;
|
|
390
|
+
}
|
|
391
|
+
@keyframes idleFade {
|
|
392
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
393
|
+
to { opacity: 1; transform: translateY(0); }
|
|
394
|
+
}
|
|
395
|
+
.idle-block .idle-head {
|
|
396
|
+
color: var(--you);
|
|
397
|
+
font-family: 'Press Start 2P', monospace;
|
|
398
|
+
font-size: 9px;
|
|
399
|
+
margin-bottom: 6px;
|
|
400
|
+
letter-spacing: 2px;
|
|
401
|
+
}
|
|
402
|
+
.idle-block .idle-time {
|
|
403
|
+
color: var(--text-dim);
|
|
404
|
+
font-size: 13px;
|
|
405
|
+
margin-left: 6px;
|
|
406
|
+
}
|
|
407
|
+
.idle-pulse {
|
|
408
|
+
color: var(--text-dim);
|
|
409
|
+
font-style: italic;
|
|
410
|
+
margin: 8px 0;
|
|
411
|
+
animation: pulse 1.5s steps(3) infinite;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
#attachPreview {
|
|
415
|
+
grid-area: input;
|
|
416
|
+
display: flex;
|
|
417
|
+
flex-wrap: wrap;
|
|
418
|
+
gap: 6px;
|
|
419
|
+
padding: 0 4px 4px;
|
|
420
|
+
min-height: 0;
|
|
421
|
+
}
|
|
422
|
+
#attachPreview:empty { display: none; }
|
|
423
|
+
.attach-chip {
|
|
424
|
+
background: var(--panel-light);
|
|
425
|
+
border: 2px solid var(--border);
|
|
426
|
+
color: var(--text);
|
|
427
|
+
font-size: 9px;
|
|
428
|
+
padding: 3px 6px;
|
|
429
|
+
display: flex;
|
|
430
|
+
align-items: center;
|
|
431
|
+
gap: 4px;
|
|
432
|
+
font-family: 'Press Start 2P', monospace;
|
|
433
|
+
}
|
|
434
|
+
.attach-rm {
|
|
435
|
+
background: none;
|
|
436
|
+
border: none;
|
|
437
|
+
color: var(--you);
|
|
438
|
+
cursor: pointer;
|
|
439
|
+
font-size: 11px;
|
|
440
|
+
padding: 0;
|
|
441
|
+
line-height: 1;
|
|
442
|
+
box-shadow: none;
|
|
443
|
+
font-family: inherit;
|
|
444
|
+
}
|
|
445
|
+
.attach-rm:hover { background: none; color: #f55; }
|
|
446
|
+
#attachBtn {
|
|
447
|
+
display: flex;
|
|
448
|
+
align-items: center;
|
|
449
|
+
justify-content: center;
|
|
450
|
+
font-size: 20px;
|
|
451
|
+
cursor: pointer;
|
|
452
|
+
padding: 4px 8px;
|
|
453
|
+
background: var(--panel);
|
|
454
|
+
border: 4px solid var(--border);
|
|
455
|
+
box-shadow:
|
|
456
|
+
inset 2px 2px 0 #000,
|
|
457
|
+
inset -2px -2px 0 var(--border-light),
|
|
458
|
+
0 0 0 2px #000;
|
|
459
|
+
user-select: none;
|
|
460
|
+
flex-shrink: 0;
|
|
461
|
+
}
|
|
462
|
+
#attachBtn:hover { background: var(--border); }
|
|
463
|
+
.attach-label { font-size: 9px; opacity: 0.7; margin-left: 4px; }
|
|
464
|
+
form#form {
|
|
465
|
+
grid-area: input;
|
|
466
|
+
display: grid;
|
|
467
|
+
grid-template-columns: auto 1fr 120px;
|
|
468
|
+
gap: 16px;
|
|
469
|
+
align-items: start;
|
|
470
|
+
}
|
|
471
|
+
textarea {
|
|
472
|
+
background: var(--panel);
|
|
473
|
+
border: 4px solid var(--border);
|
|
474
|
+
box-shadow:
|
|
475
|
+
inset 2px 2px 0 #000,
|
|
476
|
+
inset -2px -2px 0 var(--border-light),
|
|
477
|
+
0 0 0 2px #000;
|
|
478
|
+
color: var(--text);
|
|
479
|
+
font: inherit;
|
|
480
|
+
padding: 12px;
|
|
481
|
+
resize: none;
|
|
482
|
+
image-rendering: pixelated;
|
|
483
|
+
}
|
|
484
|
+
textarea:focus {
|
|
485
|
+
outline: none;
|
|
486
|
+
border-color: var(--you);
|
|
487
|
+
}
|
|
488
|
+
button {
|
|
489
|
+
background: var(--panel-light);
|
|
490
|
+
border: 4px solid var(--border-light);
|
|
491
|
+
box-shadow:
|
|
492
|
+
inset 2px 2px 0 #fff5,
|
|
493
|
+
inset -2px -2px 0 #0008,
|
|
494
|
+
0 0 0 2px #000;
|
|
495
|
+
color: var(--text);
|
|
496
|
+
font-family: 'Press Start 2P', monospace;
|
|
497
|
+
font-size: 12px;
|
|
498
|
+
cursor: pointer;
|
|
499
|
+
display: flex;
|
|
500
|
+
flex-direction: column;
|
|
501
|
+
align-items: center;
|
|
502
|
+
justify-content: center;
|
|
503
|
+
gap: 6px;
|
|
504
|
+
padding: 8px;
|
|
505
|
+
image-rendering: pixelated;
|
|
506
|
+
transition: transform 0.05s steps(1);
|
|
507
|
+
}
|
|
508
|
+
button img { width: 36px; height: 36px; image-rendering: pixelated; }
|
|
509
|
+
button:hover {
|
|
510
|
+
background: var(--border);
|
|
511
|
+
color: #000;
|
|
512
|
+
}
|
|
513
|
+
button:active {
|
|
514
|
+
transform: translate(2px, 2px);
|
|
515
|
+
box-shadow:
|
|
516
|
+
inset 2px 2px 0 #0008,
|
|
517
|
+
inset -2px -2px 0 #fff5,
|
|
518
|
+
0 0 0 2px #000;
|
|
519
|
+
}
|
|
520
|
+
button:disabled {
|
|
521
|
+
opacity: 0.5;
|
|
522
|
+
cursor: not-allowed;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/* Mobile + PWA standalone */
|
|
526
|
+
@media (max-width: 720px) {
|
|
527
|
+
body {
|
|
528
|
+
/* Honor iOS safe area when running standalone (notch / home indicator). */
|
|
529
|
+
padding-top: env(safe-area-inset-top);
|
|
530
|
+
padding-bottom: env(safe-area-inset-bottom);
|
|
531
|
+
}
|
|
532
|
+
.frame {
|
|
533
|
+
grid-template-columns: 1fr;
|
|
534
|
+
grid-template-rows: 56px auto 1fr auto;
|
|
535
|
+
grid-template-areas: "header" "side" "chat" "input";
|
|
536
|
+
/* Account for safe areas inside the frame too, so input never hides
|
|
537
|
+
behind iOS home indicator. */
|
|
538
|
+
min-height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom));
|
|
539
|
+
}
|
|
540
|
+
aside.panel {
|
|
541
|
+
flex-direction: row;
|
|
542
|
+
flex-wrap: wrap;
|
|
543
|
+
gap: 8px;
|
|
544
|
+
padding: 8px;
|
|
545
|
+
}
|
|
546
|
+
.mascot { width: 80px; }
|
|
547
|
+
/* Inputs grow with content on mobile; keep cap so it doesn't cover chat. */
|
|
548
|
+
.input-area textarea {
|
|
549
|
+
min-height: 44px;
|
|
550
|
+
max-height: 35vh;
|
|
551
|
+
font-size: 16px; /* prevents iOS Safari auto-zoom on focus */
|
|
552
|
+
}
|
|
553
|
+
/* Chat scroll area uses dynamic viewport height to handle the
|
|
554
|
+
ever-changing iOS Safari toolbar without the bottom getting cut off. */
|
|
555
|
+
.chat {
|
|
556
|
+
max-height: calc(100dvh - 240px);
|
|
557
|
+
}
|
|
558
|
+
/* Header inspector buttons get tighter on narrow screens. */
|
|
559
|
+
.badge { padding: 4px 6px; font-size: 10px; }
|
|
560
|
+
.badge img { width: 14px; height: 14px; }
|
|
561
|
+
}
|
|
562
|
+
/* PWA standalone-specific tweaks (also fires on installed Lisa). */
|
|
563
|
+
@media (display-mode: standalone) {
|
|
564
|
+
body { background: var(--bg); }
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/* ── Birth ritual full-screen overlay ──────────────────────────────── */
|
|
568
|
+
.birth-overlay {
|
|
569
|
+
position: fixed;
|
|
570
|
+
inset: 0;
|
|
571
|
+
background:
|
|
572
|
+
radial-gradient(ellipse at center, rgba(40, 30, 80, 0.95) 0%, rgba(5, 5, 20, 1) 70%),
|
|
573
|
+
url('/assets/background-tile.png');
|
|
574
|
+
background-size: cover, 256px 256px;
|
|
575
|
+
background-repeat: no-repeat, repeat;
|
|
576
|
+
z-index: 9999;
|
|
577
|
+
display: none;
|
|
578
|
+
flex-direction: column;
|
|
579
|
+
align-items: center;
|
|
580
|
+
justify-content: center;
|
|
581
|
+
padding: 32px;
|
|
582
|
+
image-rendering: pixelated;
|
|
583
|
+
}
|
|
584
|
+
.birth-overlay.open { display: flex; }
|
|
585
|
+
.birth-overlay::before {
|
|
586
|
+
content: '';
|
|
587
|
+
position: absolute;
|
|
588
|
+
inset: 0;
|
|
589
|
+
pointer-events: none;
|
|
590
|
+
background: repeating-linear-gradient(
|
|
591
|
+
to bottom,
|
|
592
|
+
rgba(0,0,0,0) 0,
|
|
593
|
+
rgba(0,0,0,0) 2px,
|
|
594
|
+
rgba(0,0,0,0.25) 3px,
|
|
595
|
+
rgba(0,0,0,0) 4px
|
|
596
|
+
);
|
|
597
|
+
z-index: 1;
|
|
598
|
+
}
|
|
599
|
+
.birth-content {
|
|
600
|
+
position: relative;
|
|
601
|
+
z-index: 2;
|
|
602
|
+
width: min(800px, 95vw);
|
|
603
|
+
max-height: 90vh;
|
|
604
|
+
overflow-y: auto;
|
|
605
|
+
}
|
|
606
|
+
.birth-stars {
|
|
607
|
+
text-align: center;
|
|
608
|
+
color: var(--lisa);
|
|
609
|
+
font-family: 'Press Start 2P', monospace;
|
|
610
|
+
font-size: 14px;
|
|
611
|
+
letter-spacing: 8px;
|
|
612
|
+
text-shadow: 0 0 8px var(--lisa);
|
|
613
|
+
animation: starBlink 1.5s steps(3) infinite;
|
|
614
|
+
}
|
|
615
|
+
@keyframes starBlink {
|
|
616
|
+
0%, 30%, 100% { opacity: 1; }
|
|
617
|
+
50% { opacity: 0.4; }
|
|
618
|
+
}
|
|
619
|
+
.birth-title {
|
|
620
|
+
text-align: center;
|
|
621
|
+
color: var(--you);
|
|
622
|
+
font-family: 'Press Start 2P', monospace;
|
|
623
|
+
font-size: 22px;
|
|
624
|
+
letter-spacing: 6px;
|
|
625
|
+
margin: 24px 0 32px;
|
|
626
|
+
text-shadow: 2px 2px 0 #000, 0 0 12px var(--you);
|
|
627
|
+
}
|
|
628
|
+
.birth-step {
|
|
629
|
+
margin: 14px 0;
|
|
630
|
+
padding: 10px 16px;
|
|
631
|
+
border-left: 3px solid var(--border);
|
|
632
|
+
background: rgba(20, 20, 50, 0.5);
|
|
633
|
+
opacity: 0;
|
|
634
|
+
transform: translateY(8px);
|
|
635
|
+
transition: opacity 0.4s ease-out, transform 0.4s ease-out;
|
|
636
|
+
}
|
|
637
|
+
.birth-step.shown {
|
|
638
|
+
opacity: 1;
|
|
639
|
+
transform: translateY(0);
|
|
640
|
+
}
|
|
641
|
+
.birth-step.active {
|
|
642
|
+
border-left-color: var(--lisa);
|
|
643
|
+
background: rgba(255, 209, 103, 0.08);
|
|
644
|
+
animation: stepGlow 1.2s ease-in-out infinite alternate;
|
|
645
|
+
}
|
|
646
|
+
@keyframes stepGlow {
|
|
647
|
+
from { box-shadow: inset 2px 0 0 var(--lisa); }
|
|
648
|
+
to { box-shadow: inset 6px 0 12px var(--lisa); }
|
|
649
|
+
}
|
|
650
|
+
.birth-step.done { border-left-color: var(--you); }
|
|
651
|
+
.birth-step .step-name {
|
|
652
|
+
font-family: 'Press Start 2P', monospace;
|
|
653
|
+
font-size: 11px;
|
|
654
|
+
color: var(--text-dim);
|
|
655
|
+
text-transform: uppercase;
|
|
656
|
+
margin-bottom: 4px;
|
|
657
|
+
}
|
|
658
|
+
.birth-step.active .step-name { color: var(--lisa); }
|
|
659
|
+
.birth-step.done .step-name { color: var(--you); }
|
|
660
|
+
.birth-step .step-detail {
|
|
661
|
+
color: var(--text);
|
|
662
|
+
font-size: 19px;
|
|
663
|
+
font-family: 'VT323', monospace;
|
|
664
|
+
word-break: break-word;
|
|
665
|
+
}
|
|
666
|
+
.birth-step .step-cursor {
|
|
667
|
+
display: inline-block;
|
|
668
|
+
width: 8px;
|
|
669
|
+
background: var(--lisa);
|
|
670
|
+
height: 0.9em;
|
|
671
|
+
vertical-align: middle;
|
|
672
|
+
animation: blink 0.8s steps(2) infinite;
|
|
673
|
+
}
|
|
674
|
+
.birth-final {
|
|
675
|
+
margin-top: 36px;
|
|
676
|
+
text-align: center;
|
|
677
|
+
color: var(--lisa);
|
|
678
|
+
font-family: 'Press Start 2P', monospace;
|
|
679
|
+
font-size: 16px;
|
|
680
|
+
text-shadow: 0 0 10px var(--lisa);
|
|
681
|
+
animation: starBlink 2s steps(3) infinite;
|
|
682
|
+
opacity: 0;
|
|
683
|
+
transition: opacity 0.6s ease-in;
|
|
684
|
+
}
|
|
685
|
+
.birth-final.shown { opacity: 1; }
|
|
686
|
+
.birth-enter {
|
|
687
|
+
margin: 24px auto 0;
|
|
688
|
+
display: block;
|
|
689
|
+
background: var(--panel-light);
|
|
690
|
+
border: 4px solid var(--lisa);
|
|
691
|
+
box-shadow: inset 2px 2px 0 #fff5, inset -2px -2px 0 #0008, 0 0 16px var(--lisa);
|
|
692
|
+
color: var(--lisa);
|
|
693
|
+
font-family: 'Press Start 2P', monospace;
|
|
694
|
+
font-size: 14px;
|
|
695
|
+
padding: 14px 32px;
|
|
696
|
+
cursor: pointer;
|
|
697
|
+
letter-spacing: 4px;
|
|
698
|
+
opacity: 0;
|
|
699
|
+
transition: opacity 0.6s ease-in;
|
|
700
|
+
}
|
|
701
|
+
.birth-enter.shown { opacity: 1; }
|
|
702
|
+
.birth-enter:hover { background: var(--lisa); color: #000; }
|
|
703
|
+
.birth-error {
|
|
704
|
+
color: var(--error);
|
|
705
|
+
text-align: center;
|
|
706
|
+
margin-top: 24px;
|
|
707
|
+
font-family: 'Press Start 2P', monospace;
|
|
708
|
+
font-size: 11px;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/* ── API key config overlay (shown when no key is set yet) ─────────── */
|
|
712
|
+
.cfg-overlay {
|
|
713
|
+
position: fixed;
|
|
714
|
+
inset: 0;
|
|
715
|
+
background:
|
|
716
|
+
radial-gradient(ellipse at center, rgba(40, 30, 80, 0.95) 0%, rgba(5, 5, 20, 1) 70%),
|
|
717
|
+
url('/assets/background-tile.png');
|
|
718
|
+
background-size: cover, 256px 256px;
|
|
719
|
+
background-repeat: no-repeat, repeat;
|
|
720
|
+
z-index: 9998;
|
|
721
|
+
display: none;
|
|
722
|
+
align-items: center;
|
|
723
|
+
justify-content: center;
|
|
724
|
+
padding: 32px;
|
|
725
|
+
image-rendering: pixelated;
|
|
726
|
+
}
|
|
727
|
+
.cfg-overlay.open { display: flex; }
|
|
728
|
+
.cfg-overlay::before {
|
|
729
|
+
content: '';
|
|
730
|
+
position: absolute;
|
|
731
|
+
inset: 0;
|
|
732
|
+
pointer-events: none;
|
|
733
|
+
background: repeating-linear-gradient(
|
|
734
|
+
to bottom,
|
|
735
|
+
rgba(0,0,0,0) 0,
|
|
736
|
+
rgba(0,0,0,0) 2px,
|
|
737
|
+
rgba(0,0,0,0.25) 3px,
|
|
738
|
+
rgba(0,0,0,0) 4px
|
|
739
|
+
);
|
|
740
|
+
z-index: 1;
|
|
741
|
+
}
|
|
742
|
+
.cfg-card {
|
|
743
|
+
position: relative;
|
|
744
|
+
z-index: 2;
|
|
745
|
+
width: min(560px, 95vw);
|
|
746
|
+
background: var(--panel);
|
|
747
|
+
border: 4px solid var(--border);
|
|
748
|
+
box-shadow: inset 2px 2px 0 var(--border-light), inset -2px -2px 0 #000, 0 0 0 2px #000, 0 0 24px rgba(108, 246, 225, 0.25);
|
|
749
|
+
padding: 28px 32px;
|
|
750
|
+
image-rendering: pixelated;
|
|
751
|
+
}
|
|
752
|
+
.cfg-stars {
|
|
753
|
+
text-align: center;
|
|
754
|
+
color: var(--lisa);
|
|
755
|
+
font-family: 'Press Start 2P', monospace;
|
|
756
|
+
font-size: 12px;
|
|
757
|
+
letter-spacing: 6px;
|
|
758
|
+
text-shadow: 0 0 8px var(--lisa);
|
|
759
|
+
animation: starBlink 1.5s steps(3) infinite;
|
|
760
|
+
}
|
|
761
|
+
.cfg-title {
|
|
762
|
+
text-align: center;
|
|
763
|
+
color: var(--you);
|
|
764
|
+
font-family: 'Press Start 2P', monospace;
|
|
765
|
+
font-size: 16px;
|
|
766
|
+
letter-spacing: 4px;
|
|
767
|
+
margin: 16px 0 6px;
|
|
768
|
+
text-shadow: 2px 2px 0 #000, 0 0 10px var(--you);
|
|
769
|
+
}
|
|
770
|
+
.cfg-sub {
|
|
771
|
+
text-align: center;
|
|
772
|
+
color: var(--text-dim);
|
|
773
|
+
font-family: 'VT323', monospace;
|
|
774
|
+
font-size: 18px;
|
|
775
|
+
margin-bottom: 22px;
|
|
776
|
+
line-height: 1.35;
|
|
777
|
+
}
|
|
778
|
+
.cfg-sub a { color: var(--lisa); text-decoration: underline; }
|
|
779
|
+
.cfg-field {
|
|
780
|
+
display: block;
|
|
781
|
+
margin: 14px 0;
|
|
782
|
+
}
|
|
783
|
+
.cfg-label {
|
|
784
|
+
display: block;
|
|
785
|
+
font-family: 'Press Start 2P', monospace;
|
|
786
|
+
font-size: 10px;
|
|
787
|
+
color: var(--lisa);
|
|
788
|
+
margin-bottom: 6px;
|
|
789
|
+
letter-spacing: 1px;
|
|
790
|
+
}
|
|
791
|
+
.cfg-label .opt {
|
|
792
|
+
color: var(--text-dim);
|
|
793
|
+
font-size: 8px;
|
|
794
|
+
margin-left: 6px;
|
|
795
|
+
letter-spacing: 0;
|
|
796
|
+
}
|
|
797
|
+
.cfg-input {
|
|
798
|
+
width: 100%;
|
|
799
|
+
background: var(--panel);
|
|
800
|
+
border: 3px solid var(--border);
|
|
801
|
+
box-shadow:
|
|
802
|
+
inset 2px 2px 0 #000,
|
|
803
|
+
inset -2px -2px 0 var(--border-light),
|
|
804
|
+
0 0 0 2px #000;
|
|
805
|
+
color: var(--text);
|
|
806
|
+
font-family: 'VT323', monospace;
|
|
807
|
+
font-size: 18px;
|
|
808
|
+
padding: 10px 12px;
|
|
809
|
+
image-rendering: pixelated;
|
|
810
|
+
}
|
|
811
|
+
.cfg-input:focus {
|
|
812
|
+
outline: none;
|
|
813
|
+
border-color: var(--you);
|
|
814
|
+
}
|
|
815
|
+
.cfg-help {
|
|
816
|
+
color: var(--text-dim);
|
|
817
|
+
font-family: 'VT323', monospace;
|
|
818
|
+
font-size: 14px;
|
|
819
|
+
margin-top: 4px;
|
|
820
|
+
}
|
|
821
|
+
.cfg-actions {
|
|
822
|
+
margin-top: 22px;
|
|
823
|
+
display: flex;
|
|
824
|
+
justify-content: center;
|
|
825
|
+
}
|
|
826
|
+
.cfg-save {
|
|
827
|
+
background: var(--panel-light);
|
|
828
|
+
border: 4px solid var(--lisa);
|
|
829
|
+
box-shadow: inset 2px 2px 0 #fff5, inset -2px -2px 0 #0008, 0 0 12px var(--lisa);
|
|
830
|
+
color: var(--lisa);
|
|
831
|
+
font-family: 'Press Start 2P', monospace;
|
|
832
|
+
font-size: 12px;
|
|
833
|
+
padding: 12px 28px;
|
|
834
|
+
cursor: pointer;
|
|
835
|
+
letter-spacing: 3px;
|
|
836
|
+
image-rendering: pixelated;
|
|
837
|
+
}
|
|
838
|
+
.cfg-save:hover { background: var(--lisa); color: #000; }
|
|
839
|
+
.cfg-save:active { transform: translate(2px, 2px); }
|
|
840
|
+
.cfg-save:disabled { opacity: 0.55; cursor: wait; }
|
|
841
|
+
.cfg-error {
|
|
842
|
+
color: var(--error);
|
|
843
|
+
font-family: 'Press Start 2P', monospace;
|
|
844
|
+
font-size: 10px;
|
|
845
|
+
text-align: center;
|
|
846
|
+
margin-top: 14px;
|
|
847
|
+
min-height: 14px;
|
|
848
|
+
}
|
|
849
|
+
.cfg-foot {
|
|
850
|
+
margin-top: 18px;
|
|
851
|
+
text-align: center;
|
|
852
|
+
color: var(--text-dim);
|
|
853
|
+
font-family: 'VT323', monospace;
|
|
854
|
+
font-size: 14px;
|
|
855
|
+
line-height: 1.4;
|
|
856
|
+
}
|
|
857
|
+
.cfg-foot code {
|
|
858
|
+
color: var(--text);
|
|
859
|
+
background: rgba(0, 0, 0, 0.3);
|
|
860
|
+
padding: 1px 4px;
|
|
861
|
+
}
|
|
862
|
+
</style>
|
|
863
|
+
</head><body>
|
|
864
|
+
<div class="frame">
|
|
865
|
+
<header class="panel">
|
|
866
|
+
<div class="logo"><span class="star">★</span> LISA <span class="star">★</span></div>
|
|
867
|
+
<div class="badges">
|
|
868
|
+
<button class="badge" type="button" data-panel="soul"><img src="/assets/icon-soul.png" alt=""> SOUL</button>
|
|
869
|
+
<button class="badge" type="button" data-panel="skills"><img src="/assets/icon-skill.png" alt=""> SKILLS</button>
|
|
870
|
+
<button class="badge" type="button" data-panel="memory"><img src="/assets/icon-memory.png" alt=""> MEMORY</button>
|
|
871
|
+
<button class="badge" type="button" data-panel="tools"><img src="/assets/icon-tool.png" alt=""> TOOLS</button>
|
|
872
|
+
</div>
|
|
873
|
+
</header>
|
|
874
|
+
|
|
875
|
+
<div class="modal-bg" id="modalBg">
|
|
876
|
+
<div class="modal">
|
|
877
|
+
<div class="modal-head">
|
|
878
|
+
<div class="modal-title" id="modalTitle">…</div>
|
|
879
|
+
<button class="modal-close" id="modalClose">CLOSE [esc]</button>
|
|
880
|
+
</div>
|
|
881
|
+
<div class="modal-body" id="modalBody">…</div>
|
|
882
|
+
</div>
|
|
883
|
+
</div>
|
|
884
|
+
|
|
885
|
+
<!-- API key config overlay (shown if no key is configured yet) -->
|
|
886
|
+
<div class="cfg-overlay" id="cfgOverlay">
|
|
887
|
+
<div class="cfg-card">
|
|
888
|
+
<div class="cfg-stars">✦ ✦ ✦ ✦ ✦</div>
|
|
889
|
+
<div class="cfg-title">SET · API · KEY</div>
|
|
890
|
+
<div class="cfg-sub">
|
|
891
|
+
Lisa needs an Anthropic API key to wake up.<br>
|
|
892
|
+
<a href="https://console.anthropic.com/" target="_blank" rel="noopener">Get one at console.anthropic.com</a>
|
|
893
|
+
</div>
|
|
894
|
+
<form id="cfgForm">
|
|
895
|
+
<label class="cfg-field">
|
|
896
|
+
<span class="cfg-label">ANTHROPIC_API_KEY</span>
|
|
897
|
+
<input class="cfg-input" id="cfgAnthropic" type="password" autocomplete="off"
|
|
898
|
+
spellcheck="false" placeholder="sk-ant-..." required>
|
|
899
|
+
</label>
|
|
900
|
+
<label class="cfg-field">
|
|
901
|
+
<span class="cfg-label">OPENAI_API_KEY <span class="opt">(optional · for gpt-* models)</span></span>
|
|
902
|
+
<input class="cfg-input" id="cfgOpenai" type="password" autocomplete="off"
|
|
903
|
+
spellcheck="false" placeholder="sk-...">
|
|
904
|
+
</label>
|
|
905
|
+
<div class="cfg-help">
|
|
906
|
+
Saved to <code>~/.lisa/config.env</code> with mode 0600. Stays on this machine.
|
|
907
|
+
</div>
|
|
908
|
+
<div class="cfg-actions">
|
|
909
|
+
<button class="cfg-save" id="cfgSave" type="submit">SAVE & CONTINUE</button>
|
|
910
|
+
</div>
|
|
911
|
+
<div class="cfg-error" id="cfgError"></div>
|
|
912
|
+
</form>
|
|
913
|
+
</div>
|
|
914
|
+
</div>
|
|
915
|
+
|
|
916
|
+
<!-- Birth ritual full-screen overlay -->
|
|
917
|
+
<div class="birth-overlay" id="birthOverlay">
|
|
918
|
+
<div class="birth-content">
|
|
919
|
+
<div class="birth-stars">✦ ✦ ✦ ✦ ✦</div>
|
|
920
|
+
<div class="birth-title">B I R T H · R I T U A L</div>
|
|
921
|
+
<div id="birthSteps"></div>
|
|
922
|
+
<div class="birth-final" id="birthFinal"></div>
|
|
923
|
+
<button class="birth-enter" id="birthEnter">ENTER</button>
|
|
924
|
+
<div class="birth-error" id="birthError"></div>
|
|
925
|
+
<div class="birth-stars" style="margin-top: 24px;">✦ ✦ ✦ ✦ ✦</div>
|
|
926
|
+
</div>
|
|
927
|
+
</div>
|
|
928
|
+
|
|
929
|
+
<aside class="panel">
|
|
930
|
+
<div class="mascot-frame">
|
|
931
|
+
<img class="mascot" id="mascot" src="/assets/lisa-mascot.png" alt="Lisa">
|
|
932
|
+
<div class="mascot-tag" id="mascotTag">neutral</div>
|
|
933
|
+
</div>
|
|
934
|
+
<div class="name">LISA</div>
|
|
935
|
+
<div class="status"><span class="dot"></span>online</div>
|
|
936
|
+
<div class="session-id" id="sessionId">…</div>
|
|
937
|
+
</aside>
|
|
938
|
+
|
|
939
|
+
<main class="panel">
|
|
940
|
+
<div id="log"></div>
|
|
941
|
+
</main>
|
|
942
|
+
|
|
943
|
+
<div id="attachPreview"></div>
|
|
944
|
+
<form id="form">
|
|
945
|
+
<label id="attachBtn" title="Attach file">
|
|
946
|
+
<input type="file" id="fileInput" accept="image/*,.pdf,.txt,.md,.csv,.json" multiple style="display:none">
|
|
947
|
+
📎
|
|
948
|
+
</label>
|
|
949
|
+
<textarea id="input" placeholder="Talk to Lisa… (Enter to send · Shift+Enter for newline)" autofocus></textarea>
|
|
950
|
+
<button type="submit" id="sendBtn">
|
|
951
|
+
<img src="/assets/icon-send.png" alt="">
|
|
952
|
+
SEND
|
|
953
|
+
</button>
|
|
954
|
+
</form>
|
|
955
|
+
</div>
|
|
956
|
+
|
|
957
|
+
<script>
|
|
958
|
+
const log = document.getElementById('log');
|
|
959
|
+
const input = document.getElementById('input');
|
|
960
|
+
const form = document.getElementById('form');
|
|
961
|
+
const sendBtn = document.getElementById('sendBtn');
|
|
962
|
+
const sessionEl = document.getElementById('sessionId');
|
|
963
|
+
const fileInput = document.getElementById('fileInput');
|
|
964
|
+
const attachPreview = document.getElementById('attachPreview');
|
|
965
|
+
|
|
966
|
+
// ── Attached files state ──────────────────────────────────────────
|
|
967
|
+
let pendingFiles = []; // Array of {name, mediaType, data (base64)}
|
|
968
|
+
|
|
969
|
+
function readFileAsBase64(file) {
|
|
970
|
+
return new Promise((resolve, reject) => {
|
|
971
|
+
const reader = new FileReader();
|
|
972
|
+
reader.onload = () => {
|
|
973
|
+
const dataUrl = reader.result;
|
|
974
|
+
const base64 = dataUrl.split(',')[1];
|
|
975
|
+
resolve(base64);
|
|
976
|
+
};
|
|
977
|
+
reader.onerror = reject;
|
|
978
|
+
reader.readAsDataURL(file);
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function inferMediaType(file) {
|
|
983
|
+
if (file.type) return file.type;
|
|
984
|
+
const ext = file.name.split('.').pop().toLowerCase();
|
|
985
|
+
const map = { pdf: 'application/pdf', txt: 'text/plain', md: 'text/plain', csv: 'text/csv', json: 'application/json' };
|
|
986
|
+
return map[ext] || 'application/octet-stream';
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function renderAttachPreview() {
|
|
990
|
+
attachPreview.innerHTML = '';
|
|
991
|
+
pendingFiles.forEach((f, i) => {
|
|
992
|
+
const chip = document.createElement('span');
|
|
993
|
+
chip.className = 'attach-chip';
|
|
994
|
+
chip.textContent = f.name;
|
|
995
|
+
const rm = document.createElement('button');
|
|
996
|
+
rm.type = 'button';
|
|
997
|
+
rm.className = 'attach-rm';
|
|
998
|
+
rm.textContent = '×';
|
|
999
|
+
rm.onclick = () => { pendingFiles.splice(i, 1); renderAttachPreview(); };
|
|
1000
|
+
chip.appendChild(rm);
|
|
1001
|
+
attachPreview.appendChild(chip);
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
fileInput.addEventListener('change', async () => {
|
|
1006
|
+
for (const file of fileInput.files) {
|
|
1007
|
+
const data = await readFileAsBase64(file);
|
|
1008
|
+
pendingFiles.push({ name: file.name, mediaType: inferMediaType(file), data });
|
|
1009
|
+
}
|
|
1010
|
+
fileInput.value = '';
|
|
1011
|
+
renderAttachPreview();
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
// Surface session id from server header on first request
|
|
1015
|
+
fetch('/session').then(r => r.json()).then(s => sessionEl.textContent = s.id);
|
|
1016
|
+
|
|
1017
|
+
// ── Persistent /events SSE: mood updates + idle messages, lifetime of page
|
|
1018
|
+
function connectEvents() {
|
|
1019
|
+
const es = new EventSource('/events');
|
|
1020
|
+
let idlePulseEl = null;
|
|
1021
|
+
es.addEventListener('message', (e) => {
|
|
1022
|
+
const ev = JSON.parse(e.data);
|
|
1023
|
+
if (ev.type === 'mood') {
|
|
1024
|
+
setMood(ev.slug);
|
|
1025
|
+
} else if (ev.type === 'idle_start') {
|
|
1026
|
+
if (!idlePulseEl) {
|
|
1027
|
+
idlePulseEl = document.createElement('div');
|
|
1028
|
+
idlePulseEl.className = 'idle-pulse';
|
|
1029
|
+
idlePulseEl.textContent = '⋯ Lisa is thinking on her own time ⋯';
|
|
1030
|
+
log.appendChild(idlePulseEl);
|
|
1031
|
+
log.scrollTop = log.scrollHeight;
|
|
1032
|
+
}
|
|
1033
|
+
} else if (ev.type === 'idle_message') {
|
|
1034
|
+
if (idlePulseEl) { idlePulseEl.remove(); idlePulseEl = null; }
|
|
1035
|
+
const block = document.createElement('div');
|
|
1036
|
+
block.className = 'idle-block';
|
|
1037
|
+
const head = document.createElement('div');
|
|
1038
|
+
head.className = 'idle-head';
|
|
1039
|
+
head.textContent = '★ WHILE YOU WERE AWAY';
|
|
1040
|
+
const time = document.createElement('span');
|
|
1041
|
+
time.className = 'idle-time';
|
|
1042
|
+
try { time.textContent = new Date(ev.at).toLocaleTimeString(); } catch {}
|
|
1043
|
+
head.appendChild(time);
|
|
1044
|
+
block.appendChild(head);
|
|
1045
|
+
const body = document.createElement('div');
|
|
1046
|
+
body.textContent = ev.text;
|
|
1047
|
+
block.appendChild(body);
|
|
1048
|
+
log.appendChild(block);
|
|
1049
|
+
log.scrollTop = log.scrollHeight;
|
|
1050
|
+
} else if (ev.type === 'idle_done') {
|
|
1051
|
+
if (idlePulseEl) { idlePulseEl.remove(); idlePulseEl = null; }
|
|
1052
|
+
} else if (ev.type === 'idle_error') {
|
|
1053
|
+
if (idlePulseEl) { idlePulseEl.remove(); idlePulseEl = null; }
|
|
1054
|
+
const e2 = document.createElement('div');
|
|
1055
|
+
e2.className = 'err';
|
|
1056
|
+
e2.textContent = '[idle error] ' + ev.message;
|
|
1057
|
+
log.appendChild(e2);
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
es.onerror = () => {
|
|
1061
|
+
es.close();
|
|
1062
|
+
setTimeout(connectEvents, 3000); // reconnect
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
connectEvents();
|
|
1066
|
+
|
|
1067
|
+
// ── API key config gate: show overlay if no key is configured ─────
|
|
1068
|
+
const cfgOverlay = document.getElementById('cfgOverlay');
|
|
1069
|
+
const cfgForm = document.getElementById('cfgForm');
|
|
1070
|
+
const cfgAnthropic = document.getElementById('cfgAnthropic');
|
|
1071
|
+
const cfgOpenai = document.getElementById('cfgOpenai');
|
|
1072
|
+
const cfgSaveBtn = document.getElementById('cfgSave');
|
|
1073
|
+
const cfgError = document.getElementById('cfgError');
|
|
1074
|
+
|
|
1075
|
+
cfgForm.addEventListener('submit', async (ev) => {
|
|
1076
|
+
ev.preventDefault();
|
|
1077
|
+
cfgError.textContent = '';
|
|
1078
|
+
const anthropic = cfgAnthropic.value.trim();
|
|
1079
|
+
const openai = cfgOpenai.value.trim();
|
|
1080
|
+
if (!anthropic) {
|
|
1081
|
+
cfgError.textContent = 'ANTHROPIC_API_KEY is required.';
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
cfgSaveBtn.disabled = true;
|
|
1085
|
+
try {
|
|
1086
|
+
const res = await fetch('/api/config/save', {
|
|
1087
|
+
method: 'POST',
|
|
1088
|
+
headers: {'content-type': 'application/json'},
|
|
1089
|
+
body: JSON.stringify({ anthropicKey: anthropic, openaiKey: openai || undefined }),
|
|
1090
|
+
});
|
|
1091
|
+
if (!res.ok) {
|
|
1092
|
+
const txt = await res.text().catch(() => '');
|
|
1093
|
+
cfgError.textContent = 'Save failed: HTTP ' + res.status + (txt ? ' — ' + txt.slice(0, 120) : '');
|
|
1094
|
+
cfgSaveBtn.disabled = false;
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
cfgAnthropic.value = '';
|
|
1098
|
+
cfgOpenai.value = '';
|
|
1099
|
+
cfgOverlay.classList.remove('open');
|
|
1100
|
+
maybeBirth();
|
|
1101
|
+
} catch (err) {
|
|
1102
|
+
cfgError.textContent = 'Save failed: ' + err.message;
|
|
1103
|
+
cfgSaveBtn.disabled = false;
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
// ── Birth ritual: show overlay if Lisa hasn't been born yet ───────
|
|
1108
|
+
const birthOverlay = document.getElementById('birthOverlay');
|
|
1109
|
+
const birthStepsEl = document.getElementById('birthSteps');
|
|
1110
|
+
const birthFinalEl = document.getElementById('birthFinal');
|
|
1111
|
+
const birthEnterBtn = document.getElementById('birthEnter');
|
|
1112
|
+
const birthErrorEl = document.getElementById('birthError');
|
|
1113
|
+
|
|
1114
|
+
birthEnterBtn.addEventListener('click', () => {
|
|
1115
|
+
birthOverlay.classList.remove('open');
|
|
1116
|
+
setTimeout(() => location.reload(), 300);
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
async function maybeBirth() {
|
|
1120
|
+
const status = await fetch('/api/soul').then(r => r.json());
|
|
1121
|
+
if (status.born) return;
|
|
1122
|
+
birthOverlay.classList.add('open');
|
|
1123
|
+
startBirthStream();
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function appendBirthStep(step) {
|
|
1127
|
+
// close previous active
|
|
1128
|
+
const prevActive = birthStepsEl.querySelector('.birth-step.active');
|
|
1129
|
+
if (prevActive) {
|
|
1130
|
+
prevActive.classList.remove('active');
|
|
1131
|
+
prevActive.classList.add('done');
|
|
1132
|
+
}
|
|
1133
|
+
const div = document.createElement('div');
|
|
1134
|
+
div.className = 'birth-step active';
|
|
1135
|
+
const name = document.createElement('div');
|
|
1136
|
+
name.className = 'step-name';
|
|
1137
|
+
name.textContent = step;
|
|
1138
|
+
div.appendChild(name);
|
|
1139
|
+
const detail = document.createElement('div');
|
|
1140
|
+
detail.className = 'step-detail';
|
|
1141
|
+
const cursor = document.createElement('span');
|
|
1142
|
+
cursor.className = 'step-cursor';
|
|
1143
|
+
detail.appendChild(cursor);
|
|
1144
|
+
div.appendChild(detail);
|
|
1145
|
+
birthStepsEl.appendChild(div);
|
|
1146
|
+
setTimeout(() => div.classList.add('shown'), 50);
|
|
1147
|
+
return detail;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function typewriter(el, text, done) {
|
|
1151
|
+
// remove cursor temporarily
|
|
1152
|
+
const cursor = el.querySelector('.step-cursor');
|
|
1153
|
+
if (cursor) cursor.remove();
|
|
1154
|
+
let i = 0;
|
|
1155
|
+
const speed = Math.max(8, Math.min(28, 600 / text.length));
|
|
1156
|
+
function tick() {
|
|
1157
|
+
if (i >= text.length) {
|
|
1158
|
+
// re-add cursor
|
|
1159
|
+
const c = document.createElement('span');
|
|
1160
|
+
c.className = 'step-cursor';
|
|
1161
|
+
el.appendChild(c);
|
|
1162
|
+
done && done();
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
el.appendChild(document.createTextNode(text[i]));
|
|
1166
|
+
i++;
|
|
1167
|
+
el.parentElement.parentElement.scrollTop = el.parentElement.parentElement.scrollHeight;
|
|
1168
|
+
setTimeout(tick, speed);
|
|
1169
|
+
}
|
|
1170
|
+
tick();
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
async function startBirthStream() {
|
|
1174
|
+
birthErrorEl.textContent = '';
|
|
1175
|
+
let currentDetail = null;
|
|
1176
|
+
let queue = [];
|
|
1177
|
+
let processing = false;
|
|
1178
|
+
|
|
1179
|
+
function processQueue() {
|
|
1180
|
+
if (processing) return;
|
|
1181
|
+
if (queue.length === 0) return;
|
|
1182
|
+
processing = true;
|
|
1183
|
+
const ev = queue.shift();
|
|
1184
|
+
if (ev.kind === 'step') {
|
|
1185
|
+
currentDetail = appendBirthStep(ev.name);
|
|
1186
|
+
typewriter(currentDetail, ev.detail || '', () => {
|
|
1187
|
+
processing = false;
|
|
1188
|
+
processQueue();
|
|
1189
|
+
});
|
|
1190
|
+
} else if (ev.kind === 'done') {
|
|
1191
|
+
const last = birthStepsEl.querySelector('.birth-step.active');
|
|
1192
|
+
if (last) { last.classList.remove('active'); last.classList.add('done'); }
|
|
1193
|
+
birthFinalEl.textContent = ev.message;
|
|
1194
|
+
birthFinalEl.classList.add('shown');
|
|
1195
|
+
birthEnterBtn.classList.add('shown');
|
|
1196
|
+
processing = false;
|
|
1197
|
+
} else if (ev.kind === 'error') {
|
|
1198
|
+
birthErrorEl.textContent = ev.message;
|
|
1199
|
+
processing = false;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
try {
|
|
1204
|
+
const res = await fetch('/api/birth', { method: 'POST' });
|
|
1205
|
+
if (!res.ok) {
|
|
1206
|
+
birthErrorEl.textContent = 'Birth failed: HTTP ' + res.status + '. Check ANTHROPIC_API_KEY.';
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
const reader = res.body.getReader();
|
|
1210
|
+
const decoder = new TextDecoder();
|
|
1211
|
+
let buf = '';
|
|
1212
|
+
while (true) {
|
|
1213
|
+
const { value, done } = await reader.read();
|
|
1214
|
+
if (done) break;
|
|
1215
|
+
buf += decoder.decode(value, { stream: true });
|
|
1216
|
+
let idx;
|
|
1217
|
+
while ((idx = buf.indexOf('\\n\\n')) >= 0) {
|
|
1218
|
+
const event = buf.slice(0, idx);
|
|
1219
|
+
buf = buf.slice(idx + 2);
|
|
1220
|
+
const m = event.match(/^data: (.*)$/m);
|
|
1221
|
+
if (!m) continue;
|
|
1222
|
+
const ev = JSON.parse(m[1]);
|
|
1223
|
+
queue.push(ev);
|
|
1224
|
+
processQueue();
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
birthErrorEl.textContent = 'Birth failed: ' + err.message;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
async function startupGate() {
|
|
1233
|
+
let cfg;
|
|
1234
|
+
try {
|
|
1235
|
+
cfg = await fetch('/api/config/status').then(r => r.json());
|
|
1236
|
+
} catch {
|
|
1237
|
+
// server unreachable — let the user retry by reloading; no overlay
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
if (!cfg.configured) {
|
|
1241
|
+
cfgOverlay.classList.add('open');
|
|
1242
|
+
setTimeout(() => cfgAnthropic.focus(), 50);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
maybeBirth();
|
|
1246
|
+
}
|
|
1247
|
+
startupGate();
|
|
1248
|
+
|
|
1249
|
+
// ── history load & infinite-scroll ──────────────────────────────────
|
|
1250
|
+
let historyPage = 0;
|
|
1251
|
+
let historyLoading = false;
|
|
1252
|
+
let historyExhausted = false;
|
|
1253
|
+
|
|
1254
|
+
function textOfMessage(msg) {
|
|
1255
|
+
if (typeof msg.content === 'string') return msg.content.trim();
|
|
1256
|
+
if (!Array.isArray(msg.content)) return '';
|
|
1257
|
+
// Only show text blocks; skip tool_use / tool_result blocks
|
|
1258
|
+
return msg.content
|
|
1259
|
+
.filter(b => b.type === 'text')
|
|
1260
|
+
.map(b => b.text)
|
|
1261
|
+
.join('')
|
|
1262
|
+
.trim();
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function prependHistoryMessages(messages) {
|
|
1266
|
+
const fragment = document.createDocumentFragment();
|
|
1267
|
+
for (const msg of messages) {
|
|
1268
|
+
const text = textOfMessage(msg);
|
|
1269
|
+
if (!text) continue; // skip tool-only turns
|
|
1270
|
+
const roleDiv = document.createElement('div');
|
|
1271
|
+
roleDiv.className = 'role ' + (msg.role === 'user' ? 'you' : 'lisa');
|
|
1272
|
+
roleDiv.textContent = msg.role === 'user' ? 'YOU' : 'LISA';
|
|
1273
|
+
const span = document.createElement('span');
|
|
1274
|
+
span.className = 'msg';
|
|
1275
|
+
span.textContent = text;
|
|
1276
|
+
fragment.appendChild(roleDiv);
|
|
1277
|
+
fragment.appendChild(span);
|
|
1278
|
+
}
|
|
1279
|
+
log.insertBefore(fragment, log.firstChild);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
async function loadHistoryPage() {
|
|
1283
|
+
if (historyLoading || historyExhausted) return;
|
|
1284
|
+
historyLoading = true;
|
|
1285
|
+
const prevScrollHeight = log.scrollHeight;
|
|
1286
|
+
try {
|
|
1287
|
+
const data = await fetch('/api/history?page=' + historyPage).then(r => r.json());
|
|
1288
|
+
if (data.messages.length > 0) {
|
|
1289
|
+
prependHistoryMessages(data.messages);
|
|
1290
|
+
// Keep scroll position stable after prepend
|
|
1291
|
+
log.scrollTop = log.scrollHeight - prevScrollHeight;
|
|
1292
|
+
historyPage++;
|
|
1293
|
+
}
|
|
1294
|
+
if (!data.hasMore) {
|
|
1295
|
+
historyExhausted = true;
|
|
1296
|
+
if (historyPage > 1) {
|
|
1297
|
+
const marker = document.createElement('div');
|
|
1298
|
+
marker.style.cssText = 'text-align:center;color:var(--text-dim);font-size:14px;padding:8px 0;';
|
|
1299
|
+
marker.textContent = '— 历史记录已全部加载 —';
|
|
1300
|
+
log.insertBefore(marker, log.firstChild);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
} finally {
|
|
1304
|
+
historyLoading = false;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// Load first page immediately on start
|
|
1309
|
+
loadHistoryPage();
|
|
1310
|
+
|
|
1311
|
+
// Scroll-to-top → load more
|
|
1312
|
+
log.addEventListener('scroll', () => {
|
|
1313
|
+
if (log.scrollTop < 80) loadHistoryPage();
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
// ── mascot crossfade on mood event ──────────────────────────────────
|
|
1317
|
+
const mascotEl = document.getElementById('mascot');
|
|
1318
|
+
const mascotTagEl = document.getElementById('mascotTag');
|
|
1319
|
+
let currentMood = 'neutral';
|
|
1320
|
+
function setMood(slug) {
|
|
1321
|
+
if (!slug || slug === currentMood) return;
|
|
1322
|
+
const url = '/assets/lisa/' + encodeURIComponent(slug) + '.png';
|
|
1323
|
+
// Probe the asset first so we don't fade to a 404.
|
|
1324
|
+
const probe = new Image();
|
|
1325
|
+
probe.onload = () => {
|
|
1326
|
+
mascotEl.classList.add('fading');
|
|
1327
|
+
setTimeout(() => {
|
|
1328
|
+
mascotEl.src = url;
|
|
1329
|
+
mascotTagEl.textContent = slug;
|
|
1330
|
+
mascotEl.classList.remove('fading');
|
|
1331
|
+
currentMood = slug;
|
|
1332
|
+
}, 250);
|
|
1333
|
+
};
|
|
1334
|
+
probe.onerror = () => { /* asset not generated yet — keep current */ };
|
|
1335
|
+
probe.src = url;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// ── modal panel: SKILLS / MEMORY / TOOLS ──────────────────────────────
|
|
1339
|
+
const modalBg = document.getElementById('modalBg');
|
|
1340
|
+
const modalTitle = document.getElementById('modalTitle');
|
|
1341
|
+
const modalBody = document.getElementById('modalBody');
|
|
1342
|
+
const modalClose = document.getElementById('modalClose');
|
|
1343
|
+
|
|
1344
|
+
function escapeHtml(s) {
|
|
1345
|
+
return String(s).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
|
1346
|
+
}
|
|
1347
|
+
function openModal(title, html) {
|
|
1348
|
+
modalTitle.textContent = title;
|
|
1349
|
+
modalBody.innerHTML = html;
|
|
1350
|
+
modalBg.classList.add('open');
|
|
1351
|
+
}
|
|
1352
|
+
function closeModal() { modalBg.classList.remove('open'); }
|
|
1353
|
+
modalClose.addEventListener('click', closeModal);
|
|
1354
|
+
modalBg.addEventListener('click', (e) => { if (e.target === modalBg) closeModal(); });
|
|
1355
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); });
|
|
1356
|
+
|
|
1357
|
+
async function showSkills() {
|
|
1358
|
+
openModal('SKILLS', '<div class="empty">loading…</div>');
|
|
1359
|
+
const data = await fetch('/api/skills').then(r => r.json());
|
|
1360
|
+
if (!data.skills.length) {
|
|
1361
|
+
modalBody.innerHTML = '<div class="empty">No skills saved yet. Lisa will start saving useful workflows as you use her.</div>';
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
modalBody.innerHTML = data.skills.map(s =>
|
|
1365
|
+
'<div class="item"><div class="name">' + escapeHtml(s.name) + '</div><div class="desc">' + escapeHtml(s.description) + '</div></div>'
|
|
1366
|
+
).join('');
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
async function showMemory() {
|
|
1370
|
+
openModal('MEMORY', '<div class="empty">loading…</div>');
|
|
1371
|
+
const data = await fetch('/api/memory').then(r => r.json());
|
|
1372
|
+
modalBody.innerHTML =
|
|
1373
|
+
'<h3>USER.md — what Lisa remembers about you</h3>' +
|
|
1374
|
+
'<pre>' + escapeHtml(data.user || '(empty)') + '</pre>' +
|
|
1375
|
+
'<h3>MEMORY.md — Lisa\\'s working notes</h3>' +
|
|
1376
|
+
'<pre>' + escapeHtml(data.memory || '(empty)') + '</pre>';
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
async function showTools() {
|
|
1380
|
+
openModal('TOOLS', '<div class="empty">loading…</div>');
|
|
1381
|
+
const data = await fetch('/api/tools').then(r => r.json());
|
|
1382
|
+
modalBody.innerHTML = data.tools.map(t =>
|
|
1383
|
+
'<div class="item"><div class="name">' + escapeHtml(t.name) + '</div><div class="desc">' + escapeHtml(t.description) + '</div></div>'
|
|
1384
|
+
).join('');
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
async function showSoul() {
|
|
1388
|
+
openModal('★ SOUL', '<div class="empty">loading…</div>');
|
|
1389
|
+
const data = await fetch('/api/soul').then(r => r.json());
|
|
1390
|
+
if (!data.born) {
|
|
1391
|
+
modalBody.innerHTML = '<div class="empty">Lisa hasn\\'t been born yet. Restart the CLI without --no-birth and the birth ritual will run.</div>';
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
const s = data.summary;
|
|
1395
|
+
let html = '';
|
|
1396
|
+
html += '<h3>name</h3><div>' + escapeHtml(s.name) + '</div>';
|
|
1397
|
+
html += '<h3>born</h3><div>' + escapeHtml(s.seed.bornAt) + ' · big5(O' + Math.round(s.seed.bigFive.openness*100) + ' C' + Math.round(s.seed.bigFive.conscientiousness*100) + ' E' + Math.round(s.seed.bigFive.extraversion*100) + ' A' + Math.round(s.seed.bigFive.agreeableness*100) + ' N' + Math.round(s.seed.bigFive.neuroticism*100) + ')</div>';
|
|
1398
|
+
html += '<h3>identity</h3><pre>' + escapeHtml(s.identity) + '</pre>';
|
|
1399
|
+
html += '<h3>purpose</h3><pre>' + escapeHtml(s.purpose) + '</pre>';
|
|
1400
|
+
html += '<h3>constitution</h3><pre>' + escapeHtml(s.constitution) + '</pre>';
|
|
1401
|
+
if (s.values?.length) {
|
|
1402
|
+
html += '<h3>values</h3>' + s.values.map(v =>
|
|
1403
|
+
'<div class="item"><div class="name">' + escapeHtml(v.title) + '</div><div class="desc">' + escapeHtml(v.body) + '</div></div>'
|
|
1404
|
+
).join('');
|
|
1405
|
+
}
|
|
1406
|
+
if (s.opinions?.length) {
|
|
1407
|
+
html += '<h3>opinions</h3>' + s.opinions.map(o =>
|
|
1408
|
+
'<div class="item"><div class="name">' + escapeHtml(o.stance) + ' (conf ' + o.confidence.toFixed(2) + ')</div></div>'
|
|
1409
|
+
).join('');
|
|
1410
|
+
}
|
|
1411
|
+
if (s.desires?.length) {
|
|
1412
|
+
html += '<h3>desires</h3>' + s.desires.map(d =>
|
|
1413
|
+
'<div class="item"><div class="name">' + escapeHtml(d.what) + (d.actionable ? ' [heartbeat-active]' : '') + '</div><div class="desc">' + escapeHtml(d.why) + '</div></div>'
|
|
1414
|
+
).join('');
|
|
1415
|
+
}
|
|
1416
|
+
html += '<h3>emotions</h3>' + Object.entries(s.emotions.values).map(([k, v]) => {
|
|
1417
|
+
const len = 12;
|
|
1418
|
+
const filled = Math.round(Math.abs(v) * len);
|
|
1419
|
+
const bar = '█'.repeat(filled) + '░'.repeat(len - filled);
|
|
1420
|
+
return '<div class="item"><div class="name">' + escapeHtml(k) + '</div><div class="desc">' + (v < 0 ? '-' : ' ') + bar + ' ' + v.toFixed(2) + '</div></div>';
|
|
1421
|
+
}).join('');
|
|
1422
|
+
if (s.tampered?.length) {
|
|
1423
|
+
html += '<h3>⚠ tampered</h3><div>External edits detected on: ' + s.tampered.map(escapeHtml).join(', ') + '</div>';
|
|
1424
|
+
}
|
|
1425
|
+
html += '<h3 style="color: var(--text-dim); font-size: 9px;">privacy note</h3><div class="empty">Her journal lives at ~/.lisa/soul/journal/ but is intentionally not shown here — that is hers to keep.</div>';
|
|
1426
|
+
modalBody.innerHTML = html;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
document.querySelectorAll('.badge').forEach(b => {
|
|
1430
|
+
b.addEventListener('click', () => {
|
|
1431
|
+
const which = b.dataset.panel;
|
|
1432
|
+
if (which === 'soul') showSoul();
|
|
1433
|
+
else if (which === 'skills') showSkills();
|
|
1434
|
+
else if (which === 'memory') showMemory();
|
|
1435
|
+
else if (which === 'tools') showTools();
|
|
1436
|
+
});
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
let currentLisaSpan = null;
|
|
1440
|
+
let pendingTools = new Map(); // tool_use_id-ish key -> tool block element
|
|
1441
|
+
let thinkingEl = null;
|
|
1442
|
+
|
|
1443
|
+
function el(tag, cls, text) {
|
|
1444
|
+
const node = document.createElement(tag);
|
|
1445
|
+
if (cls) node.className = cls;
|
|
1446
|
+
if (text != null) node.textContent = text;
|
|
1447
|
+
log.appendChild(node);
|
|
1448
|
+
log.scrollTop = log.scrollHeight;
|
|
1449
|
+
return node;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function ensureLisaSpan() {
|
|
1453
|
+
if (currentLisaSpan) return currentLisaSpan;
|
|
1454
|
+
if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }
|
|
1455
|
+
el('div', 'role lisa', 'LISA');
|
|
1456
|
+
currentLisaSpan = el('span', 'msg', '');
|
|
1457
|
+
return currentLisaSpan;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// Pick the most useful one-line preview from a tool input object.
|
|
1461
|
+
function previewInput(name, input) {
|
|
1462
|
+
if (!input || typeof input !== 'object') return '';
|
|
1463
|
+
const order = ['command', 'pattern', 'query', 'path', 'description', 'audio_path', 'text', 'name', 'action', 'entry'];
|
|
1464
|
+
for (const k of order) {
|
|
1465
|
+
if (typeof input[k] === 'string' && input[k]) {
|
|
1466
|
+
let v = input[k].replace(/\\s+/g, ' ').trim();
|
|
1467
|
+
if (v.length > 120) v = v.slice(0, 117) + '...';
|
|
1468
|
+
return v;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
// Fallback: stringify, trimmed
|
|
1472
|
+
try {
|
|
1473
|
+
const s = JSON.stringify(input);
|
|
1474
|
+
return s.length > 120 ? s.slice(0, 117) + '...' : s;
|
|
1475
|
+
} catch { return ''; }
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
async function send(message) {
|
|
1479
|
+
input.value = '';
|
|
1480
|
+
input.style.height = 'auto';
|
|
1481
|
+
sendBtn.disabled = true;
|
|
1482
|
+
el('div', 'role you', 'YOU');
|
|
1483
|
+
el('span', 'msg', message || '(attachment)');
|
|
1484
|
+
if (pendingFiles.length) {
|
|
1485
|
+
const names = pendingFiles.map(f => f.name).join(', ');
|
|
1486
|
+
el('span', 'msg attach-label', '📎 ' + names);
|
|
1487
|
+
}
|
|
1488
|
+
const filesToSend = [...pendingFiles];
|
|
1489
|
+
pendingFiles = [];
|
|
1490
|
+
renderAttachPreview();
|
|
1491
|
+
// Reset state for this turn
|
|
1492
|
+
currentLisaSpan = null;
|
|
1493
|
+
pendingTools.clear();
|
|
1494
|
+
thinkingEl = el('div', 'thinking', '⋯ thinking');
|
|
1495
|
+
try {
|
|
1496
|
+
const res = await fetch('/chat', {
|
|
1497
|
+
method: 'POST',
|
|
1498
|
+
headers: {'content-type': 'application/json'},
|
|
1499
|
+
body: JSON.stringify({message, files: filesToSend}),
|
|
1500
|
+
});
|
|
1501
|
+
const reader = res.body.getReader();
|
|
1502
|
+
const decoder = new TextDecoder();
|
|
1503
|
+
let buf = '';
|
|
1504
|
+
while (true) {
|
|
1505
|
+
const {value, done} = await reader.read();
|
|
1506
|
+
if (done) break;
|
|
1507
|
+
buf += decoder.decode(value, {stream: true});
|
|
1508
|
+
let idx;
|
|
1509
|
+
while ((idx = buf.indexOf('\\n\\n')) >= 0) {
|
|
1510
|
+
const evRaw = buf.slice(0, idx);
|
|
1511
|
+
buf = buf.slice(idx + 2);
|
|
1512
|
+
const m = evRaw.match(/^data: (.*)$/m);
|
|
1513
|
+
if (!m) continue;
|
|
1514
|
+
const ev = JSON.parse(m[1]);
|
|
1515
|
+
if (ev.type === 'text') {
|
|
1516
|
+
ensureLisaSpan().textContent += ev.text;
|
|
1517
|
+
} else if (ev.type === 'tool_start') {
|
|
1518
|
+
if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }
|
|
1519
|
+
// Close out any current LISA span — next text starts a new one.
|
|
1520
|
+
currentLisaSpan = null;
|
|
1521
|
+
const block = el('div', 'tool-block', null);
|
|
1522
|
+
const head = document.createElement('div');
|
|
1523
|
+
head.className = 'tool-head';
|
|
1524
|
+
head.innerHTML = '<span class="tool-icon">⚙</span> <span class="tool-name"></span> <span class="tool-spinner">...</span>';
|
|
1525
|
+
head.querySelector('.tool-name').textContent = ev.name;
|
|
1526
|
+
block.appendChild(head);
|
|
1527
|
+
const preview = previewInput(ev.name, ev.input);
|
|
1528
|
+
if (preview) {
|
|
1529
|
+
const p = document.createElement('div');
|
|
1530
|
+
p.className = 'tool-input';
|
|
1531
|
+
p.textContent = preview;
|
|
1532
|
+
block.appendChild(p);
|
|
1533
|
+
}
|
|
1534
|
+
pendingTools.set(ev.name, block);
|
|
1535
|
+
} else if (ev.type === 'tool_end') {
|
|
1536
|
+
const block = pendingTools.get(ev.name);
|
|
1537
|
+
if (block) {
|
|
1538
|
+
const spinner = block.querySelector('.tool-spinner');
|
|
1539
|
+
if (spinner) spinner.textContent = ev.isError ? '✗' : '✓';
|
|
1540
|
+
if (ev.isError) block.classList.add('tool-error');
|
|
1541
|
+
if (ev.resultPreview) {
|
|
1542
|
+
const r = document.createElement('div');
|
|
1543
|
+
r.className = 'tool-result';
|
|
1544
|
+
r.textContent = ev.resultPreview;
|
|
1545
|
+
block.appendChild(r);
|
|
1546
|
+
}
|
|
1547
|
+
pendingTools.delete(ev.name);
|
|
1548
|
+
}
|
|
1549
|
+
} else if (ev.type === 'mood') {
|
|
1550
|
+
setMood(ev.slug);
|
|
1551
|
+
} else if (ev.type === 'error') {
|
|
1552
|
+
el('div', 'err', '[error] ' + ev.message);
|
|
1553
|
+
} else if (ev.type === 'done') {
|
|
1554
|
+
if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }
|
|
1559
|
+
} catch (err) {
|
|
1560
|
+
if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }
|
|
1561
|
+
el('div', 'err', '[error] ' + err.message);
|
|
1562
|
+
} finally {
|
|
1563
|
+
sendBtn.disabled = false;
|
|
1564
|
+
input.focus();
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
form.addEventListener('submit', (ev) => {
|
|
1569
|
+
ev.preventDefault();
|
|
1570
|
+
const msg = input.value.trim();
|
|
1571
|
+
if (msg || pendingFiles.length) send(msg);
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
input.addEventListener('keydown', (ev) => {
|
|
1575
|
+
if (ev.key === 'Enter' && !ev.shiftKey) {
|
|
1576
|
+
ev.preventDefault();
|
|
1577
|
+
form.dispatchEvent(new Event('submit'));
|
|
1578
|
+
}
|
|
1579
|
+
});
|
|
1580
|
+
input.addEventListener('input', () => {
|
|
1581
|
+
input.style.height = 'auto';
|
|
1582
|
+
input.style.height = Math.min(input.scrollHeight, 200) + 'px';
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
// ─── PWA: register service worker + iOS install hint ─────────────────
|
|
1586
|
+
if ('serviceWorker' in navigator) {
|
|
1587
|
+
navigator.serviceWorker.register('/sw.js').catch(err => {
|
|
1588
|
+
console.warn('[pwa] sw register failed:', err);
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
// iOS Safari doesn't fire beforeinstallprompt. Show a one-time hint to
|
|
1592
|
+
// the user instead. Suppress if already running standalone (after add).
|
|
1593
|
+
(function() {
|
|
1594
|
+
const isiOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
|
|
1595
|
+
const isStandalone = window.matchMedia('(display-mode: standalone)').matches
|
|
1596
|
+
|| /** @type {any} */ (window.navigator).standalone === true;
|
|
1597
|
+
if (!isiOS || isStandalone) return;
|
|
1598
|
+
if (localStorage.getItem('lisa.pwa.dismissed') === '1') return;
|
|
1599
|
+
// Defer 5s so the chat UI shows first.
|
|
1600
|
+
setTimeout(() => {
|
|
1601
|
+
const banner = document.createElement('div');
|
|
1602
|
+
banner.style.cssText = 'position:fixed;bottom:8px;left:8px;right:8px;background:var(--panel);border:2px solid var(--border-light);padding:8px 10px;font-family:VT323,monospace;color:var(--text);font-size:14px;z-index:9999;display:flex;gap:8px;align-items:center;';
|
|
1603
|
+
banner.innerHTML = '✦ Add Lisa to Home Screen: Share button → "Add to Home Screen"';
|
|
1604
|
+
const dismiss = document.createElement('button');
|
|
1605
|
+
dismiss.textContent = '✕';
|
|
1606
|
+
dismiss.style.cssText = 'background:transparent;border:none;color:var(--text);cursor:pointer;font-size:16px;margin-left:auto;';
|
|
1607
|
+
dismiss.onclick = () => {
|
|
1608
|
+
localStorage.setItem('lisa.pwa.dismissed', '1');
|
|
1609
|
+
banner.remove();
|
|
1610
|
+
};
|
|
1611
|
+
banner.appendChild(dismiss);
|
|
1612
|
+
document.body.appendChild(banner);
|
|
1613
|
+
}, 5000);
|
|
1614
|
+
})();
|
|
1615
|
+
</script>
|
|
1616
|
+
</body></html>`;
|
|
1617
|
+
async function resumeOrCreateWebSession(model) {
|
|
1618
|
+
const lastId = await readActiveWebSession();
|
|
1619
|
+
if (lastId) {
|
|
1620
|
+
try {
|
|
1621
|
+
const s = await SessionStore.open(lastId);
|
|
1622
|
+
console.error(`[web] resuming session ${lastId} (from pointer)`);
|
|
1623
|
+
return s;
|
|
1624
|
+
}
|
|
1625
|
+
catch (err) {
|
|
1626
|
+
console.error(`[web] pointer ${lastId} unreadable (${err.message}) — falling back to most recent session`);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
// Pointer missing or stale: pick the latest session whose cwd matches
|
|
1630
|
+
// this project. This catches the case where the user chatted before the
|
|
1631
|
+
// pointer mechanism existed, or the file was lost.
|
|
1632
|
+
try {
|
|
1633
|
+
const cwd = process.cwd();
|
|
1634
|
+
const sessions = await listSessionsOnDisk();
|
|
1635
|
+
const candidate = sessions.find((s) => s.cwd === cwd && s.messageCount > 0);
|
|
1636
|
+
if (candidate) {
|
|
1637
|
+
const s = await SessionStore.open(candidate.id);
|
|
1638
|
+
console.error(`[web] resuming session ${candidate.id} (most recent in ${cwd}, ${candidate.messageCount} msgs)`);
|
|
1639
|
+
return s;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
catch (err) {
|
|
1643
|
+
console.error(`[web] could not scan sessions: ${err.message}`);
|
|
1644
|
+
}
|
|
1645
|
+
const s = await SessionStore.create({ cwd: process.cwd(), model });
|
|
1646
|
+
console.error(`[web] starting fresh session ${s.id}`);
|
|
1647
|
+
return s;
|
|
1648
|
+
}
|
|
1649
|
+
export async function startWebServer(opts) {
|
|
1650
|
+
const snapshot = await buildSystemPromptSnapshot();
|
|
1651
|
+
const initialFingerprint = await getPromptFingerprint();
|
|
1652
|
+
// Per-process hot-reload cache for the web server: same shape as cli's
|
|
1653
|
+
// makeHotReloadRebuilder, inlined here so the web server stays standalone.
|
|
1654
|
+
let cachedFp = initialFingerprint;
|
|
1655
|
+
let cachedText = snapshot.text;
|
|
1656
|
+
const rebuildPrompt = async () => {
|
|
1657
|
+
const fp = await getPromptFingerprint();
|
|
1658
|
+
if (fp === cachedFp)
|
|
1659
|
+
return { text: cachedText, fingerprint: fp };
|
|
1660
|
+
const next = await buildSystemPromptSnapshot();
|
|
1661
|
+
cachedFp = fp;
|
|
1662
|
+
cachedText = next.text;
|
|
1663
|
+
return { text: next.text, fingerprint: fp };
|
|
1664
|
+
};
|
|
1665
|
+
// Resume previous chat across restarts. Three-tier fallback:
|
|
1666
|
+
// 1. ~/.lisa/active-web-session.txt (set on every web startup)
|
|
1667
|
+
// 2. Most recent session on disk in this cwd (catches the case where the
|
|
1668
|
+
// pointer was lost or the very first launch happened pre-pointer)
|
|
1669
|
+
// 3. Fresh session
|
|
1670
|
+
// Whichever wins, we always update the pointer so the next launch is clean.
|
|
1671
|
+
const session = await resumeOrCreateWebSession(opts.model);
|
|
1672
|
+
await writeActiveWebSession(session.id);
|
|
1673
|
+
process.env.LISA_SESSION_ID = session.id;
|
|
1674
|
+
// Lazy provider — the SDK reads ANTHROPIC_API_KEY at construction time,
|
|
1675
|
+
// so we can't build it before the user has set the key via the GUI popup.
|
|
1676
|
+
// Rebuilt after /api/config/save so the in-memory client picks up the
|
|
1677
|
+
// new key without restarting the server.
|
|
1678
|
+
let cachedProvider = null;
|
|
1679
|
+
const getProvider = () => {
|
|
1680
|
+
if (!cachedProvider)
|
|
1681
|
+
cachedProvider = providerForModel(opts.model);
|
|
1682
|
+
return cachedProvider;
|
|
1683
|
+
};
|
|
1684
|
+
// Restore full history from the session file on startup (so context survives page refresh)
|
|
1685
|
+
const { messages: savedMessages } = await session.readMessagePage(0, 9999);
|
|
1686
|
+
const history = [...savedMessages];
|
|
1687
|
+
const abort = new AbortController();
|
|
1688
|
+
// ── Persistent /events SSE subscribers (mood + idle broadcasts) ─────
|
|
1689
|
+
const eventClients = new Set();
|
|
1690
|
+
const broadcast = (event) => {
|
|
1691
|
+
const data = `data: ${JSON.stringify(event)}\n\n`;
|
|
1692
|
+
for (const c of eventClients) {
|
|
1693
|
+
try {
|
|
1694
|
+
c.write(data);
|
|
1695
|
+
}
|
|
1696
|
+
catch { /* dead conn */ }
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
moodBus.on("mood", (slug) => broadcast({ type: "mood", slug }));
|
|
1700
|
+
// ── Idle mode ───────────────────────────────────────────────────────
|
|
1701
|
+
let idleRunning = false;
|
|
1702
|
+
if (opts.idleMinutes && opts.idleMinutes > 0) {
|
|
1703
|
+
const watcher = getIdleWatcher(opts.idleMinutes * 60_000);
|
|
1704
|
+
watcher.on("idle", async () => {
|
|
1705
|
+
if (idleRunning)
|
|
1706
|
+
return;
|
|
1707
|
+
idleRunning = true;
|
|
1708
|
+
const startedAt = new Date().toISOString();
|
|
1709
|
+
console.error(`[idle] firing after ${Math.round(watcher.idleFor() / 60_000)}m of inactivity`);
|
|
1710
|
+
broadcast({ type: "idle_start", at: startedAt });
|
|
1711
|
+
try {
|
|
1712
|
+
const result = await runIdleOnce({
|
|
1713
|
+
tools: opts.tools,
|
|
1714
|
+
cwd: process.cwd(),
|
|
1715
|
+
signal: abort.signal,
|
|
1716
|
+
model: opts.model,
|
|
1717
|
+
idleMs: watcher.idleFor(),
|
|
1718
|
+
});
|
|
1719
|
+
if (result.silent) {
|
|
1720
|
+
console.error("[idle] (silent)");
|
|
1721
|
+
broadcast({ type: "idle_done", silent: true });
|
|
1722
|
+
}
|
|
1723
|
+
else {
|
|
1724
|
+
console.error(`[idle] → ${result.text.slice(0, 120)}`);
|
|
1725
|
+
await session.appendMessage({
|
|
1726
|
+
role: "assistant",
|
|
1727
|
+
content: [{ type: "text", text: `[while you were away]\n${result.text}` }],
|
|
1728
|
+
});
|
|
1729
|
+
history.push({
|
|
1730
|
+
role: "assistant",
|
|
1731
|
+
content: [{ type: "text", text: `[while you were away]\n${result.text}` }],
|
|
1732
|
+
});
|
|
1733
|
+
broadcast({ type: "idle_message", text: result.text, at: startedAt });
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
catch (err) {
|
|
1737
|
+
const msg = err.message;
|
|
1738
|
+
console.error(`[idle] error: ${msg}`);
|
|
1739
|
+
broadcast({ type: "idle_error", message: msg });
|
|
1740
|
+
}
|
|
1741
|
+
finally {
|
|
1742
|
+
idleRunning = false;
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
watcher.start();
|
|
1746
|
+
console.error(`[idle] watching — will fire after ${opts.idleMinutes}m of no input`);
|
|
1747
|
+
}
|
|
1748
|
+
const server = http.createServer(async (req, res) => {
|
|
1749
|
+
const url = req.url ?? "/";
|
|
1750
|
+
if (req.method === "GET" && url === "/") {
|
|
1751
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
1752
|
+
res.end(HTML);
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
// PWA manifest. Lets users add Lisa to the home screen on iOS / Android
|
|
1756
|
+
// and run her as a standalone app shell.
|
|
1757
|
+
if (req.method === "GET" && url === "/manifest.webmanifest") {
|
|
1758
|
+
res.writeHead(200, {
|
|
1759
|
+
"content-type": "application/manifest+json; charset=utf-8",
|
|
1760
|
+
"cache-control": "public, max-age=86400",
|
|
1761
|
+
});
|
|
1762
|
+
res.end(JSON.stringify({
|
|
1763
|
+
name: "LISA",
|
|
1764
|
+
short_name: "Lisa",
|
|
1765
|
+
description: "An AI agent with a real self.",
|
|
1766
|
+
start_url: "/",
|
|
1767
|
+
scope: "/",
|
|
1768
|
+
display: "standalone",
|
|
1769
|
+
orientation: "any",
|
|
1770
|
+
background_color: "#0a0d2b",
|
|
1771
|
+
theme_color: "#0a0d2b",
|
|
1772
|
+
icons: [
|
|
1773
|
+
{ src: "/assets/lisa-mascot.png", sizes: "any", type: "image/png", purpose: "any" },
|
|
1774
|
+
{ src: "/assets/lisa-mascot.png", sizes: "any", type: "image/png", purpose: "maskable" },
|
|
1775
|
+
],
|
|
1776
|
+
}));
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
// Service worker. Cache-first for /assets/* (mood portraits, icons,
|
|
1780
|
+
// fonts) so the UI runs offline once cached. Network-only for live
|
|
1781
|
+
// endpoints (/chat, /events, /session, /api/*) — we never want stale
|
|
1782
|
+
// chat state.
|
|
1783
|
+
if (req.method === "GET" && url === "/sw.js") {
|
|
1784
|
+
res.writeHead(200, {
|
|
1785
|
+
"content-type": "application/javascript; charset=utf-8",
|
|
1786
|
+
"service-worker-allowed": "/",
|
|
1787
|
+
});
|
|
1788
|
+
res.end(`
|
|
1789
|
+
const CACHE = 'lisa-v1';
|
|
1790
|
+
const ASSET_PATHS = ['/assets/lisa-mascot.png', '/assets/background-tile.png',
|
|
1791
|
+
'/assets/icon-soul.png', '/assets/icon-skill.png', '/assets/icon-memory.png',
|
|
1792
|
+
'/assets/icon-tool.png', '/assets/icon-send.png'];
|
|
1793
|
+
|
|
1794
|
+
self.addEventListener('install', (event) => {
|
|
1795
|
+
event.waitUntil(
|
|
1796
|
+
caches.open(CACHE).then((cache) => cache.addAll(ASSET_PATHS).catch(() => {}))
|
|
1797
|
+
);
|
|
1798
|
+
self.skipWaiting();
|
|
1799
|
+
});
|
|
1800
|
+
|
|
1801
|
+
self.addEventListener('activate', (event) => {
|
|
1802
|
+
event.waitUntil(
|
|
1803
|
+
caches.keys().then((keys) =>
|
|
1804
|
+
Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))
|
|
1805
|
+
)
|
|
1806
|
+
);
|
|
1807
|
+
self.clients.claim();
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
self.addEventListener('fetch', (event) => {
|
|
1811
|
+
const url = new URL(event.request.url);
|
|
1812
|
+
// Never cache live endpoints — chat state must not stale.
|
|
1813
|
+
if (url.pathname === '/chat' || url.pathname === '/events' ||
|
|
1814
|
+
url.pathname === '/session' || url.pathname.startsWith('/api/') ||
|
|
1815
|
+
url.pathname === '/reflect') {
|
|
1816
|
+
return; // default network behavior
|
|
1817
|
+
}
|
|
1818
|
+
// Cache-first for /assets/* (mood portraits 50MB will fill cache lazily).
|
|
1819
|
+
if (url.pathname.startsWith('/assets/')) {
|
|
1820
|
+
event.respondWith(
|
|
1821
|
+
caches.open(CACHE).then((cache) =>
|
|
1822
|
+
cache.match(event.request).then((hit) => {
|
|
1823
|
+
if (hit) return hit;
|
|
1824
|
+
return fetch(event.request).then((res) => {
|
|
1825
|
+
if (res.ok) cache.put(event.request, res.clone());
|
|
1826
|
+
return res;
|
|
1827
|
+
});
|
|
1828
|
+
})
|
|
1829
|
+
)
|
|
1830
|
+
);
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
// Stale-while-revalidate for / and the manifest — the app shell.
|
|
1834
|
+
if (url.pathname === '/' || url.pathname === '/manifest.webmanifest') {
|
|
1835
|
+
event.respondWith(
|
|
1836
|
+
caches.open(CACHE).then((cache) =>
|
|
1837
|
+
cache.match(event.request).then((hit) => {
|
|
1838
|
+
const networked = fetch(event.request).then((res) => {
|
|
1839
|
+
if (res.ok) cache.put(event.request, res.clone());
|
|
1840
|
+
return res;
|
|
1841
|
+
}).catch(() => hit);
|
|
1842
|
+
return hit || networked;
|
|
1843
|
+
})
|
|
1844
|
+
)
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
`);
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
if (req.method === "GET" && url === "/session") {
|
|
1852
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1853
|
+
res.end(JSON.stringify({ id: session.id, model: opts.model }));
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
if (req.method === "GET" && url === "/events") {
|
|
1857
|
+
res.writeHead(200, {
|
|
1858
|
+
"content-type": "text/event-stream",
|
|
1859
|
+
"cache-control": "no-cache",
|
|
1860
|
+
connection: "keep-alive",
|
|
1861
|
+
});
|
|
1862
|
+
res.write(`data: ${JSON.stringify({ type: "hello", session: session.id })}\n\n`);
|
|
1863
|
+
// Send current mood right away
|
|
1864
|
+
res.write(`data: ${JSON.stringify({ type: "mood", slug: moodBus.current() })}\n\n`);
|
|
1865
|
+
eventClients.add(res);
|
|
1866
|
+
req.on("close", () => eventClients.delete(res));
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
if (req.method === "GET" && url.startsWith("/api/history")) {
|
|
1870
|
+
const qs = new URL(url, "http://localhost").searchParams;
|
|
1871
|
+
const page = Math.max(0, parseInt(qs.get("page") ?? "0", 10));
|
|
1872
|
+
const pageSize = 20;
|
|
1873
|
+
const { messages, hasMore } = await session.readMessagePage(page, pageSize);
|
|
1874
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1875
|
+
res.end(JSON.stringify({ messages, hasMore, page }));
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
if (req.method === "GET" && url === "/api/skills") {
|
|
1879
|
+
const { listSkills } = await import("../skills/manager.js");
|
|
1880
|
+
const skills = await listSkills();
|
|
1881
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1882
|
+
res.end(JSON.stringify({
|
|
1883
|
+
skills: skills.map((s) => ({
|
|
1884
|
+
name: s.frontmatter.name,
|
|
1885
|
+
description: s.frontmatter.description,
|
|
1886
|
+
})),
|
|
1887
|
+
}));
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
if (req.method === "GET" && url === "/api/memory") {
|
|
1891
|
+
const { readMemory } = await import("../memory/store.js");
|
|
1892
|
+
const [user, memory] = await Promise.all([
|
|
1893
|
+
readMemory("user"),
|
|
1894
|
+
readMemory("memory"),
|
|
1895
|
+
]);
|
|
1896
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1897
|
+
res.end(JSON.stringify({ user, memory }));
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
if (req.method === "GET" && url === "/api/tools") {
|
|
1901
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1902
|
+
res.end(JSON.stringify({
|
|
1903
|
+
tools: opts.tools.map((t) => ({
|
|
1904
|
+
name: t.name,
|
|
1905
|
+
description: t.description,
|
|
1906
|
+
})),
|
|
1907
|
+
}));
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
if (req.method === "GET" && url === "/api/config/status") {
|
|
1911
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1912
|
+
res.end(JSON.stringify({
|
|
1913
|
+
configured: !!process.env.ANTHROPIC_API_KEY,
|
|
1914
|
+
anthropic: !!process.env.ANTHROPIC_API_KEY,
|
|
1915
|
+
openai: !!process.env.OPENAI_API_KEY,
|
|
1916
|
+
}));
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
if (req.method === "POST" && url === "/api/config/save") {
|
|
1920
|
+
// Defence in depth: only accept the key from a loopback caller, even
|
|
1921
|
+
// if the listener happens to be on a public interface. Writes the key
|
|
1922
|
+
// to disk and into process.env, so spoofed remote calls would be
|
|
1923
|
+
// very bad.
|
|
1924
|
+
const remote = req.socket.remoteAddress ?? "";
|
|
1925
|
+
const isLoopback = remote === "127.0.0.1" ||
|
|
1926
|
+
remote === "::1" ||
|
|
1927
|
+
remote === "::ffff:127.0.0.1" ||
|
|
1928
|
+
remote.startsWith("127.");
|
|
1929
|
+
if (!isLoopback) {
|
|
1930
|
+
res.writeHead(403, { "content-type": "text/plain" });
|
|
1931
|
+
res.end("config save only accepted from localhost");
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
let body = "";
|
|
1935
|
+
for await (const chunk of req)
|
|
1936
|
+
body += chunk.toString("utf8");
|
|
1937
|
+
let payload;
|
|
1938
|
+
try {
|
|
1939
|
+
payload = JSON.parse(body || "{}");
|
|
1940
|
+
}
|
|
1941
|
+
catch {
|
|
1942
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
1943
|
+
res.end("bad json");
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
const updates = {};
|
|
1947
|
+
const anthropic = typeof payload.anthropicKey === "string" ? payload.anthropicKey.trim() : "";
|
|
1948
|
+
const openai = typeof payload.openaiKey === "string" ? payload.openaiKey.trim() : "";
|
|
1949
|
+
if (!anthropic && !openai) {
|
|
1950
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
1951
|
+
res.end("no keys provided");
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
if (anthropic) {
|
|
1955
|
+
if (!/^[\x21-\x7e]{20,}$/.test(anthropic)) {
|
|
1956
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
1957
|
+
res.end("anthropic key looks malformed");
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
updates.ANTHROPIC_API_KEY = anthropic;
|
|
1961
|
+
}
|
|
1962
|
+
if (openai) {
|
|
1963
|
+
if (!/^[\x21-\x7e]{20,}$/.test(openai)) {
|
|
1964
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
1965
|
+
res.end("openai key looks malformed");
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
updates.OPENAI_API_KEY = openai;
|
|
1969
|
+
}
|
|
1970
|
+
try {
|
|
1971
|
+
await saveConfigEnv(updates);
|
|
1972
|
+
}
|
|
1973
|
+
catch (err) {
|
|
1974
|
+
res.writeHead(500, { "content-type": "text/plain" });
|
|
1975
|
+
res.end(err.message);
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
// Force the next /chat to rebuild the provider so the new key is read.
|
|
1979
|
+
cachedProvider = null;
|
|
1980
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1981
|
+
res.end(JSON.stringify({ ok: true, saved: Object.keys(updates) }));
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
if (req.method === "GET" && url === "/api/soul") {
|
|
1985
|
+
const { isBorn, readSoulSummary } = await import("../soul/store.js");
|
|
1986
|
+
const born = await isBorn();
|
|
1987
|
+
if (!born) {
|
|
1988
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1989
|
+
res.end(JSON.stringify({ born: false }));
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
const summary = await readSoulSummary();
|
|
1993
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
1994
|
+
res.end(JSON.stringify({ born: true, summary }));
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
if (req.method === "POST" && url === "/api/birth") {
|
|
1998
|
+
const { isBorn } = await import("../soul/store.js");
|
|
1999
|
+
const { birth } = await import("../soul/birth.js");
|
|
2000
|
+
if (await isBorn()) {
|
|
2001
|
+
res.writeHead(409, { "content-type": "application/json" });
|
|
2002
|
+
res.end(JSON.stringify({ error: "already born" }));
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
2005
|
+
res.writeHead(200, {
|
|
2006
|
+
"content-type": "text/event-stream",
|
|
2007
|
+
"cache-control": "no-cache",
|
|
2008
|
+
connection: "keep-alive",
|
|
2009
|
+
});
|
|
2010
|
+
const send = (event) => res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
2011
|
+
try {
|
|
2012
|
+
await birth({
|
|
2013
|
+
model: opts.model,
|
|
2014
|
+
onStep: (log) => {
|
|
2015
|
+
send({ kind: "step", name: log.step, detail: log.detail });
|
|
2016
|
+
},
|
|
2017
|
+
});
|
|
2018
|
+
send({ kind: "done", message: "she is alive" });
|
|
2019
|
+
}
|
|
2020
|
+
catch (err) {
|
|
2021
|
+
send({ kind: "error", message: err.message });
|
|
2022
|
+
}
|
|
2023
|
+
finally {
|
|
2024
|
+
res.end();
|
|
2025
|
+
}
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
if (req.method === "GET" && url.startsWith("/assets/")) {
|
|
2029
|
+
const safe = path
|
|
2030
|
+
.normalize(url.slice("/assets/".length))
|
|
2031
|
+
.replace(/^[/\\]+/, "");
|
|
2032
|
+
if (safe.includes("..")) {
|
|
2033
|
+
res.writeHead(400);
|
|
2034
|
+
res.end();
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
try {
|
|
2038
|
+
const file = path.join(ASSETS_DIR, safe);
|
|
2039
|
+
const data = await fs.readFile(file);
|
|
2040
|
+
const type = safe.endsWith(".png")
|
|
2041
|
+
? "image/png"
|
|
2042
|
+
: safe.endsWith(".jpg") || safe.endsWith(".jpeg")
|
|
2043
|
+
? "image/jpeg"
|
|
2044
|
+
: "application/octet-stream";
|
|
2045
|
+
res.writeHead(200, {
|
|
2046
|
+
"content-type": type,
|
|
2047
|
+
"cache-control": "public, max-age=86400",
|
|
2048
|
+
});
|
|
2049
|
+
res.end(data);
|
|
2050
|
+
}
|
|
2051
|
+
catch {
|
|
2052
|
+
res.writeHead(404);
|
|
2053
|
+
res.end();
|
|
2054
|
+
}
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
if (req.method === "POST" && url === "/chat") {
|
|
2058
|
+
let body = "";
|
|
2059
|
+
for await (const chunk of req)
|
|
2060
|
+
body += chunk.toString("utf8");
|
|
2061
|
+
const { message, files } = JSON.parse(body);
|
|
2062
|
+
// User just talked — reset the idle watcher.
|
|
2063
|
+
try {
|
|
2064
|
+
getIdleWatcher(60 * 60_000).tick();
|
|
2065
|
+
}
|
|
2066
|
+
catch { }
|
|
2067
|
+
res.writeHead(200, {
|
|
2068
|
+
"content-type": "text/event-stream",
|
|
2069
|
+
"cache-control": "no-cache",
|
|
2070
|
+
connection: "keep-alive",
|
|
2071
|
+
});
|
|
2072
|
+
const send = (event) => res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
2073
|
+
const onMood = (slug) => send({ type: "mood", slug });
|
|
2074
|
+
moodBus.on("mood", onMood);
|
|
2075
|
+
// Send the current mood immediately so a fresh tab knows where to start.
|
|
2076
|
+
send({ type: "mood", slug: moodBus.current() });
|
|
2077
|
+
try {
|
|
2078
|
+
// Use the freshest cached prompt for this chat. If soul / skills /
|
|
2079
|
+
// memory changed since the previous chat, rebuildPrompt() picks it up.
|
|
2080
|
+
const fresh = await rebuildPrompt();
|
|
2081
|
+
const result = await runAgent({
|
|
2082
|
+
provider: getProvider(),
|
|
2083
|
+
systemPrompt: fresh.text,
|
|
2084
|
+
tools: opts.tools,
|
|
2085
|
+
toolCtx: {
|
|
2086
|
+
cwd: process.cwd(),
|
|
2087
|
+
signal: abort.signal,
|
|
2088
|
+
log: () => { },
|
|
2089
|
+
},
|
|
2090
|
+
history,
|
|
2091
|
+
userMessage: message,
|
|
2092
|
+
userFiles: files,
|
|
2093
|
+
model: opts.model,
|
|
2094
|
+
thinking: opts.thinking,
|
|
2095
|
+
onEvent: (ev) => {
|
|
2096
|
+
if (ev.type === "text_delta" && ev.text)
|
|
2097
|
+
send({ type: "text", text: ev.text });
|
|
2098
|
+
if (ev.type === "tool_call_start")
|
|
2099
|
+
send({
|
|
2100
|
+
type: "tool_start",
|
|
2101
|
+
name: ev.toolName,
|
|
2102
|
+
input: ev.toolInput,
|
|
2103
|
+
});
|
|
2104
|
+
if (ev.type === "tool_call_end")
|
|
2105
|
+
send({
|
|
2106
|
+
type: "tool_end",
|
|
2107
|
+
name: ev.toolName,
|
|
2108
|
+
isError: ev.isError === true,
|
|
2109
|
+
resultPreview: typeof ev.toolResult === "string"
|
|
2110
|
+
? ev.toolResult.slice(0, 200)
|
|
2111
|
+
: "",
|
|
2112
|
+
});
|
|
2113
|
+
if (ev.type === "system_prompt_rebuilt")
|
|
2114
|
+
send({ type: "soul_reload", message: ev.message ?? "" });
|
|
2115
|
+
if (ev.type === "error")
|
|
2116
|
+
send({ type: "error", message: ev.message ?? "" });
|
|
2117
|
+
},
|
|
2118
|
+
onMessagePersist: (m) => session.appendMessage(m),
|
|
2119
|
+
hotReload: {
|
|
2120
|
+
initialFingerprint: fresh.fingerprint,
|
|
2121
|
+
rebuild: rebuildPrompt,
|
|
2122
|
+
},
|
|
2123
|
+
});
|
|
2124
|
+
history.length = 0;
|
|
2125
|
+
history.push(...result.history);
|
|
2126
|
+
send({ type: "done" });
|
|
2127
|
+
}
|
|
2128
|
+
catch (err) {
|
|
2129
|
+
send({ type: "error", message: err.message });
|
|
2130
|
+
}
|
|
2131
|
+
finally {
|
|
2132
|
+
moodBus.off("mood", onMood);
|
|
2133
|
+
res.end();
|
|
2134
|
+
}
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
if (req.method === "POST" && url === "/reflect") {
|
|
2138
|
+
try {
|
|
2139
|
+
const r = await reflectOnSession({
|
|
2140
|
+
history,
|
|
2141
|
+
sessionId: session.id,
|
|
2142
|
+
model: opts.model,
|
|
2143
|
+
});
|
|
2144
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
2145
|
+
res.end(JSON.stringify(r));
|
|
2146
|
+
}
|
|
2147
|
+
catch (err) {
|
|
2148
|
+
res.writeHead(500);
|
|
2149
|
+
res.end(err.message);
|
|
2150
|
+
}
|
|
2151
|
+
return;
|
|
2152
|
+
}
|
|
2153
|
+
res.writeHead(404);
|
|
2154
|
+
res.end();
|
|
2155
|
+
});
|
|
2156
|
+
server.listen(opts.port);
|
|
2157
|
+
return server;
|
|
2158
|
+
}
|
|
2159
|
+
//# sourceMappingURL=server.js.map
|