nikcli 0.0.6
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/.turbo/turbo-typecheck.log +1 -0
- package/AGENTS.md +27 -0
- package/Dockerfile +18 -0
- package/README.md +15 -0
- package/bin/nikcli +84 -0
- package/config.json +13 -0
- package/docs/tailscale-mobile/01-tailscale-setup.md +94 -0
- package/docs/tailscale-mobile/02-host-setup.md +115 -0
- package/docs/tailscale-mobile/03-phone-and-serve.md +134 -0
- package/docs/tailscale-mobile/README.md +59 -0
- package/examples/README.md +54 -0
- package/package.json +147 -0
- package/parsers-config.ts +253 -0
- package/script/build.ts +179 -0
- package/script/postinstall.mjs +125 -0
- package/script/publish-registries.ts +187 -0
- package/script/publish.ts +100 -0
- package/script/schema.ts +47 -0
- package/script/seed-e2e.ts +50 -0
- package/sequential-prancing-forest.md +373 -0
- package/src/acp/README.md +164 -0
- package/src/acp/agent.ts +1303 -0
- package/src/acp/session.ts +105 -0
- package/src/acp/types.ts +22 -0
- package/src/agent/agent.ts +528 -0
- package/src/agent/generate.txt +32 -0
- package/src/agent/prompt/compaction.txt +14 -0
- package/src/agent/prompt/explore.txt +18 -0
- package/src/agent/prompt/summary.txt +11 -0
- package/src/agent/prompt/title.txt +44 -0
- package/src/auth/index.ts +73 -0
- package/src/bun/index.ts +119 -0
- package/src/bun/registry.ts +54 -0
- package/src/bus/bus-event.ts +43 -0
- package/src/bus/global.ts +10 -0
- package/src/bus/index.ts +105 -0
- package/src/chatbot/handlers.ts +150 -0
- package/src/chatbot/index.ts +132 -0
- package/src/cli/bootstrap.ts +17 -0
- package/src/cli/cmd/acp.ts +69 -0
- package/src/cli/cmd/ads.ts +377 -0
- package/src/cli/cmd/agent.ts +259 -0
- package/src/cli/cmd/auth.ts +400 -0
- package/src/cli/cmd/chatbot.ts +420 -0
- package/src/cli/cmd/cmd.ts +7 -0
- package/src/cli/cmd/companion.ts +81 -0
- package/src/cli/cmd/connectors.ts +593 -0
- package/src/cli/cmd/debug/agent.ts +166 -0
- package/src/cli/cmd/debug/config.ts +16 -0
- package/src/cli/cmd/debug/file.ts +97 -0
- package/src/cli/cmd/debug/index.ts +48 -0
- package/src/cli/cmd/debug/lsp.ts +52 -0
- package/src/cli/cmd/debug/ripgrep.ts +87 -0
- package/src/cli/cmd/debug/scrap.ts +16 -0
- package/src/cli/cmd/debug/skill.ts +16 -0
- package/src/cli/cmd/debug/snapshot.ts +52 -0
- package/src/cli/cmd/export.ts +88 -0
- package/src/cli/cmd/generate.ts +38 -0
- package/src/cli/cmd/github.ts +412 -0
- package/src/cli/cmd/image-model.ts +128 -0
- package/src/cli/cmd/import.ts +201 -0
- package/src/cli/cmd/lovable.ts +128 -0
- package/src/cli/cmd/mcp.ts +738 -0
- package/src/cli/cmd/mobile.ts +223 -0
- package/src/cli/cmd/models.ts +77 -0
- package/src/cli/cmd/plug.ts +231 -0
- package/src/cli/cmd/pr.ts +104 -0
- package/src/cli/cmd/rag-model.ts +167 -0
- package/src/cli/cmd/remote.ts +416 -0
- package/src/cli/cmd/run.ts +589 -0
- package/src/cli/cmd/serve.ts +51 -0
- package/src/cli/cmd/session.ts +133 -0
- package/src/cli/cmd/speak-model.ts +204 -0
- package/src/cli/cmd/stats.ts +402 -0
- package/src/cli/cmd/tui/app.tsx +841 -0
- package/src/cli/cmd/tui/attach.ts +31 -0
- package/src/cli/cmd/tui/component/border.tsx +75 -0
- package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-command.tsx +172 -0
- package/src/cli/cmd/tui/component/dialog-config.tsx +291 -0
- package/src/cli/cmd/tui/component/dialog-connectors.tsx +440 -0
- package/src/cli/cmd/tui/component/dialog-image-model.tsx +97 -0
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog-model.tsx +234 -0
- package/src/cli/cmd/tui/component/dialog-provider.tsx +260 -0
- package/src/cli/cmd/tui/component/dialog-rag-model.tsx +217 -0
- package/src/cli/cmd/tui/component/dialog-remote.tsx +489 -0
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +170 -0
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-settings/index.tsx +59 -0
- package/src/cli/cmd/tui/component/dialog-settings/prompt.tsx +40 -0
- package/src/cli/cmd/tui/component/dialog-settings/sidebar.tsx +39 -0
- package/src/cli/cmd/tui/component/dialog-settings/spinner.tsx +62 -0
- package/src/cli/cmd/tui/component/dialog-settings/ui.tsx +58 -0
- package/src/cli/cmd/tui/component/dialog-skills.tsx +117 -0
- package/src/cli/cmd/tui/component/dialog-speak-model.tsx +304 -0
- package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
- package/src/cli/cmd/tui/component/dialog-status.tsx +165 -0
- package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
- package/src/cli/cmd/tui/component/dialog-theme-create.tsx +717 -0
- package/src/cli/cmd/tui/component/dialog-theme-list.tsx +52 -0
- package/src/cli/cmd/tui/component/dialog-workspace-list.tsx +350 -0
- package/src/cli/cmd/tui/component/error-component.tsx +91 -0
- package/src/cli/cmd/tui/component/logo.tsx +103 -0
- package/src/cli/cmd/tui/component/plugin-route-missing.tsx +14 -0
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +669 -0
- package/src/cli/cmd/tui/component/prompt/frecency.tsx +89 -0
- package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
- package/src/cli/cmd/tui/component/prompt/index.tsx +2165 -0
- package/src/cli/cmd/tui/component/prompt/stash.tsx +63 -0
- package/src/cli/cmd/tui/component/spinner.tsx +24 -0
- package/src/cli/cmd/tui/component/startup-loading.tsx +63 -0
- package/src/cli/cmd/tui/component/table/markdown-table.tsx +267 -0
- package/src/cli/cmd/tui/component/table-db/db/connections.ts +75 -0
- package/src/cli/cmd/tui/component/table-db/db/db-connection.ts +223 -0
- package/src/cli/cmd/tui/component/table-db/db/db-preview.ts +202 -0
- package/src/cli/cmd/tui/component/table-db/db/factory.ts +77 -0
- package/src/cli/cmd/tui/component/table-db/db/index.ts +9 -0
- package/src/cli/cmd/tui/component/table-db/db/mysql-connection.ts +330 -0
- package/src/cli/cmd/tui/component/table-db/db/postgres-connection.ts +338 -0
- package/src/cli/cmd/tui/component/table-db/db/sqlite-connection.ts +302 -0
- package/src/cli/cmd/tui/component/table-db/db/types.ts +108 -0
- package/src/cli/cmd/tui/component/table-db/table/dbedit-hooks.ts +74 -0
- package/src/cli/cmd/tui/component/table-db/table/index.ts +15 -0
- package/src/cli/cmd/tui/component/table-db/table/table-events.ts +54 -0
- package/src/cli/cmd/tui/component/table-db/table/table-formatters.ts +191 -0
- package/src/cli/cmd/tui/component/table-db/table/table-hooks.ts +105 -0
- package/src/cli/cmd/tui/component/table-db/table/table-keyboard-handler.ts +255 -0
- package/src/cli/cmd/tui/component/table-db/table/table-layout-engine.ts +208 -0
- package/src/cli/cmd/tui/component/table-db/table/table-renderable.ts +486 -0
- package/src/cli/cmd/tui/component/table-db/table/table-selection-manager.ts +136 -0
- package/src/cli/cmd/tui/component/table-db/table/table-state.ts +198 -0
- package/src/cli/cmd/tui/component/table-db/table/types.ts +69 -0
- package/src/cli/cmd/tui/component/table-db/ui/db-visualizer.tsx +71 -0
- package/src/cli/cmd/tui/component/table-db/ui/index.ts +2 -0
- package/src/cli/cmd/tui/component/table-db/ui/table-renderer.ts +607 -0
- package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
- package/src/cli/cmd/tui/component/tips.tsx +195 -0
- package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
- package/src/cli/cmd/tui/context/args.tsx +14 -0
- package/src/cli/cmd/tui/context/directory.ts +13 -0
- package/src/cli/cmd/tui/context/exit.tsx +24 -0
- package/src/cli/cmd/tui/context/helper.tsx +25 -0
- package/src/cli/cmd/tui/context/keybind.tsx +102 -0
- package/src/cli/cmd/tui/context/kv.tsx +52 -0
- package/src/cli/cmd/tui/context/local.tsx +458 -0
- package/src/cli/cmd/tui/context/plugin-keybinds.ts +41 -0
- package/src/cli/cmd/tui/context/prompt.tsx +18 -0
- package/src/cli/cmd/tui/context/route.tsx +54 -0
- package/src/cli/cmd/tui/context/sdk.tsx +128 -0
- package/src/cli/cmd/tui/context/server.tsx +8 -0
- package/src/cli/cmd/tui/context/sync.tsx +510 -0
- package/src/cli/cmd/tui/context/theme/abyss.json +233 -0
- package/src/cli/cmd/tui/context/theme/apple.json +235 -0
- package/src/cli/cmd/tui/context/theme/arctic.json +232 -0
- package/src/cli/cmd/tui/context/theme/aura.json +69 -0
- package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
- package/src/cli/cmd/tui/context/theme/ayuai.json +229 -0
- package/src/cli/cmd/tui/context/theme/blood.json +229 -0
- package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
- package/src/cli/cmd/tui/context/theme/catmoe.json +235 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-latte.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin.json +259 -0
- package/src/cli/cmd/tui/context/theme/charcoal.json +230 -0
- package/src/cli/cmd/tui/context/theme/chromatic.json +235 -0
- package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -0
- package/src/cli/cmd/tui/context/theme/cosmic.json +234 -0
- package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
- package/src/cli/cmd/tui/context/theme/cyber.json +235 -0
- package/src/cli/cmd/tui/context/theme/dawnfox.json +229 -0
- package/src/cli/cmd/tui/context/theme/dimension.json +235 -0
- package/src/cli/cmd/tui/context/theme/dracula-official.json +222 -0
- package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
- package/src/cli/cmd/tui/context/theme/dream.json +235 -0
- package/src/cli/cmd/tui/context/theme/duo.json +235 -0
- package/src/cli/cmd/tui/context/theme/dusk.json +235 -0
- package/src/cli/cmd/tui/context/theme/ebony.json +232 -0
- package/src/cli/cmd/tui/context/theme/equilibrium.json +232 -0
- package/src/cli/cmd/tui/context/theme/ethereal.json +235 -0
- package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
- package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
- package/src/cli/cmd/tui/context/theme/fusion.json +235 -0
- package/src/cli/cmd/tui/context/theme/ghost.json +235 -0
- package/src/cli/cmd/tui/context/theme/github-dark.json +229 -0
- package/src/cli/cmd/tui/context/theme/github-dimmed.json +231 -0
- package/src/cli/cmd/tui/context/theme/github-light.json +229 -0
- package/src/cli/cmd/tui/context/theme/github.json +233 -0
- package/src/cli/cmd/tui/context/theme/glass.json +235 -0
- package/src/cli/cmd/tui/context/theme/gold.json +235 -0
- package/src/cli/cmd/tui/context/theme/gone.json +234 -0
- package/src/cli/cmd/tui/context/theme/greyscale.json +229 -0
- package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
- package/src/cli/cmd/tui/context/theme/hacker.json +229 -0
- package/src/cli/cmd/tui/context/theme/holo.json +235 -0
- package/src/cli/cmd/tui/context/theme/ink.json +235 -0
- package/src/cli/cmd/tui/context/theme/jet.json +233 -0
- package/src/cli/cmd/tui/context/theme/kanagawa.json +227 -0
- package/src/cli/cmd/tui/context/theme/lavender.json +236 -0
- package/src/cli/cmd/tui/context/theme/lightph.json +235 -0
- package/src/cli/cmd/tui/context/theme/lucent-orng.json +237 -0
- package/src/cli/cmd/tui/context/theme/material-ocean.json +230 -0
- package/src/cli/cmd/tui/context/theme/material.json +235 -0
- package/src/cli/cmd/tui/context/theme/matrix.json +227 -0
- package/src/cli/cmd/tui/context/theme/mercury.json +245 -0
- package/src/cli/cmd/tui/context/theme/midnight.json +235 -0
- package/src/cli/cmd/tui/context/theme/modern.json +235 -0
- package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
- package/src/cli/cmd/tui/context/theme/muted.json +229 -0
- package/src/cli/cmd/tui/context/theme/neon.json +229 -0
- package/src/cli/cmd/tui/context/theme/neonfusion.json +235 -0
- package/src/cli/cmd/tui/context/theme/neutral.json +235 -0
- package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
- package/src/cli/cmd/tui/context/theme/nikcli.json +245 -0
- package/src/cli/cmd/tui/context/theme/nord.json +223 -0
- package/src/cli/cmd/tui/context/theme/nordic.json +235 -0
- package/src/cli/cmd/tui/context/theme/nova.json +235 -0
- package/src/cli/cmd/tui/context/theme/obsidian.json +234 -0
- package/src/cli/cmd/tui/context/theme/one-dark.json +231 -0
- package/src/cli/cmd/tui/context/theme/one-pro.json +229 -0
- package/src/cli/cmd/tui/context/theme/onyx.json +233 -0
- package/src/cli/cmd/tui/context/theme/orng.json +249 -0
- package/src/cli/cmd/tui/context/theme/osaka-jade.json +240 -0
- package/src/cli/cmd/tui/context/theme/oxocarbon.json +229 -0
- package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
- package/src/cli/cmd/tui/context/theme/poimandres.json +230 -0
- package/src/cli/cmd/tui/context/theme/prism.json +235 -0
- package/src/cli/cmd/tui/context/theme/radiant.json +235 -0
- package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
- package/src/cli/cmd/tui/context/theme/shadow.json +235 -0
- package/src/cli/cmd/tui/context/theme/silicon.json +235 -0
- package/src/cli/cmd/tui/context/theme/slate.json +233 -0
- package/src/cli/cmd/tui/context/theme/soft.json +235 -0
- package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
- package/src/cli/cmd/tui/context/theme/spectrum.json +235 -0
- package/src/cli/cmd/tui/context/theme/starlight.json +233 -0
- package/src/cli/cmd/tui/context/theme/sunrise.json +235 -0
- package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
- package/src/cli/cmd/tui/context/theme/tech.json +235 -0
- package/src/cli/cmd/tui/context/theme/tokyonight-storm.json +245 -0
- package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
- package/src/cli/cmd/tui/context/theme/vapor.json +235 -0
- package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
- package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
- package/src/cli/cmd/tui/context/theme/vivid.json +232 -0
- package/src/cli/cmd/tui/context/theme/void.json +235 -0
- package/src/cli/cmd/tui/context/theme/vscode.json +235 -0
- package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
- package/src/cli/cmd/tui/context/theme/zinc.json +236 -0
- package/src/cli/cmd/tui/context/theme.tsx +1303 -0
- package/src/cli/cmd/tui/event.ts +48 -0
- package/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx +152 -0
- package/src/cli/cmd/tui/feature-plugins/home/tips.tsx +50 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx +63 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx +62 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx +93 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/lsp.tsx +66 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/mcp.tsx +96 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/todo.tsx +48 -0
- package/src/cli/cmd/tui/feature-plugins/system/plugins.tsx +288 -0
- package/src/cli/cmd/tui/plugin/api.tsx +407 -0
- package/src/cli/cmd/tui/plugin/index.ts +3 -0
- package/src/cli/cmd/tui/plugin/internal.ts +25 -0
- package/src/cli/cmd/tui/plugin/runtime.ts +1048 -0
- package/src/cli/cmd/tui/plugin/slots.tsx +61 -0
- package/src/cli/cmd/tui/routes/home.tsx +153 -0
- package/src/cli/cmd/tui/routes/session/dbedit.tsx +474 -0
- package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +65 -0
- package/src/cli/cmd/tui/routes/session/dialog-message.tsx +110 -0
- package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +105 -0
- package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/cli/cmd/tui/routes/session/footer.tsx +75 -0
- package/src/cli/cmd/tui/routes/session/header.tsx +177 -0
- package/src/cli/cmd/tui/routes/session/index.tsx +2280 -0
- package/src/cli/cmd/tui/routes/session/permission.tsx +540 -0
- package/src/cli/cmd/tui/routes/session/question.tsx +435 -0
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +313 -0
- package/src/cli/cmd/tui/thread.ts +174 -0
- package/src/cli/cmd/tui/ui/dialog-alert.tsx +57 -0
- package/src/cli/cmd/tui/ui/dialog-confirm.tsx +83 -0
- package/src/cli/cmd/tui/ui/dialog-export-options.tsx +204 -0
- package/src/cli/cmd/tui/ui/dialog-help.tsx +38 -0
- package/src/cli/cmd/tui/ui/dialog-prompt.tsx +102 -0
- package/src/cli/cmd/tui/ui/dialog-select.tsx +389 -0
- package/src/cli/cmd/tui/ui/dialog.tsx +180 -0
- package/src/cli/cmd/tui/ui/link.tsx +34 -0
- package/src/cli/cmd/tui/ui/spinner.ts +368 -0
- package/src/cli/cmd/tui/ui/toast.tsx +138 -0
- package/src/cli/cmd/tui/util/clipboard.ts +154 -0
- package/src/cli/cmd/tui/util/editor.ts +32 -0
- package/src/cli/cmd/tui/util/signal.ts +7 -0
- package/src/cli/cmd/tui/util/terminal.ts +114 -0
- package/src/cli/cmd/tui/util/transcript.ts +98 -0
- package/src/cli/cmd/tui/win32.ts +110 -0
- package/src/cli/cmd/tui/worker.ts +156 -0
- package/src/cli/cmd/uninstall.ts +357 -0
- package/src/cli/cmd/upgrade.ts +72 -0
- package/src/cli/cmd/web.ts +87 -0
- package/src/cli/cmd/workspace-serve.ts +16 -0
- package/src/cli/error.ts +57 -0
- package/src/cli/network.ts +55 -0
- package/src/cli/remote/index.ts +36 -0
- package/src/cli/remote/notifications.ts +104 -0
- package/src/cli/remote/qr-renderer.ts +86 -0
- package/src/cli/remote/remote-service.ts +757 -0
- package/src/cli/remote/session-manager.ts +284 -0
- package/src/cli/remote/subagent-hooks.ts +151 -0
- package/src/cli/remote/types.ts +121 -0
- package/src/cli/ui.ts +96 -0
- package/src/cli/upgrade.ts +25 -0
- package/src/command/index.ts +174 -0
- package/src/command/template/initialize.txt +10 -0
- package/src/command/template/review.txt +99 -0
- package/src/config/config.ts +1760 -0
- package/src/config/markdown.ts +88 -0
- package/src/config/migrate-tui-config.ts +155 -0
- package/src/config/paths.ts +174 -0
- package/src/config/tui-schema.ts +36 -0
- package/src/config/tui.ts +209 -0
- package/src/connectors/api/base.ts +75 -0
- package/src/connectors/api/figma.ts +103 -0
- package/src/connectors/api/github.ts +247 -0
- package/src/connectors/api/lovable.ts +126 -0
- package/src/connectors/api/slack.ts +137 -0
- package/src/connectors/auth.ts +68 -0
- package/src/connectors/cache.ts +119 -0
- package/src/connectors/credentials.ts +81 -0
- package/src/connectors/index.ts +202 -0
- package/src/connectors/registry.ts +358 -0
- package/src/docs/context.ts +120 -0
- package/src/docs/library.ts +189 -0
- package/src/env/index.ts +26 -0
- package/src/file/ignore.ts +83 -0
- package/src/file/index.ts +411 -0
- package/src/file/ripgrep.ts +402 -0
- package/src/file/time.ts +65 -0
- package/src/file/watcher.ts +127 -0
- package/src/flag/flag.ts +128 -0
- package/src/format/formatter.ts +356 -0
- package/src/format/index.ts +137 -0
- package/src/global/index.ts +57 -0
- package/src/id/id.ts +83 -0
- package/src/ide/index.ts +76 -0
- package/src/index.ts +184 -0
- package/src/installation/index.ts +246 -0
- package/src/lsp/client.ts +250 -0
- package/src/lsp/index.ts +483 -0
- package/src/lsp/language.ts +119 -0
- package/src/lsp/server.ts +2046 -0
- package/src/mcp/auth.ts +121 -0
- package/src/mcp/index.ts +860 -0
- package/src/mcp/oauth-callback.ts +198 -0
- package/src/mcp/oauth-provider.ts +148 -0
- package/src/mobile/auth.ts +97 -0
- package/src/mobile/github-repo.ts +185 -0
- package/src/patch/index.ts +631 -0
- package/src/permission/arity.ts +150 -0
- package/src/permission/dbedit.ts +236 -0
- package/src/permission/index.ts +210 -0
- package/src/permission/next.ts +287 -0
- package/src/plugin/codex.ts +493 -0
- package/src/plugin/copilot.ts +261 -0
- package/src/plugin/index.ts +714 -0
- package/src/plugin/install.ts +379 -0
- package/src/plugin/meta.ts +165 -0
- package/src/plugin/shared.ts +188 -0
- package/src/project/bootstrap.ts +35 -0
- package/src/project/instance.ts +84 -0
- package/src/project/project.ts +373 -0
- package/src/project/state.ts +66 -0
- package/src/project/vcs.ts +76 -0
- package/src/prompt/stash-store.ts +93 -0
- package/src/provider/auth.ts +147 -0
- package/src/provider/models-macro.ts +22 -0
- package/src/provider/models.ts +216 -0
- package/src/provider/provider.ts +1483 -0
- package/src/provider/sdk/openai-compatible/src/README.md +5 -0
- package/src/provider/sdk/openai-compatible/src/index.ts +2 -0
- package/src/provider/sdk/openai-compatible/src/openai-compatible-provider.ts +100 -0
- package/src/provider/sdk/openai-compatible/src/responses/convert-to-openai-responses-input.ts +303 -0
- package/src/provider/sdk/openai-compatible/src/responses/map-openai-responses-finish-reason.ts +22 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-config.ts +18 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-error.ts +22 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-api-types.ts +207 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +1732 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-prepare-tools.ts +177 -0
- package/src/provider/sdk/openai-compatible/src/responses/openai-responses-settings.ts +1 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/code-interpreter.ts +88 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/file-search.ts +128 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/image-generation.ts +115 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/local-shell.ts +65 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/web-search-preview.ts +104 -0
- package/src/provider/sdk/openai-compatible/src/responses/tool/web-search.ts +103 -0
- package/src/provider/transform.ts +828 -0
- package/src/pty/index.ts +241 -0
- package/src/question/index.ts +171 -0
- package/src/rag/chunk.ts +43 -0
- package/src/rag/embed.ts +179 -0
- package/src/rag/index.ts +376 -0
- package/src/rag/storage.ts +76 -0
- package/src/scheduler/index.ts +61 -0
- package/src/server/error.ts +36 -0
- package/src/server/event.ts +7 -0
- package/src/server/mdns.ts +59 -0
- package/src/server/routes/chatbot.ts +205 -0
- package/src/server/routes/companion.ts +729 -0
- package/src/server/routes/config.ts +92 -0
- package/src/server/routes/connectors.ts +121 -0
- package/src/server/routes/dbedit.ts +76 -0
- package/src/server/routes/experimental.ts +210 -0
- package/src/server/routes/file.ts +197 -0
- package/src/server/routes/global.ts +135 -0
- package/src/server/routes/mcp.ts +225 -0
- package/src/server/routes/mobile.ts +2044 -0
- package/src/server/routes/permission.ts +68 -0
- package/src/server/routes/project.ts +82 -0
- package/src/server/routes/provider.ts +235 -0
- package/src/server/routes/pty.ts +169 -0
- package/src/server/routes/question.ts +98 -0
- package/src/server/routes/session.ts +968 -0
- package/src/server/routes/tui.ts +379 -0
- package/src/server/routes/workspace.ts +104 -0
- package/src/server/server.ts +761 -0
- package/src/server/ssh.ts +207 -0
- package/src/session/auth.ts +402 -0
- package/src/session/compaction.ts +253 -0
- package/src/session/generate.ts +38 -0
- package/src/session/index.ts +598 -0
- package/src/session/llm.ts +273 -0
- package/src/session/message-v2.ts +836 -0
- package/src/session/message.ts +189 -0
- package/src/session/processor.ts +408 -0
- package/src/session/prompt/anthropic-20250930.txt +165 -0
- package/src/session/prompt/anthropic.txt +105 -0
- package/src/session/prompt/anthropic_spoof.txt +1 -0
- package/src/session/prompt/beast.txt +147 -0
- package/src/session/prompt/build-switch.txt +5 -0
- package/src/session/prompt/codex_header.txt +79 -0
- package/src/session/prompt/copilot-gpt-5.txt +143 -0
- package/src/session/prompt/gemini.txt +155 -0
- package/src/session/prompt/max-steps.txt +16 -0
- package/src/session/prompt/plan-reminder-anthropic.txt +67 -0
- package/src/session/prompt/plan.txt +25 -0
- package/src/session/prompt/qwen.txt +108 -0
- package/src/session/prompt.ts +1942 -0
- package/src/session/retry.ts +90 -0
- package/src/session/revert.ts +120 -0
- package/src/session/stats.ts +404 -0
- package/src/session/status.ts +84 -0
- package/src/session/summary.ts +184 -0
- package/src/session/system.ts +195 -0
- package/src/session/toast.tsx +105 -0
- package/src/session/todo.ts +258 -0
- package/src/session/uninstall.ts +357 -0
- package/src/share/share-next.ts +421 -0
- package/src/share/share.ts +92 -0
- package/src/shell/shell.ts +65 -0
- package/src/skill/index.ts +1 -0
- package/src/skill/skill.ts +232 -0
- package/src/snapshot/index.ts +297 -0
- package/src/storage/storage.ts +227 -0
- package/src/tool/apply_patch.ts +288 -0
- package/src/tool/apply_patch.txt +33 -0
- package/src/tool/bash.ts +252 -0
- package/src/tool/bash.txt +115 -0
- package/src/tool/batch.ts +175 -0
- package/src/tool/batch.txt +24 -0
- package/src/tool/codesearch.ts +132 -0
- package/src/tool/codesearch.txt +12 -0
- package/src/tool/context_collect.ts +152 -0
- package/src/tool/context_collect.txt +9 -0
- package/src/tool/context_diagnostics.ts +81 -0
- package/src/tool/context_diagnostics.txt +5 -0
- package/src/tool/context_related.ts +117 -0
- package/src/tool/context_related.txt +5 -0
- package/src/tool/context_search.ts +108 -0
- package/src/tool/context_search.txt +8 -0
- package/src/tool/db-diff.ts +434 -0
- package/src/tool/db-table.txt +15 -0
- package/src/tool/docs_add.ts +50 -0
- package/src/tool/docs_add.txt +5 -0
- package/src/tool/docs_context.ts +56 -0
- package/src/tool/docs_context.txt +4 -0
- package/src/tool/docs_gap_report.ts +79 -0
- package/src/tool/docs_gap_report.txt +7 -0
- package/src/tool/docs_load.ts +41 -0
- package/src/tool/docs_load.txt +4 -0
- package/src/tool/docs_request.ts +129 -0
- package/src/tool/docs_request.txt +7 -0
- package/src/tool/docs_search.ts +51 -0
- package/src/tool/docs_search.txt +6 -0
- package/src/tool/docs_unload.ts +38 -0
- package/src/tool/docs_unload.txt +5 -0
- package/src/tool/edit.ts +614 -0
- package/src/tool/edit.txt +10 -0
- package/src/tool/external-directory.ts +32 -0
- package/src/tool/generate_image.ts +174 -0
- package/src/tool/generate_image.txt +12 -0
- package/src/tool/glob.ts +79 -0
- package/src/tool/glob.txt +6 -0
- package/src/tool/grep.ts +153 -0
- package/src/tool/grep.txt +8 -0
- package/src/tool/invalid.ts +17 -0
- package/src/tool/ls.ts +116 -0
- package/src/tool/ls.txt +1 -0
- package/src/tool/lsp.ts +96 -0
- package/src/tool/lsp.txt +19 -0
- package/src/tool/memory_search.ts +141 -0
- package/src/tool/memory_search.txt +8 -0
- package/src/tool/multiedit.ts +46 -0
- package/src/tool/multiedit.txt +41 -0
- package/src/tool/plan-enter.txt +14 -0
- package/src/tool/plan-exit.txt +13 -0
- package/src/tool/plan.ts +130 -0
- package/src/tool/question.ts +33 -0
- package/src/tool/question.txt +10 -0
- package/src/tool/rag_index.ts +77 -0
- package/src/tool/rag_index.txt +10 -0
- package/src/tool/rag_reset.ts +26 -0
- package/src/tool/rag_reset.txt +4 -0
- package/src/tool/rag_search.ts +62 -0
- package/src/tool/rag_search.txt +6 -0
- package/src/tool/rag_status.ts +45 -0
- package/src/tool/rag_status.txt +4 -0
- package/src/tool/read.ts +203 -0
- package/src/tool/read.txt +12 -0
- package/src/tool/registry.ts +214 -0
- package/src/tool/skill.ts +169 -0
- package/src/tool/skill.txt +3 -0
- package/src/tool/smart_docs.ts +74 -0
- package/src/tool/smart_docs.txt +7 -0
- package/src/tool/speak/elevenlabs.ts +201 -0
- package/src/tool/speak/openrouter.ts +240 -0
- package/src/tool/speak/provider.ts +83 -0
- package/src/tool/speak.ts +440 -0
- package/src/tool/task.ts +194 -0
- package/src/tool/task.txt +60 -0
- package/src/tool/todo.ts +53 -0
- package/src/tool/todoread.txt +14 -0
- package/src/tool/todowrite.txt +167 -0
- package/src/tool/tool.ts +87 -0
- package/src/tool/tree.ts +218 -0
- package/src/tool/tree.txt +8 -0
- package/src/tool/truncation.ts +106 -0
- package/src/tool/use-connector.ts +47 -0
- package/src/tool/voice.ts +188 -0
- package/src/tool/webfetch.ts +205 -0
- package/src/tool/webfetch.txt +13 -0
- package/src/tool/websearch.ts +150 -0
- package/src/tool/websearch.txt +14 -0
- package/src/tool/write.ts +80 -0
- package/src/tool/write.txt +8 -0
- package/src/util/archive.ts +16 -0
- package/src/util/color.ts +19 -0
- package/src/util/context.ts +25 -0
- package/src/util/defer.ts +12 -0
- package/src/util/error.ts +77 -0
- package/src/util/eventloop.ts +20 -0
- package/src/util/filesystem.ts +125 -0
- package/src/util/flock.ts +329 -0
- package/src/util/fn.ts +11 -0
- package/src/util/format.ts +20 -0
- package/src/util/hash.ts +7 -0
- package/src/util/iife.ts +3 -0
- package/src/util/keybind.ts +103 -0
- package/src/util/lazy.ts +18 -0
- package/src/util/locale.ts +81 -0
- package/src/util/lock.ts +98 -0
- package/src/util/log.ts +180 -0
- package/src/util/network.ts +9 -0
- package/src/util/process.ts +15 -0
- package/src/util/queue.ts +32 -0
- package/src/util/record.ts +3 -0
- package/src/util/rpc.ts +66 -0
- package/src/util/scrap.ts +10 -0
- package/src/util/signal.ts +12 -0
- package/src/util/timeout.ts +14 -0
- package/src/util/token.ts +7 -0
- package/src/util/wildcard.ts +56 -0
- package/src/workspace/adaptors/index.ts +271 -0
- package/src/workspace/adaptors/types.ts +14 -0
- package/src/workspace/adaptors/worktree.ts +31 -0
- package/src/workspace/config.ts +19 -0
- package/src/workspace/index.ts +223 -0
- package/src/workspace/session-proxy-middleware.ts +97 -0
- package/src/workspace/sse.ts +66 -0
- package/src/workspace/workspace-context.ts +23 -0
- package/src/workspace/workspace-server/routes.ts +33 -0
- package/src/workspace/workspace-server/server.ts +47 -0
- package/src/worktree/index.ts +487 -0
- package/sst-env.d.ts +10 -0
- package/test/benchmark.test.ts +121 -0
- package/test/build-optimizations.test.ts +124 -0
- package/test/id-benchmark.test.ts +132 -0
- package/test/optimizations.test.ts +302 -0
- package/test/preload.ts +1 -0
- package/test/solidjs-benchmark.test.ts +262 -0
- package/test/solidjs-optimizations.test.ts +259 -0
- package/test/tui-benchmark.test.ts +230 -0
- package/test/wildcard-benchmark.test.ts +180 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,2044 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { Hono } from "hono"
|
|
3
|
+
import { describeRoute, resolver, validator } from "hono-openapi"
|
|
4
|
+
import { streamSSE } from "hono/streaming"
|
|
5
|
+
import { NamedError } from "@nikcli-ai/util/error"
|
|
6
|
+
import z from "zod"
|
|
7
|
+
import { Bus } from "@/bus"
|
|
8
|
+
import { Instance } from "@/project/instance"
|
|
9
|
+
import { Project } from "@/project/project"
|
|
10
|
+
import { Session } from "@/session"
|
|
11
|
+
import { SessionPrompt } from "@/session/prompt"
|
|
12
|
+
import { SessionStatus } from "@/session/status"
|
|
13
|
+
import { SessionSummary } from "@/session/summary"
|
|
14
|
+
import { MessageV2 } from "@/session/message-v2"
|
|
15
|
+
import { Agent } from "@/agent/agent"
|
|
16
|
+
import { PermissionNext } from "@/permission/next"
|
|
17
|
+
import { Provider } from "@/provider/provider"
|
|
18
|
+
import { GlobalBus } from "@/bus/global"
|
|
19
|
+
import { Snapshot } from "@/snapshot"
|
|
20
|
+
import { Worktree } from "@/worktree"
|
|
21
|
+
import { GithubApi } from "@/connectors/api/github"
|
|
22
|
+
import { ConnectorAuth } from "@/connectors/auth"
|
|
23
|
+
import { Connectors } from "@/connectors"
|
|
24
|
+
import { Installation } from "@/installation"
|
|
25
|
+
import { Global } from "@/global"
|
|
26
|
+
import { MobileAuth } from "@/mobile/auth"
|
|
27
|
+
import { MobileGithubRepo } from "@/mobile/github-repo"
|
|
28
|
+
import { Storage } from "@/storage/storage"
|
|
29
|
+
import { Flag } from "@/flag/flag"
|
|
30
|
+
import { Config } from "@/config/config"
|
|
31
|
+
import { Command } from "@/command"
|
|
32
|
+
import { Workspace } from "@/workspace"
|
|
33
|
+
import { WorkspaceContext } from "@/workspace/workspace-context"
|
|
34
|
+
import { getContainerRuntimeInfo } from "@/workspace/adaptors"
|
|
35
|
+
import { proxyWorkspaceRequest } from "@/workspace/session-proxy-middleware"
|
|
36
|
+
import { PromptStashStore } from "@/prompt/stash-store"
|
|
37
|
+
import { errors } from "../error"
|
|
38
|
+
import { lazy } from "@/util/lazy"
|
|
39
|
+
import { Log } from "@/util/log"
|
|
40
|
+
|
|
41
|
+
const log = Log.create({ service: "mobile-routes" })
|
|
42
|
+
|
|
43
|
+
const MobileProject = Project.Info.extend({ current: z.boolean() }).meta({ ref: "MobileProject" })
|
|
44
|
+
const MobileExecutionTarget = z.enum(["local", "container"]).meta({ ref: "MobileExecutionTarget" })
|
|
45
|
+
|
|
46
|
+
const MobileBootstrap = z
|
|
47
|
+
.object({
|
|
48
|
+
version: z.string(),
|
|
49
|
+
auth: z.object({
|
|
50
|
+
bearerEnabled: z.boolean(),
|
|
51
|
+
currentToken: MobileAuth.PublicToken.optional(),
|
|
52
|
+
}),
|
|
53
|
+
currentProject: MobileProject,
|
|
54
|
+
projects: MobileProject.array(),
|
|
55
|
+
execution: z.object({
|
|
56
|
+
container: z.object({
|
|
57
|
+
available: z.boolean(),
|
|
58
|
+
runtime: z.enum(["docker", "podman"]).optional(),
|
|
59
|
+
image: z.string(),
|
|
60
|
+
}),
|
|
61
|
+
}),
|
|
62
|
+
github: z.object({
|
|
63
|
+
connected: z.boolean(),
|
|
64
|
+
oauthDeviceEnabled: z.boolean(),
|
|
65
|
+
oauthDeviceConfigured: z.boolean().optional(),
|
|
66
|
+
oauthClientSource: z.enum(["flag", "config", "env"]).optional(),
|
|
67
|
+
user: z
|
|
68
|
+
.object({
|
|
69
|
+
login: z.string(),
|
|
70
|
+
name: z.string().nullable().optional(),
|
|
71
|
+
avatar_url: z.string().optional(),
|
|
72
|
+
})
|
|
73
|
+
.optional(),
|
|
74
|
+
}),
|
|
75
|
+
})
|
|
76
|
+
.meta({ ref: "MobileBootstrap" })
|
|
77
|
+
|
|
78
|
+
const MobileSessionSummary = z
|
|
79
|
+
.object({
|
|
80
|
+
info: Session.Info,
|
|
81
|
+
status: SessionStatus.Info.optional(),
|
|
82
|
+
})
|
|
83
|
+
.meta({ ref: "MobileSessionSummary" })
|
|
84
|
+
|
|
85
|
+
const MobileSessionDetail = z
|
|
86
|
+
.object({
|
|
87
|
+
info: Session.Info,
|
|
88
|
+
status: SessionStatus.Info.optional(),
|
|
89
|
+
messages: MessageV2.WithParts.array(),
|
|
90
|
+
permissions: PermissionNext.Request.array(),
|
|
91
|
+
})
|
|
92
|
+
.meta({ ref: "MobileSessionDetail" })
|
|
93
|
+
|
|
94
|
+
const GithubAuthInput = z.object({ token: z.string().min(1) })
|
|
95
|
+
|
|
96
|
+
const MobileGithubBranch = z
|
|
97
|
+
.object({
|
|
98
|
+
name: z.string(),
|
|
99
|
+
protected: z.boolean().optional(),
|
|
100
|
+
commit: z.object({
|
|
101
|
+
sha: z.string(),
|
|
102
|
+
}),
|
|
103
|
+
})
|
|
104
|
+
.meta({ ref: "MobileGithubBranch" })
|
|
105
|
+
|
|
106
|
+
const MobileGithubSessionCreateInput = z
|
|
107
|
+
.object({
|
|
108
|
+
owner: z.string().min(1),
|
|
109
|
+
repo: z.string().min(1),
|
|
110
|
+
cloneUrl: z.url(),
|
|
111
|
+
htmlUrl: z.url().optional(),
|
|
112
|
+
defaultBranch: z.string().min(1),
|
|
113
|
+
baseBranch: z.string().min(1),
|
|
114
|
+
private: z.boolean().default(false),
|
|
115
|
+
title: z.string().optional(),
|
|
116
|
+
executionTarget: MobileExecutionTarget.default("local"),
|
|
117
|
+
})
|
|
118
|
+
.meta({ ref: "MobileGithubSessionCreateInput" })
|
|
119
|
+
|
|
120
|
+
const MobileSessionCreateInput = z
|
|
121
|
+
.object({
|
|
122
|
+
parentID: Session.Info.shape.parentID,
|
|
123
|
+
title: Session.Info.shape.title.optional(),
|
|
124
|
+
permission: Session.Info.shape.permission,
|
|
125
|
+
github: Session.Info.shape.github.optional(),
|
|
126
|
+
executionTarget: MobileExecutionTarget.default("local"),
|
|
127
|
+
})
|
|
128
|
+
.optional()
|
|
129
|
+
.meta({ ref: "MobileSessionCreateInput" })
|
|
130
|
+
|
|
131
|
+
const MobileGithubSessionCreateResult = z
|
|
132
|
+
.object({
|
|
133
|
+
session: Session.Info,
|
|
134
|
+
worktree: Worktree.Info,
|
|
135
|
+
project: Project.Info,
|
|
136
|
+
workspace: Workspace.Info.optional(),
|
|
137
|
+
})
|
|
138
|
+
.meta({ ref: "MobileGithubSessionCreateResult" })
|
|
139
|
+
|
|
140
|
+
const MobileCommand = z
|
|
141
|
+
.object({
|
|
142
|
+
name: z.string(),
|
|
143
|
+
description: z.string().optional(),
|
|
144
|
+
agent: z.string().optional(),
|
|
145
|
+
model: z.string().optional(),
|
|
146
|
+
mcp: z.boolean().optional(),
|
|
147
|
+
skill: z.boolean().optional(),
|
|
148
|
+
subtask: z.boolean().optional(),
|
|
149
|
+
hints: z.array(z.string()),
|
|
150
|
+
})
|
|
151
|
+
.meta({ ref: "MobileCommand" })
|
|
152
|
+
|
|
153
|
+
const MobileSessionCommandInput = z
|
|
154
|
+
.object({
|
|
155
|
+
command: z.string().min(1),
|
|
156
|
+
arguments: z.string().default(""),
|
|
157
|
+
agent: z.string().optional(),
|
|
158
|
+
model: z
|
|
159
|
+
.object({
|
|
160
|
+
providerID: z.string(),
|
|
161
|
+
modelID: z.string(),
|
|
162
|
+
})
|
|
163
|
+
.optional(),
|
|
164
|
+
})
|
|
165
|
+
.meta({ ref: "MobileSessionCommandInput" })
|
|
166
|
+
|
|
167
|
+
const MobileGithubPublishInput = z
|
|
168
|
+
.object({
|
|
169
|
+
title: z.string().optional(),
|
|
170
|
+
body: z.string().optional(),
|
|
171
|
+
commitMessage: z.string().optional(),
|
|
172
|
+
})
|
|
173
|
+
.optional()
|
|
174
|
+
.meta({ ref: "MobileGithubPublishInput" })
|
|
175
|
+
|
|
176
|
+
const MobileGithubPublishResult = z
|
|
177
|
+
.object({
|
|
178
|
+
commitSha: z.string(),
|
|
179
|
+
branch: z.string(),
|
|
180
|
+
pullRequest: z.object({
|
|
181
|
+
number: z.number(),
|
|
182
|
+
url: z.string(),
|
|
183
|
+
title: z.string(),
|
|
184
|
+
}),
|
|
185
|
+
})
|
|
186
|
+
.meta({ ref: "MobileGithubPublishResult" })
|
|
187
|
+
|
|
188
|
+
const MobileGithubDeviceAuthStart = z
|
|
189
|
+
.object({
|
|
190
|
+
deviceCode: z.string(),
|
|
191
|
+
userCode: z.string(),
|
|
192
|
+
verificationUri: z.string(),
|
|
193
|
+
verificationUriComplete: z.string().optional(),
|
|
194
|
+
expiresAt: z.number(),
|
|
195
|
+
interval: z.number(),
|
|
196
|
+
})
|
|
197
|
+
.meta({ ref: "MobileGithubDeviceAuthStart" })
|
|
198
|
+
|
|
199
|
+
const MobileGithubDeviceAuthPollInput = z
|
|
200
|
+
.object({
|
|
201
|
+
deviceCode: z.string().min(1),
|
|
202
|
+
})
|
|
203
|
+
.meta({ ref: "MobileGithubDeviceAuthPollInput" })
|
|
204
|
+
|
|
205
|
+
const MobileGithubDeviceAuthPollResult = z
|
|
206
|
+
.object({
|
|
207
|
+
status: z.enum(["pending", "approved", "denied", "expired"]),
|
|
208
|
+
interval: z.number().optional(),
|
|
209
|
+
user: z
|
|
210
|
+
.object({
|
|
211
|
+
login: z.string(),
|
|
212
|
+
name: z.string().nullable().optional(),
|
|
213
|
+
avatar_url: z.string().optional(),
|
|
214
|
+
})
|
|
215
|
+
.optional(),
|
|
216
|
+
})
|
|
217
|
+
.meta({ ref: "MobileGithubDeviceAuthPollResult" })
|
|
218
|
+
|
|
219
|
+
const MobilePromptHistoryEntry = z
|
|
220
|
+
.object({
|
|
221
|
+
id: z.string(),
|
|
222
|
+
input: z.string(),
|
|
223
|
+
mode: z.enum(["normal", "shell"]).optional(),
|
|
224
|
+
partsCount: z.number(),
|
|
225
|
+
})
|
|
226
|
+
.meta({ ref: "MobilePromptHistoryEntry" })
|
|
227
|
+
|
|
228
|
+
const MobilePromptStashEntry = z
|
|
229
|
+
.object({
|
|
230
|
+
id: z.string(),
|
|
231
|
+
input: z.string(),
|
|
232
|
+
timestamp: z.number(),
|
|
233
|
+
partsCount: z.number(),
|
|
234
|
+
})
|
|
235
|
+
.meta({ ref: "MobilePromptStashEntry" })
|
|
236
|
+
|
|
237
|
+
const MobilePromptStashCreateInput = z
|
|
238
|
+
.object({
|
|
239
|
+
input: z.string().trim().min(1),
|
|
240
|
+
})
|
|
241
|
+
.meta({ ref: "MobilePromptStashCreateInput" })
|
|
242
|
+
|
|
243
|
+
const MobileMemorySearchHit = z
|
|
244
|
+
.object({
|
|
245
|
+
id: z.string(),
|
|
246
|
+
sessionID: z.string(),
|
|
247
|
+
sessionTitle: z.string(),
|
|
248
|
+
messageID: z.string(),
|
|
249
|
+
role: z.enum(["user", "assistant"]),
|
|
250
|
+
createdAt: z.number(),
|
|
251
|
+
preview: z.string(),
|
|
252
|
+
})
|
|
253
|
+
.meta({ ref: "MobileMemorySearchHit" })
|
|
254
|
+
|
|
255
|
+
function currentToken(c: any) {
|
|
256
|
+
return (c.get("mobileAuth") as MobileAuth.PublicToken | undefined) ?? undefined
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
type PromptHistoryRecord = {
|
|
260
|
+
input: string
|
|
261
|
+
mode?: "normal" | "shell"
|
|
262
|
+
parts?: unknown[]
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
type PromptStashRecord = {
|
|
266
|
+
input: string
|
|
267
|
+
timestamp: number
|
|
268
|
+
parts?: unknown[]
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function readJsonLines<T>(filePath: string) {
|
|
272
|
+
const text = await Bun.file(filePath)
|
|
273
|
+
.text()
|
|
274
|
+
.catch(() => "")
|
|
275
|
+
return text
|
|
276
|
+
.split("\n")
|
|
277
|
+
.filter(Boolean)
|
|
278
|
+
.map((line) => {
|
|
279
|
+
try {
|
|
280
|
+
return JSON.parse(line) as T
|
|
281
|
+
} catch {
|
|
282
|
+
return null
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
.filter((item): item is T => item !== null)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function historyFilePath() {
|
|
289
|
+
return path.join(Global.Path.state, "prompt-history.jsonl")
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function stashFilePath() {
|
|
293
|
+
return path.join(Global.Path.state, "prompt-stash.jsonl")
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function listPromptHistory() {
|
|
297
|
+
const entries = await readJsonLines<PromptHistoryRecord>(historyFilePath())
|
|
298
|
+
return entries
|
|
299
|
+
.filter((entry): entry is PromptHistoryRecord => typeof entry.input === "string")
|
|
300
|
+
.slice(-50)
|
|
301
|
+
.reverse()
|
|
302
|
+
.map((entry, index) => ({
|
|
303
|
+
id: `${index}`,
|
|
304
|
+
input: entry.input,
|
|
305
|
+
mode: entry.mode === "shell" ? "shell" : entry.mode === "normal" ? "normal" : undefined,
|
|
306
|
+
partsCount: Array.isArray(entry.parts) ? entry.parts.length : 0,
|
|
307
|
+
}))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function listPromptStash() {
|
|
311
|
+
const entries = await PromptStashStore.list()
|
|
312
|
+
return entries
|
|
313
|
+
.sort((a, b) => b.timestamp - a.timestamp)
|
|
314
|
+
.map((entry) => ({
|
|
315
|
+
id: entry.id,
|
|
316
|
+
input: entry.input,
|
|
317
|
+
timestamp: entry.timestamp,
|
|
318
|
+
partsCount: Array.isArray(entry.parts) ? entry.parts.length : 0,
|
|
319
|
+
}))
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function messageSearchText(message: MessageV2.WithParts) {
|
|
323
|
+
const text = message.parts
|
|
324
|
+
.filter((part): part is Extract<MessageV2.WithParts["parts"][number], { type: "text" }> => part.type === "text")
|
|
325
|
+
.map((part) => part.text)
|
|
326
|
+
.join("\n\n")
|
|
327
|
+
.trim()
|
|
328
|
+
if (text) return text
|
|
329
|
+
if (message.info.role === "assistant") {
|
|
330
|
+
return message.info.error?.data?.message?.trim() ?? ""
|
|
331
|
+
}
|
|
332
|
+
return ""
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function snippetForQuery(text: string, query: string) {
|
|
336
|
+
const lower = text.toLowerCase()
|
|
337
|
+
const index = lower.indexOf(query)
|
|
338
|
+
if (index === -1) return text.slice(0, 180)
|
|
339
|
+
const start = Math.max(0, index - 48)
|
|
340
|
+
const end = Math.min(text.length, index + query.length + 108)
|
|
341
|
+
const prefix = start > 0 ? "..." : ""
|
|
342
|
+
const suffix = end < text.length ? "..." : ""
|
|
343
|
+
return `${prefix}${text.slice(start, end).trim()}${suffix}`
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function searchPromptMemories(query: string) {
|
|
347
|
+
const normalized = query.trim().toLowerCase()
|
|
348
|
+
if (!normalized) return []
|
|
349
|
+
const hits: Array<{
|
|
350
|
+
id: string
|
|
351
|
+
sessionID: string
|
|
352
|
+
sessionTitle: string
|
|
353
|
+
messageID: string
|
|
354
|
+
role: "user" | "assistant"
|
|
355
|
+
createdAt: number
|
|
356
|
+
preview: string
|
|
357
|
+
}> = []
|
|
358
|
+
|
|
359
|
+
const sessionKeys = await Storage.list(["session"])
|
|
360
|
+
for (const key of sessionKeys) {
|
|
361
|
+
if (key.length !== 3) continue
|
|
362
|
+
const session = await Storage.read<Session.Info>(key).catch(() => undefined)
|
|
363
|
+
if (!session) continue
|
|
364
|
+
const messages = await Session.messages({ sessionID: session.id }).catch(() => [])
|
|
365
|
+
for (const message of messages) {
|
|
366
|
+
const text = messageSearchText(message)
|
|
367
|
+
if (!text || !text.toLowerCase().includes(normalized)) continue
|
|
368
|
+
hits.push({
|
|
369
|
+
id: `${session.id}:${message.info.id}`,
|
|
370
|
+
sessionID: session.id,
|
|
371
|
+
sessionTitle: session.title,
|
|
372
|
+
messageID: message.info.id,
|
|
373
|
+
role: message.info.role,
|
|
374
|
+
createdAt: message.info.time.created,
|
|
375
|
+
preview: snippetForQuery(text, normalized),
|
|
376
|
+
})
|
|
377
|
+
if (hits.length >= 40) break
|
|
378
|
+
}
|
|
379
|
+
if (hits.length >= 40) break
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return hits.sort((a, b) => b.createdAt - a.createdAt).slice(0, 24)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function latestPromptDefaults(sessionID: string) {
|
|
386
|
+
const messages = await Session.messages({ sessionID, limit: 24 }).catch(() => [])
|
|
387
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
388
|
+
const info = messages[index]?.info
|
|
389
|
+
if (!info || info.role !== "user") continue
|
|
390
|
+
return {
|
|
391
|
+
agent: info.agent,
|
|
392
|
+
model: info.model,
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return {}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function resolveMobilePromptDefaults(session: Session.Info) {
|
|
399
|
+
return Instance.provide({
|
|
400
|
+
directory: session.directory,
|
|
401
|
+
async fn() {
|
|
402
|
+
const current = await latestPromptDefaults(session.id)
|
|
403
|
+
if (current.agent && current.model) return current
|
|
404
|
+
|
|
405
|
+
const allKeys = await Storage.list(["session"])
|
|
406
|
+
const sessions: Session.Info[] = []
|
|
407
|
+
for (const key of allKeys) {
|
|
408
|
+
if (key.length !== 3 || key[2] === session.id) continue
|
|
409
|
+
const candidate = await Storage.read<Session.Info>(key).catch(() => undefined)
|
|
410
|
+
if (!candidate || candidate.projectID !== session.projectID) continue
|
|
411
|
+
sessions.push(candidate)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
sessions.sort((a, b) => b.time.updated - a.time.updated)
|
|
415
|
+
|
|
416
|
+
for (const candidate of sessions) {
|
|
417
|
+
const fallback = await latestPromptDefaults(candidate.id)
|
|
418
|
+
if (!fallback.agent || !fallback.model) continue
|
|
419
|
+
return {
|
|
420
|
+
agent: current.agent ?? fallback.agent,
|
|
421
|
+
model: current.model ?? fallback.model,
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return {
|
|
426
|
+
agent: current.agent ?? (await Agent.defaultAgent()),
|
|
427
|
+
model: current.model ?? (await Provider.defaultModel()),
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function extractSessionIDs(value: unknown): string[] {
|
|
434
|
+
if (!value || typeof value !== "object") return []
|
|
435
|
+
const result = new Set<string>()
|
|
436
|
+
const visit = (input: unknown) => {
|
|
437
|
+
if (!input || typeof input !== "object") return
|
|
438
|
+
if (Array.isArray(input)) {
|
|
439
|
+
for (const item of input) visit(item)
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
for (const [key, current] of Object.entries(input)) {
|
|
443
|
+
if ((key === "sessionID" || key === "id") && typeof current === "string" && current.startsWith("ses_")) {
|
|
444
|
+
result.add(current)
|
|
445
|
+
}
|
|
446
|
+
if (current && typeof current === "object") visit(current)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
visit(value)
|
|
450
|
+
return [...result]
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function githubToken() {
|
|
454
|
+
const auth = await ConnectorAuth.get("github")
|
|
455
|
+
return auth?.token
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function githubOAuthClientID() {
|
|
459
|
+
const config = await Config.get().catch(() => undefined)
|
|
460
|
+
const githubConnector = Object.values(config?.connectors ?? {}).find(
|
|
461
|
+
(connector): connector is Config.ConnectorGithub =>
|
|
462
|
+
typeof connector === "object" && connector !== null && "type" in connector && connector.type === "github",
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
const flagValue = Flag.NIKCLI_GITHUB_OAUTH_CLIENT_ID
|
|
466
|
+
if (flagValue) {
|
|
467
|
+
return {
|
|
468
|
+
clientID: flagValue,
|
|
469
|
+
source: "flag" as const,
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const configValue = githubConnector?.oauthClientId || githubConnector?.clientId
|
|
474
|
+
if (configValue) {
|
|
475
|
+
return {
|
|
476
|
+
clientID: configValue,
|
|
477
|
+
source: "config" as const,
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const envValue =
|
|
482
|
+
process.env.NIKCLI_GITHUB_OAUTH_CLIENT_ID || process.env.GITHUB_CLIENT_ID_CONSOLE || process.env.GITHUB_CLIENT_ID
|
|
483
|
+
|
|
484
|
+
if (envValue) {
|
|
485
|
+
return {
|
|
486
|
+
clientID: envValue,
|
|
487
|
+
source: "env" as const,
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
clientID: undefined,
|
|
493
|
+
source: undefined,
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function startGithubDeviceAuth() {
|
|
498
|
+
const { clientID } = await githubOAuthClientID()
|
|
499
|
+
if (!clientID) throw new Error("GitHub OAuth client ID is not configured on the host")
|
|
500
|
+
const response = await fetch("https://github.com/login/device/code", {
|
|
501
|
+
method: "POST",
|
|
502
|
+
headers: {
|
|
503
|
+
Accept: "application/json",
|
|
504
|
+
"Content-Type": "application/json",
|
|
505
|
+
"User-Agent": "nikcli-mobile",
|
|
506
|
+
},
|
|
507
|
+
body: JSON.stringify({
|
|
508
|
+
client_id: clientID,
|
|
509
|
+
scope: "repo read:user user:email",
|
|
510
|
+
}),
|
|
511
|
+
})
|
|
512
|
+
if (!response.ok) {
|
|
513
|
+
throw new Error(`GitHub device auth failed: ${response.status} ${response.statusText}`)
|
|
514
|
+
}
|
|
515
|
+
const payload = (await response.json()) as {
|
|
516
|
+
device_code: string
|
|
517
|
+
user_code: string
|
|
518
|
+
verification_uri: string
|
|
519
|
+
verification_uri_complete?: string
|
|
520
|
+
expires_in: number
|
|
521
|
+
interval?: number
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
deviceCode: payload.device_code,
|
|
525
|
+
userCode: payload.user_code,
|
|
526
|
+
verificationUri: payload.verification_uri,
|
|
527
|
+
verificationUriComplete: payload.verification_uri_complete,
|
|
528
|
+
expiresAt: Date.now() + payload.expires_in * 1000,
|
|
529
|
+
interval: payload.interval ?? 5,
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async function pollGithubDeviceAuth(deviceCode: string) {
|
|
534
|
+
const { clientID } = await githubOAuthClientID()
|
|
535
|
+
if (!clientID) throw new Error("GitHub OAuth client ID is not configured on the host")
|
|
536
|
+
const response = await fetch("https://github.com/login/oauth/access_token", {
|
|
537
|
+
method: "POST",
|
|
538
|
+
headers: {
|
|
539
|
+
Accept: "application/json",
|
|
540
|
+
"Content-Type": "application/json",
|
|
541
|
+
"User-Agent": "nikcli-mobile",
|
|
542
|
+
},
|
|
543
|
+
body: JSON.stringify({
|
|
544
|
+
client_id: clientID,
|
|
545
|
+
device_code: deviceCode,
|
|
546
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
547
|
+
}),
|
|
548
|
+
})
|
|
549
|
+
if (!response.ok) {
|
|
550
|
+
throw new Error(`GitHub auth polling failed: ${response.status} ${response.statusText}`)
|
|
551
|
+
}
|
|
552
|
+
const payload = (await response.json()) as {
|
|
553
|
+
access_token?: string
|
|
554
|
+
error?: string
|
|
555
|
+
interval?: number
|
|
556
|
+
}
|
|
557
|
+
if (payload.access_token) {
|
|
558
|
+
const user = await GithubApi.getUser(payload.access_token)
|
|
559
|
+
await ConnectorAuth.set("github", { token: payload.access_token })
|
|
560
|
+
Connectors.invalidateConnector("github")
|
|
561
|
+
return {
|
|
562
|
+
status: "approved" as const,
|
|
563
|
+
user: {
|
|
564
|
+
login: user.login,
|
|
565
|
+
name: user.name,
|
|
566
|
+
avatar_url: user.avatar_url,
|
|
567
|
+
},
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (payload.error === "authorization_pending") {
|
|
571
|
+
return {
|
|
572
|
+
status: "pending" as const,
|
|
573
|
+
interval: payload.interval ?? 5,
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (payload.error === "slow_down") {
|
|
577
|
+
return {
|
|
578
|
+
status: "pending" as const,
|
|
579
|
+
interval: Math.max(payload.interval ?? 5, 10),
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (payload.error === "access_denied") {
|
|
583
|
+
return { status: "denied" as const }
|
|
584
|
+
}
|
|
585
|
+
if (payload.error === "expired_token") {
|
|
586
|
+
return { status: "expired" as const }
|
|
587
|
+
}
|
|
588
|
+
throw new Error(payload.error || "GitHub auth polling failed")
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function githubUser() {
|
|
592
|
+
const token = await githubToken()
|
|
593
|
+
if (!token) return
|
|
594
|
+
return GithubApi.getUser(token).catch(() => undefined)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function githubImports() {
|
|
598
|
+
const imports = await MobileGithubRepo.list()
|
|
599
|
+
return new Map(imports.map((item) => [item.fullName.toLowerCase(), item] as const))
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function slug(input: string) {
|
|
603
|
+
return input
|
|
604
|
+
.trim()
|
|
605
|
+
.toLowerCase()
|
|
606
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
607
|
+
.replace(/^-+/, "")
|
|
608
|
+
.replace(/-+$/, "")
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function sessionSeed() {
|
|
612
|
+
return Math.random().toString(36).slice(2, 8)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function defaultPullRequestBody(session: Session.Info) {
|
|
616
|
+
return [
|
|
617
|
+
`Generated from mobile session \`${session.id}\`.`,
|
|
618
|
+
session.share?.url ? `Session share: ${session.share.url}` : "",
|
|
619
|
+
]
|
|
620
|
+
.filter(Boolean)
|
|
621
|
+
.join("\n\n")
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function toHeadersObject(headers: Headers) {
|
|
625
|
+
return Object.fromEntries(headers.entries())
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async function createExecutionWorkspace(input: {
|
|
629
|
+
directory: string
|
|
630
|
+
branch?: string | null
|
|
631
|
+
target: z.infer<typeof MobileExecutionTarget>
|
|
632
|
+
}) {
|
|
633
|
+
if (input.target !== "container") return undefined
|
|
634
|
+
const runtimeInfo = await getContainerRuntimeInfo()
|
|
635
|
+
if (!runtimeInfo.available || !runtimeInfo.runtime) {
|
|
636
|
+
throw new Error(
|
|
637
|
+
"Container sandbox is unavailable. Check Docker or Podman and the Nikcli workspace image on the host.",
|
|
638
|
+
)
|
|
639
|
+
}
|
|
640
|
+
const runtime: "docker" | "podman" = runtimeInfo.runtime
|
|
641
|
+
const project = await Project.fromDirectory(input.directory)
|
|
642
|
+
return Workspace.create({
|
|
643
|
+
projectID: project.project.id,
|
|
644
|
+
branch: input.branch ?? null,
|
|
645
|
+
config: {
|
|
646
|
+
type: "container",
|
|
647
|
+
directory: input.directory,
|
|
648
|
+
runtime,
|
|
649
|
+
image: runtimeInfo.image,
|
|
650
|
+
containerName: "pending",
|
|
651
|
+
port: 1,
|
|
652
|
+
serverUrl: "http://127.0.0.1:1",
|
|
653
|
+
},
|
|
654
|
+
})
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async function statusForSession(session: Session.Info) {
|
|
658
|
+
return Instance.provide({
|
|
659
|
+
directory: session.directory,
|
|
660
|
+
async fn() {
|
|
661
|
+
return SessionStatus.get(session.id)
|
|
662
|
+
},
|
|
663
|
+
})
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export const MobileRoutes = lazy(() =>
|
|
667
|
+
new Hono()
|
|
668
|
+
.post(
|
|
669
|
+
"/auth/token",
|
|
670
|
+
describeRoute({
|
|
671
|
+
summary: "Create mobile auth token",
|
|
672
|
+
description: "Exchange valid Basic auth credentials for a long-lived mobile Bearer token.",
|
|
673
|
+
operationId: "mobile.auth.token.create",
|
|
674
|
+
responses: {
|
|
675
|
+
200: {
|
|
676
|
+
description: "Mobile token",
|
|
677
|
+
content: {
|
|
678
|
+
"application/json": {
|
|
679
|
+
schema: resolver(z.object({ token: z.string(), info: MobileAuth.PublicToken })),
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
},
|
|
683
|
+
},
|
|
684
|
+
}),
|
|
685
|
+
validator(
|
|
686
|
+
"json",
|
|
687
|
+
z
|
|
688
|
+
.object({
|
|
689
|
+
name: z.string().optional(),
|
|
690
|
+
expiresInDays: z.number().optional(),
|
|
691
|
+
})
|
|
692
|
+
.optional(),
|
|
693
|
+
),
|
|
694
|
+
async (c) => {
|
|
695
|
+
const body = c.req.valid("json")
|
|
696
|
+
const result = await MobileAuth.create(body ?? undefined)
|
|
697
|
+
return c.json(result)
|
|
698
|
+
},
|
|
699
|
+
)
|
|
700
|
+
.delete(
|
|
701
|
+
"/auth/token/:id",
|
|
702
|
+
describeRoute({
|
|
703
|
+
summary: "Revoke mobile auth token",
|
|
704
|
+
description: "Revoke a previously issued mobile Bearer token.",
|
|
705
|
+
operationId: "mobile.auth.token.revoke",
|
|
706
|
+
responses: {
|
|
707
|
+
200: {
|
|
708
|
+
description: "Token revoked",
|
|
709
|
+
content: { "application/json": { schema: resolver(z.object({ revoked: z.boolean() })) } },
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
}),
|
|
713
|
+
validator("param", z.object({ id: z.string() })),
|
|
714
|
+
async (c) => {
|
|
715
|
+
const removed = await MobileAuth.remove(c.req.valid("param").id)
|
|
716
|
+
return c.json({ revoked: removed })
|
|
717
|
+
},
|
|
718
|
+
)
|
|
719
|
+
.get(
|
|
720
|
+
"/auth/token",
|
|
721
|
+
describeRoute({
|
|
722
|
+
summary: "List mobile auth tokens",
|
|
723
|
+
description: "List all active mobile Bearer tokens.",
|
|
724
|
+
operationId: "mobile.auth.token.list",
|
|
725
|
+
responses: {
|
|
726
|
+
200: {
|
|
727
|
+
description: "Token list",
|
|
728
|
+
content: { "application/json": { schema: resolver(MobileAuth.PublicToken.array()) } },
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
}),
|
|
732
|
+
async (c) => {
|
|
733
|
+
return c.json(await MobileAuth.list())
|
|
734
|
+
},
|
|
735
|
+
)
|
|
736
|
+
.get(
|
|
737
|
+
"/bootstrap",
|
|
738
|
+
describeRoute({
|
|
739
|
+
summary: "Get mobile bootstrap payload",
|
|
740
|
+
description: "Return the current mobile bootstrap state for the connected host.",
|
|
741
|
+
operationId: "mobile.bootstrap",
|
|
742
|
+
responses: {
|
|
743
|
+
200: {
|
|
744
|
+
description: "Bootstrap payload",
|
|
745
|
+
content: { "application/json": { schema: resolver(MobileBootstrap) } },
|
|
746
|
+
},
|
|
747
|
+
},
|
|
748
|
+
}),
|
|
749
|
+
async (c) => {
|
|
750
|
+
const projects = await Project.list()
|
|
751
|
+
const token = currentToken(c)
|
|
752
|
+
const user = await githubUser()
|
|
753
|
+
const container = await getContainerRuntimeInfo()
|
|
754
|
+
const oauth = await githubOAuthClientID()
|
|
755
|
+
return c.json({
|
|
756
|
+
version: Installation.VERSION,
|
|
757
|
+
auth: {
|
|
758
|
+
bearerEnabled: true,
|
|
759
|
+
currentToken: token,
|
|
760
|
+
},
|
|
761
|
+
currentProject: {
|
|
762
|
+
...Instance.project,
|
|
763
|
+
current: true,
|
|
764
|
+
},
|
|
765
|
+
projects: projects.map((project) => ({
|
|
766
|
+
...project,
|
|
767
|
+
current: project.id === Instance.project.id,
|
|
768
|
+
})),
|
|
769
|
+
execution: {
|
|
770
|
+
container,
|
|
771
|
+
},
|
|
772
|
+
github: {
|
|
773
|
+
connected: Boolean(user),
|
|
774
|
+
oauthDeviceEnabled: true,
|
|
775
|
+
oauthDeviceConfigured: Boolean(oauth.clientID),
|
|
776
|
+
oauthClientSource: oauth.source,
|
|
777
|
+
user: user
|
|
778
|
+
? {
|
|
779
|
+
login: user.login,
|
|
780
|
+
name: user.name,
|
|
781
|
+
avatar_url: user.avatar_url,
|
|
782
|
+
}
|
|
783
|
+
: undefined,
|
|
784
|
+
},
|
|
785
|
+
})
|
|
786
|
+
},
|
|
787
|
+
)
|
|
788
|
+
.get(
|
|
789
|
+
"/memory/history",
|
|
790
|
+
describeRoute({
|
|
791
|
+
summary: "List prompt history for mobile",
|
|
792
|
+
description: "Return recent prompt history stored on the Nikcli host.",
|
|
793
|
+
operationId: "mobile.memory.history",
|
|
794
|
+
responses: {
|
|
795
|
+
200: {
|
|
796
|
+
description: "Prompt history",
|
|
797
|
+
content: { "application/json": { schema: resolver(MobilePromptHistoryEntry.array()) } },
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
}),
|
|
801
|
+
async (c) => {
|
|
802
|
+
return c.json(await listPromptHistory())
|
|
803
|
+
},
|
|
804
|
+
)
|
|
805
|
+
.get(
|
|
806
|
+
"/memory/search",
|
|
807
|
+
describeRoute({
|
|
808
|
+
summary: "Search prompt memories for mobile",
|
|
809
|
+
description: "Search across stored session messages for memory-like prompt context from mobile.",
|
|
810
|
+
operationId: "mobile.memory.search",
|
|
811
|
+
responses: {
|
|
812
|
+
200: {
|
|
813
|
+
description: "Memory search hits",
|
|
814
|
+
content: { "application/json": { schema: resolver(MobileMemorySearchHit.array()) } },
|
|
815
|
+
},
|
|
816
|
+
},
|
|
817
|
+
}),
|
|
818
|
+
validator("query", z.object({ query: z.string().trim().min(1) })),
|
|
819
|
+
async (c) => {
|
|
820
|
+
const query = c.req.valid("query").query
|
|
821
|
+
return c.json(await searchPromptMemories(query))
|
|
822
|
+
},
|
|
823
|
+
)
|
|
824
|
+
.get(
|
|
825
|
+
"/memory/stash",
|
|
826
|
+
describeRoute({
|
|
827
|
+
summary: "List prompt stash for mobile",
|
|
828
|
+
description: "Return reusable prompt snippets stored on the Nikcli host.",
|
|
829
|
+
operationId: "mobile.memory.stash.list",
|
|
830
|
+
responses: {
|
|
831
|
+
200: {
|
|
832
|
+
description: "Prompt stash",
|
|
833
|
+
content: { "application/json": { schema: resolver(MobilePromptStashEntry.array()) } },
|
|
834
|
+
},
|
|
835
|
+
},
|
|
836
|
+
}),
|
|
837
|
+
async (c) => {
|
|
838
|
+
return c.json(await listPromptStash())
|
|
839
|
+
},
|
|
840
|
+
)
|
|
841
|
+
.post(
|
|
842
|
+
"/memory/stash",
|
|
843
|
+
describeRoute({
|
|
844
|
+
summary: "Create prompt stash entry",
|
|
845
|
+
description: "Save a reusable prompt snippet on the Nikcli host.",
|
|
846
|
+
operationId: "mobile.memory.stash.create",
|
|
847
|
+
responses: {
|
|
848
|
+
200: {
|
|
849
|
+
description: "Created prompt stash entry",
|
|
850
|
+
content: { "application/json": { schema: resolver(MobilePromptStashEntry) } },
|
|
851
|
+
},
|
|
852
|
+
...errors(400),
|
|
853
|
+
},
|
|
854
|
+
}),
|
|
855
|
+
validator("json", MobilePromptStashCreateInput),
|
|
856
|
+
async (c) => {
|
|
857
|
+
const body = c.req.valid("json")
|
|
858
|
+
const [entry] = (
|
|
859
|
+
await PromptStashStore.push({
|
|
860
|
+
input: body.input.trim(),
|
|
861
|
+
parts: [] as any,
|
|
862
|
+
})
|
|
863
|
+
).slice(-1)
|
|
864
|
+
return c.json({
|
|
865
|
+
id: entry.id,
|
|
866
|
+
input: entry.input,
|
|
867
|
+
timestamp: entry.timestamp,
|
|
868
|
+
partsCount: 0,
|
|
869
|
+
})
|
|
870
|
+
},
|
|
871
|
+
)
|
|
872
|
+
.delete(
|
|
873
|
+
"/memory/stash/:id",
|
|
874
|
+
describeRoute({
|
|
875
|
+
summary: "Delete prompt stash entry",
|
|
876
|
+
description: "Remove a reusable prompt snippet from the Nikcli host.",
|
|
877
|
+
operationId: "mobile.memory.stash.delete",
|
|
878
|
+
responses: {
|
|
879
|
+
200: {
|
|
880
|
+
description: "Deleted",
|
|
881
|
+
content: { "application/json": { schema: resolver(z.object({ success: z.literal(true) })) } },
|
|
882
|
+
},
|
|
883
|
+
...errors(404),
|
|
884
|
+
},
|
|
885
|
+
}),
|
|
886
|
+
validator("param", z.object({ id: z.string() })),
|
|
887
|
+
async (c) => {
|
|
888
|
+
const id = c.req.valid("param").id
|
|
889
|
+
const current = await PromptStashStore.list()
|
|
890
|
+
const next = await PromptStashStore.removeByID(id)
|
|
891
|
+
if (next.length === current.length) {
|
|
892
|
+
return c.json({ error: "Prompt snippet not found" }, 404)
|
|
893
|
+
}
|
|
894
|
+
return c.json({ success: true as const })
|
|
895
|
+
},
|
|
896
|
+
)
|
|
897
|
+
.get(
|
|
898
|
+
"/command",
|
|
899
|
+
describeRoute({
|
|
900
|
+
summary: "List mobile commands",
|
|
901
|
+
description: "Return command metadata safe for the mobile command palette and slash autocomplete.",
|
|
902
|
+
operationId: "mobile.command.list",
|
|
903
|
+
responses: {
|
|
904
|
+
200: {
|
|
905
|
+
description: "Commands",
|
|
906
|
+
content: { "application/json": { schema: resolver(MobileCommand.array()) } },
|
|
907
|
+
},
|
|
908
|
+
},
|
|
909
|
+
}),
|
|
910
|
+
async (c) => {
|
|
911
|
+
const commands = await Command.list()
|
|
912
|
+
return c.json(
|
|
913
|
+
commands
|
|
914
|
+
.map((command) => ({
|
|
915
|
+
name: command.name,
|
|
916
|
+
description: command.description,
|
|
917
|
+
agent: command.agent,
|
|
918
|
+
model: command.model,
|
|
919
|
+
mcp: command.mcp,
|
|
920
|
+
skill: command.skill,
|
|
921
|
+
subtask: command.subtask,
|
|
922
|
+
hints: command.hints,
|
|
923
|
+
}))
|
|
924
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
925
|
+
)
|
|
926
|
+
},
|
|
927
|
+
)
|
|
928
|
+
.get(
|
|
929
|
+
"/project",
|
|
930
|
+
describeRoute({
|
|
931
|
+
summary: "List local projects for mobile",
|
|
932
|
+
description: "Return local projects and sandboxes visible to the connected Nikcli host.",
|
|
933
|
+
operationId: "mobile.project.list",
|
|
934
|
+
responses: {
|
|
935
|
+
200: {
|
|
936
|
+
description: "Projects",
|
|
937
|
+
content: { "application/json": { schema: resolver(MobileProject.array()) } },
|
|
938
|
+
},
|
|
939
|
+
},
|
|
940
|
+
}),
|
|
941
|
+
async (c) => {
|
|
942
|
+
const projects = await Project.list()
|
|
943
|
+
return c.json(
|
|
944
|
+
projects.map((project) => ({
|
|
945
|
+
...project,
|
|
946
|
+
current: project.id === Instance.project.id,
|
|
947
|
+
})),
|
|
948
|
+
)
|
|
949
|
+
},
|
|
950
|
+
)
|
|
951
|
+
.get(
|
|
952
|
+
"/github/repos",
|
|
953
|
+
describeRoute({
|
|
954
|
+
summary: "List GitHub repositories for mobile",
|
|
955
|
+
description: "List repositories available to the stored GitHub connector token.",
|
|
956
|
+
operationId: "mobile.github.repos",
|
|
957
|
+
responses: {
|
|
958
|
+
200: {
|
|
959
|
+
description: "GitHub repositories",
|
|
960
|
+
content: { "application/json": { schema: resolver(z.array(z.any())) } },
|
|
961
|
+
},
|
|
962
|
+
...errors(401),
|
|
963
|
+
},
|
|
964
|
+
}),
|
|
965
|
+
async (c) => {
|
|
966
|
+
const token = await githubToken()
|
|
967
|
+
if (!token) return c.json({ error: "GitHub token not configured" }, 401)
|
|
968
|
+
const [repos, imports] = await Promise.all([GithubApi.listRepos(token, "all", "updated"), githubImports()])
|
|
969
|
+
return c.json(
|
|
970
|
+
repos.map((repo: any) => {
|
|
971
|
+
const existing = imports.get(String(repo.full_name).toLowerCase())
|
|
972
|
+
return {
|
|
973
|
+
...repo,
|
|
974
|
+
imported: Boolean(existing),
|
|
975
|
+
imported_directory: existing?.directory,
|
|
976
|
+
imported_project_id: existing?.projectID,
|
|
977
|
+
}
|
|
978
|
+
}),
|
|
979
|
+
)
|
|
980
|
+
},
|
|
981
|
+
)
|
|
982
|
+
.get(
|
|
983
|
+
"/github/repos/:owner/:repo/branches",
|
|
984
|
+
describeRoute({
|
|
985
|
+
summary: "List GitHub branches for mobile",
|
|
986
|
+
description: "List branches for a GitHub repository available to the stored mobile GitHub token.",
|
|
987
|
+
operationId: "mobile.github.branches",
|
|
988
|
+
responses: {
|
|
989
|
+
200: {
|
|
990
|
+
description: "GitHub branches",
|
|
991
|
+
content: { "application/json": { schema: resolver(MobileGithubBranch.array()) } },
|
|
992
|
+
},
|
|
993
|
+
...errors(401),
|
|
994
|
+
},
|
|
995
|
+
}),
|
|
996
|
+
validator("param", z.object({ owner: z.string(), repo: z.string() })),
|
|
997
|
+
async (c) => {
|
|
998
|
+
const token = await githubToken()
|
|
999
|
+
if (!token) return c.json({ error: "GitHub token not configured" }, 401)
|
|
1000
|
+
const params = c.req.valid("param")
|
|
1001
|
+
const branches = await GithubApi.listBranches(token, params.owner, params.repo)
|
|
1002
|
+
return c.json(branches)
|
|
1003
|
+
},
|
|
1004
|
+
)
|
|
1005
|
+
.get(
|
|
1006
|
+
"/github/imports",
|
|
1007
|
+
describeRoute({
|
|
1008
|
+
summary: "List imported GitHub repos for mobile",
|
|
1009
|
+
description: "List GitHub repositories that have already been cloned into the Nikcli host cache.",
|
|
1010
|
+
operationId: "mobile.github.imports",
|
|
1011
|
+
responses: {
|
|
1012
|
+
200: {
|
|
1013
|
+
description: "Imported repos",
|
|
1014
|
+
content: { "application/json": { schema: resolver(MobileGithubRepo.Import.array()) } },
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
1017
|
+
}),
|
|
1018
|
+
async (c) => {
|
|
1019
|
+
return c.json(await MobileGithubRepo.list())
|
|
1020
|
+
},
|
|
1021
|
+
)
|
|
1022
|
+
.post(
|
|
1023
|
+
"/github/oauth/device",
|
|
1024
|
+
describeRoute({
|
|
1025
|
+
summary: "Start GitHub OAuth device flow",
|
|
1026
|
+
description: "Start a GitHub device authorization flow and return the verification code for mobile sign-in.",
|
|
1027
|
+
operationId: "mobile.github.oauth.device.start",
|
|
1028
|
+
responses: {
|
|
1029
|
+
200: {
|
|
1030
|
+
description: "GitHub device flow started",
|
|
1031
|
+
content: { "application/json": { schema: resolver(MobileGithubDeviceAuthStart) } },
|
|
1032
|
+
},
|
|
1033
|
+
...errors(400),
|
|
1034
|
+
},
|
|
1035
|
+
}),
|
|
1036
|
+
async (c) => {
|
|
1037
|
+
try {
|
|
1038
|
+
return c.json(await startGithubDeviceAuth())
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400)
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1043
|
+
)
|
|
1044
|
+
.post(
|
|
1045
|
+
"/github/oauth/device/poll",
|
|
1046
|
+
describeRoute({
|
|
1047
|
+
summary: "Poll GitHub OAuth device flow",
|
|
1048
|
+
description: "Poll GitHub device authorization status and persist the approved token on the host.",
|
|
1049
|
+
operationId: "mobile.github.oauth.device.poll",
|
|
1050
|
+
responses: {
|
|
1051
|
+
200: {
|
|
1052
|
+
description: "GitHub device flow status",
|
|
1053
|
+
content: { "application/json": { schema: resolver(MobileGithubDeviceAuthPollResult) } },
|
|
1054
|
+
},
|
|
1055
|
+
...errors(400),
|
|
1056
|
+
},
|
|
1057
|
+
}),
|
|
1058
|
+
validator("json", MobileGithubDeviceAuthPollInput),
|
|
1059
|
+
async (c) => {
|
|
1060
|
+
try {
|
|
1061
|
+
return c.json(await pollGithubDeviceAuth(c.req.valid("json").deviceCode))
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
return c.json({ error: error instanceof Error ? error.message : String(error) }, 400)
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
)
|
|
1067
|
+
.post(
|
|
1068
|
+
"/github/auth",
|
|
1069
|
+
describeRoute({
|
|
1070
|
+
summary: "Store GitHub token for mobile",
|
|
1071
|
+
description: "Persist a GitHub token on the Nikcli host for mobile repo access.",
|
|
1072
|
+
operationId: "mobile.github.auth.set",
|
|
1073
|
+
responses: {
|
|
1074
|
+
200: {
|
|
1075
|
+
description: "GitHub auth status",
|
|
1076
|
+
content: { "application/json": { schema: resolver(z.object({ success: z.literal(true) })) } },
|
|
1077
|
+
},
|
|
1078
|
+
...errors(400),
|
|
1079
|
+
},
|
|
1080
|
+
}),
|
|
1081
|
+
validator("json", GithubAuthInput),
|
|
1082
|
+
async (c) => {
|
|
1083
|
+
const payload = c.req.valid("json")
|
|
1084
|
+
await ConnectorAuth.set("github", { token: payload.token })
|
|
1085
|
+
Connectors.invalidateConnector("github")
|
|
1086
|
+
return c.json({ success: true as const })
|
|
1087
|
+
},
|
|
1088
|
+
)
|
|
1089
|
+
.delete(
|
|
1090
|
+
"/github/auth",
|
|
1091
|
+
describeRoute({
|
|
1092
|
+
summary: "Remove stored GitHub token for mobile",
|
|
1093
|
+
description: "Delete the mobile GitHub token from the Nikcli host.",
|
|
1094
|
+
operationId: "mobile.github.auth.remove",
|
|
1095
|
+
responses: {
|
|
1096
|
+
200: {
|
|
1097
|
+
description: "GitHub auth removed",
|
|
1098
|
+
content: { "application/json": { schema: resolver(z.object({ success: z.literal(true) })) } },
|
|
1099
|
+
},
|
|
1100
|
+
},
|
|
1101
|
+
}),
|
|
1102
|
+
async (c) => {
|
|
1103
|
+
await ConnectorAuth.remove("github")
|
|
1104
|
+
Connectors.invalidateConnector("github")
|
|
1105
|
+
return c.json({ success: true as const })
|
|
1106
|
+
},
|
|
1107
|
+
)
|
|
1108
|
+
.post(
|
|
1109
|
+
"/github/import",
|
|
1110
|
+
describeRoute({
|
|
1111
|
+
summary: "Import GitHub repo into Nikcli host",
|
|
1112
|
+
description: "Clone or refresh a repository from the connected GitHub account into the managed host cache.",
|
|
1113
|
+
operationId: "mobile.github.import",
|
|
1114
|
+
responses: {
|
|
1115
|
+
200: {
|
|
1116
|
+
description: "Imported repository",
|
|
1117
|
+
content: {
|
|
1118
|
+
"application/json": {
|
|
1119
|
+
schema: resolver(
|
|
1120
|
+
z.object({
|
|
1121
|
+
import: MobileGithubRepo.Import,
|
|
1122
|
+
project: Project.Info,
|
|
1123
|
+
}),
|
|
1124
|
+
),
|
|
1125
|
+
},
|
|
1126
|
+
},
|
|
1127
|
+
},
|
|
1128
|
+
...errors(400, 401),
|
|
1129
|
+
},
|
|
1130
|
+
}),
|
|
1131
|
+
validator("json", MobileGithubRepo.ImportRequest),
|
|
1132
|
+
async (c) => {
|
|
1133
|
+
const token = await githubToken()
|
|
1134
|
+
if (!token) return c.json({ error: "GitHub token not configured" }, 401)
|
|
1135
|
+
const result = await MobileGithubRepo.importRepo(c.req.valid("json"), token)
|
|
1136
|
+
return c.json(result)
|
|
1137
|
+
},
|
|
1138
|
+
)
|
|
1139
|
+
.post(
|
|
1140
|
+
"/github/session",
|
|
1141
|
+
describeRoute({
|
|
1142
|
+
summary: "Create GitHub-backed mobile session",
|
|
1143
|
+
description:
|
|
1144
|
+
"Import a GitHub repo if needed, create an isolated worktree from a base branch, and start a session there.",
|
|
1145
|
+
operationId: "mobile.github.session.create",
|
|
1146
|
+
responses: {
|
|
1147
|
+
200: {
|
|
1148
|
+
description: "GitHub mobile session",
|
|
1149
|
+
content: { "application/json": { schema: resolver(MobileGithubSessionCreateResult) } },
|
|
1150
|
+
},
|
|
1151
|
+
...errors(400, 401),
|
|
1152
|
+
},
|
|
1153
|
+
}),
|
|
1154
|
+
validator("json", MobileGithubSessionCreateInput),
|
|
1155
|
+
async (c) => {
|
|
1156
|
+
const token = await githubToken()
|
|
1157
|
+
if (!token) return c.json({ error: "GitHub token not configured" }, 401)
|
|
1158
|
+
|
|
1159
|
+
const body = c.req.valid("json")
|
|
1160
|
+
const baseBranch = body.baseBranch.trim() || body.defaultBranch
|
|
1161
|
+
const imported = await MobileGithubRepo.importRepo(
|
|
1162
|
+
{
|
|
1163
|
+
owner: body.owner,
|
|
1164
|
+
repo: body.repo,
|
|
1165
|
+
cloneUrl: body.cloneUrl,
|
|
1166
|
+
defaultBranch: body.defaultBranch,
|
|
1167
|
+
private: body.private,
|
|
1168
|
+
},
|
|
1169
|
+
token,
|
|
1170
|
+
)
|
|
1171
|
+
|
|
1172
|
+
const seed = sessionSeed()
|
|
1173
|
+
const headBranch = `nikcli/mobile/${slug(body.repo)}/${seed}`
|
|
1174
|
+
let workspace: Workspace.Info | undefined
|
|
1175
|
+
const worktree = await Instance.provide({
|
|
1176
|
+
directory: imported.import.directory,
|
|
1177
|
+
async fn() {
|
|
1178
|
+
return Worktree.create({
|
|
1179
|
+
name: `${slug(body.repo)}-${slug(baseBranch)}-${seed}`,
|
|
1180
|
+
branch: headBranch,
|
|
1181
|
+
baseBranch,
|
|
1182
|
+
remote: "origin",
|
|
1183
|
+
})
|
|
1184
|
+
},
|
|
1185
|
+
})
|
|
1186
|
+
|
|
1187
|
+
try {
|
|
1188
|
+
workspace = await createExecutionWorkspace({
|
|
1189
|
+
directory: worktree.directory,
|
|
1190
|
+
branch: headBranch,
|
|
1191
|
+
target: body.executionTarget,
|
|
1192
|
+
})
|
|
1193
|
+
|
|
1194
|
+
const session = await Instance.provide({
|
|
1195
|
+
directory: worktree.directory,
|
|
1196
|
+
async fn() {
|
|
1197
|
+
return WorkspaceContext.provide({
|
|
1198
|
+
workspaceID: workspace?.id,
|
|
1199
|
+
async fn() {
|
|
1200
|
+
return Session.create({
|
|
1201
|
+
title: body.title?.trim() || `${body.owner}/${body.repo} ${baseBranch}`,
|
|
1202
|
+
workspaceID: workspace?.id,
|
|
1203
|
+
github: {
|
|
1204
|
+
owner: body.owner,
|
|
1205
|
+
repo: body.repo,
|
|
1206
|
+
fullName: `${body.owner}/${body.repo}`,
|
|
1207
|
+
baseBranch,
|
|
1208
|
+
headBranch,
|
|
1209
|
+
repositoryDirectory: imported.import.directory,
|
|
1210
|
+
cloneUrl: body.cloneUrl,
|
|
1211
|
+
htmlUrl: body.htmlUrl,
|
|
1212
|
+
private: body.private,
|
|
1213
|
+
worktree,
|
|
1214
|
+
},
|
|
1215
|
+
})
|
|
1216
|
+
},
|
|
1217
|
+
})
|
|
1218
|
+
},
|
|
1219
|
+
})
|
|
1220
|
+
|
|
1221
|
+
return c.json({ session, worktree, project: imported.project, workspace })
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
if (workspace) {
|
|
1224
|
+
await Workspace.remove(workspace.id).catch(() => undefined)
|
|
1225
|
+
}
|
|
1226
|
+
await Instance.provide({
|
|
1227
|
+
directory: imported.import.directory,
|
|
1228
|
+
async fn() {
|
|
1229
|
+
await Worktree.remove({ directory: worktree.directory }).catch(() => undefined)
|
|
1230
|
+
},
|
|
1231
|
+
}).catch(() => undefined)
|
|
1232
|
+
throw error
|
|
1233
|
+
}
|
|
1234
|
+
},
|
|
1235
|
+
)
|
|
1236
|
+
.get(
|
|
1237
|
+
"/session",
|
|
1238
|
+
describeRoute({
|
|
1239
|
+
summary: "List mobile sessions",
|
|
1240
|
+
description: "Return mobile-friendly session summaries with current status.",
|
|
1241
|
+
operationId: "mobile.session.list",
|
|
1242
|
+
responses: {
|
|
1243
|
+
200: {
|
|
1244
|
+
description: "Sessions",
|
|
1245
|
+
content: { "application/json": { schema: resolver(MobileSessionSummary.array()) } },
|
|
1246
|
+
},
|
|
1247
|
+
},
|
|
1248
|
+
}),
|
|
1249
|
+
validator(
|
|
1250
|
+
"query",
|
|
1251
|
+
z.object({
|
|
1252
|
+
limit: z.coerce.number().optional(),
|
|
1253
|
+
search: z.string().optional(),
|
|
1254
|
+
}),
|
|
1255
|
+
),
|
|
1256
|
+
async (c) => {
|
|
1257
|
+
const query = c.req.valid("query")
|
|
1258
|
+
const term = query.search?.toLowerCase()
|
|
1259
|
+
const sessions: z.infer<typeof MobileSessionSummary>[] = []
|
|
1260
|
+
// List sessions across all projects for mobile (cross-project view)
|
|
1261
|
+
const allKeys = await Storage.list(["session"])
|
|
1262
|
+
const seen = new Set<string>()
|
|
1263
|
+
for (const key of allKeys) {
|
|
1264
|
+
if (key.length !== 3) continue
|
|
1265
|
+
const sessionID = key[2]
|
|
1266
|
+
if (seen.has(sessionID)) continue
|
|
1267
|
+
seen.add(sessionID)
|
|
1268
|
+
try {
|
|
1269
|
+
const session = await Storage.read<Session.Info>(key)
|
|
1270
|
+
if (term) {
|
|
1271
|
+
const haystack = [
|
|
1272
|
+
session.title,
|
|
1273
|
+
session.github?.fullName,
|
|
1274
|
+
session.github?.baseBranch,
|
|
1275
|
+
session.github?.headBranch,
|
|
1276
|
+
]
|
|
1277
|
+
.filter(Boolean)
|
|
1278
|
+
.join(" ")
|
|
1279
|
+
.toLowerCase()
|
|
1280
|
+
if (!haystack.includes(term)) continue
|
|
1281
|
+
}
|
|
1282
|
+
sessions.push({ info: session, status: await statusForSession(session) })
|
|
1283
|
+
} catch {
|
|
1284
|
+
continue
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
// Sort by most recently updated, then apply limit
|
|
1288
|
+
sessions.sort((a, b) => b.info.time.updated - a.info.time.updated)
|
|
1289
|
+
return c.json(query.limit ? sessions.slice(0, query.limit) : sessions)
|
|
1290
|
+
},
|
|
1291
|
+
)
|
|
1292
|
+
.post(
|
|
1293
|
+
"/session",
|
|
1294
|
+
describeRoute({
|
|
1295
|
+
summary: "Create mobile session",
|
|
1296
|
+
description: "Create a new session for the mobile app.",
|
|
1297
|
+
operationId: "mobile.session.create",
|
|
1298
|
+
responses: {
|
|
1299
|
+
200: {
|
|
1300
|
+
description: "Created session",
|
|
1301
|
+
content: { "application/json": { schema: resolver(Session.Info) } },
|
|
1302
|
+
},
|
|
1303
|
+
...errors(400),
|
|
1304
|
+
},
|
|
1305
|
+
}),
|
|
1306
|
+
validator("json", MobileSessionCreateInput),
|
|
1307
|
+
async (c) => {
|
|
1308
|
+
const body = c.req.valid("json") as Record<string, unknown> | undefined
|
|
1309
|
+
const executionTarget = body?.executionTarget === "container" ? "container" : "local"
|
|
1310
|
+
let workspace: Workspace.Info | undefined
|
|
1311
|
+
const sessionInput = body
|
|
1312
|
+
? {
|
|
1313
|
+
parentID: typeof body.parentID === "string" ? body.parentID : undefined,
|
|
1314
|
+
title: typeof body.title === "string" ? body.title : undefined,
|
|
1315
|
+
permission: body.permission as Session.Info["permission"],
|
|
1316
|
+
github: body.github as Session.Info["github"],
|
|
1317
|
+
}
|
|
1318
|
+
: undefined
|
|
1319
|
+
|
|
1320
|
+
try {
|
|
1321
|
+
workspace = await createExecutionWorkspace({
|
|
1322
|
+
directory: Instance.directory,
|
|
1323
|
+
target: executionTarget,
|
|
1324
|
+
})
|
|
1325
|
+
const session = await WorkspaceContext.provide({
|
|
1326
|
+
workspaceID: workspace?.id,
|
|
1327
|
+
async fn() {
|
|
1328
|
+
return Session.create(
|
|
1329
|
+
workspace?.id
|
|
1330
|
+
? {
|
|
1331
|
+
...sessionInput,
|
|
1332
|
+
workspaceID: workspace.id,
|
|
1333
|
+
}
|
|
1334
|
+
: sessionInput,
|
|
1335
|
+
)
|
|
1336
|
+
},
|
|
1337
|
+
})
|
|
1338
|
+
return c.json(session)
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
if (workspace) {
|
|
1341
|
+
await Workspace.remove(workspace.id).catch(() => undefined)
|
|
1342
|
+
}
|
|
1343
|
+
throw error
|
|
1344
|
+
}
|
|
1345
|
+
},
|
|
1346
|
+
)
|
|
1347
|
+
.get(
|
|
1348
|
+
"/session/:sessionID",
|
|
1349
|
+
describeRoute({
|
|
1350
|
+
summary: "Get mobile session detail",
|
|
1351
|
+
description: "Return a session, its messages, status, and pending permissions.",
|
|
1352
|
+
operationId: "mobile.session.detail",
|
|
1353
|
+
responses: {
|
|
1354
|
+
200: {
|
|
1355
|
+
description: "Session detail",
|
|
1356
|
+
content: { "application/json": { schema: resolver(MobileSessionDetail) } },
|
|
1357
|
+
},
|
|
1358
|
+
...errors(404),
|
|
1359
|
+
},
|
|
1360
|
+
}),
|
|
1361
|
+
validator("param", z.object({ sessionID: z.string() })),
|
|
1362
|
+
async (c) => {
|
|
1363
|
+
const sessionID = c.req.valid("param").sessionID
|
|
1364
|
+
const info = await Session.getAnyProject(sessionID)
|
|
1365
|
+
const { messages, permissions, status } = await Instance.provide({
|
|
1366
|
+
directory: info.directory,
|
|
1367
|
+
async fn() {
|
|
1368
|
+
const [messages, permissions] = await Promise.all([
|
|
1369
|
+
Session.messages({ sessionID }),
|
|
1370
|
+
PermissionNext.list().then((items) => items.filter((item) => item.sessionID === sessionID)),
|
|
1371
|
+
])
|
|
1372
|
+
return { messages, permissions, status: SessionStatus.get(sessionID) }
|
|
1373
|
+
},
|
|
1374
|
+
})
|
|
1375
|
+
return c.json({
|
|
1376
|
+
info,
|
|
1377
|
+
status,
|
|
1378
|
+
messages,
|
|
1379
|
+
permissions,
|
|
1380
|
+
})
|
|
1381
|
+
},
|
|
1382
|
+
)
|
|
1383
|
+
.get(
|
|
1384
|
+
"/session/:sessionID/diff/:messageID",
|
|
1385
|
+
describeRoute({
|
|
1386
|
+
summary: "Get session diff for mobile",
|
|
1387
|
+
description: "Return file diffs for a specific message in a session.",
|
|
1388
|
+
operationId: "mobile.session.diff",
|
|
1389
|
+
responses: {
|
|
1390
|
+
200: {
|
|
1391
|
+
description: "Message diff",
|
|
1392
|
+
content: { "application/json": { schema: resolver(Snapshot.FileDiff.array()) } },
|
|
1393
|
+
},
|
|
1394
|
+
},
|
|
1395
|
+
}),
|
|
1396
|
+
validator("param", z.object({ sessionID: z.string(), messageID: z.string() })),
|
|
1397
|
+
async (c) => {
|
|
1398
|
+
const params = c.req.valid("param")
|
|
1399
|
+
const session = await Session.getAnyProject(params.sessionID)
|
|
1400
|
+
const result = await Instance.provide({
|
|
1401
|
+
directory: session.directory,
|
|
1402
|
+
async fn() {
|
|
1403
|
+
return SessionSummary.diff({ sessionID: params.sessionID, messageID: params.messageID })
|
|
1404
|
+
},
|
|
1405
|
+
})
|
|
1406
|
+
return c.json(result)
|
|
1407
|
+
},
|
|
1408
|
+
)
|
|
1409
|
+
.get(
|
|
1410
|
+
"/session/:sessionID/command",
|
|
1411
|
+
describeRoute({
|
|
1412
|
+
summary: "List session commands for mobile",
|
|
1413
|
+
description: "Return command metadata resolved in the current session context for mobile slash autocomplete.",
|
|
1414
|
+
operationId: "mobile.session.command.list",
|
|
1415
|
+
responses: {
|
|
1416
|
+
200: {
|
|
1417
|
+
description: "Commands",
|
|
1418
|
+
content: { "application/json": { schema: resolver(MobileCommand.array()) } },
|
|
1419
|
+
},
|
|
1420
|
+
...errors(404),
|
|
1421
|
+
},
|
|
1422
|
+
}),
|
|
1423
|
+
validator("param", z.object({ sessionID: z.string() })),
|
|
1424
|
+
async (c) => {
|
|
1425
|
+
const sessionID = c.req.valid("param").sessionID
|
|
1426
|
+
const session = await Session.getAnyProject(sessionID)
|
|
1427
|
+
if (session.workspaceID) {
|
|
1428
|
+
const response = await proxyWorkspaceRequest({
|
|
1429
|
+
workspaceID: session.workspaceID,
|
|
1430
|
+
method: "GET",
|
|
1431
|
+
url: "/command",
|
|
1432
|
+
signal: c.req.raw.signal,
|
|
1433
|
+
})
|
|
1434
|
+
|
|
1435
|
+
if (response) {
|
|
1436
|
+
if (!response.ok) {
|
|
1437
|
+
return new Response(response.body, {
|
|
1438
|
+
status: response.status,
|
|
1439
|
+
headers: toHeadersObject(response.headers),
|
|
1440
|
+
})
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
const commands = (await response.json().catch(() => [])) as Array<Record<string, unknown>>
|
|
1444
|
+
return c.json(
|
|
1445
|
+
commands
|
|
1446
|
+
.map((command) => ({
|
|
1447
|
+
name: typeof command.name === "string" ? command.name : "unknown",
|
|
1448
|
+
description: typeof command.description === "string" ? command.description : undefined,
|
|
1449
|
+
agent: typeof command.agent === "string" ? command.agent : undefined,
|
|
1450
|
+
model: typeof command.model === "string" ? command.model : undefined,
|
|
1451
|
+
mcp: typeof command.mcp === "boolean" ? command.mcp : undefined,
|
|
1452
|
+
skill: typeof command.skill === "boolean" ? command.skill : undefined,
|
|
1453
|
+
subtask: typeof command.subtask === "boolean" ? command.subtask : undefined,
|
|
1454
|
+
hints: Array.isArray(command.hints)
|
|
1455
|
+
? command.hints.filter((hint): hint is string => typeof hint === "string")
|
|
1456
|
+
: [],
|
|
1457
|
+
}))
|
|
1458
|
+
.filter((command) => command.name !== "unknown")
|
|
1459
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
1460
|
+
)
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
const commands = await Instance.provide({
|
|
1465
|
+
directory: session.directory,
|
|
1466
|
+
async fn() {
|
|
1467
|
+
return WorkspaceContext.provide({
|
|
1468
|
+
workspaceID: session.workspaceID,
|
|
1469
|
+
async fn() {
|
|
1470
|
+
return Command.list()
|
|
1471
|
+
},
|
|
1472
|
+
})
|
|
1473
|
+
},
|
|
1474
|
+
})
|
|
1475
|
+
return c.json(
|
|
1476
|
+
commands
|
|
1477
|
+
.map((command) => ({
|
|
1478
|
+
name: command.name,
|
|
1479
|
+
description: command.description,
|
|
1480
|
+
agent: command.agent,
|
|
1481
|
+
model: command.model,
|
|
1482
|
+
mcp: command.mcp,
|
|
1483
|
+
skill: command.skill,
|
|
1484
|
+
subtask: command.subtask,
|
|
1485
|
+
hints: command.hints,
|
|
1486
|
+
}))
|
|
1487
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
1488
|
+
)
|
|
1489
|
+
},
|
|
1490
|
+
)
|
|
1491
|
+
.post(
|
|
1492
|
+
"/session/:sessionID/message",
|
|
1493
|
+
describeRoute({
|
|
1494
|
+
summary: "Send mobile session message",
|
|
1495
|
+
description: "Queue a message for a session and rely on the session stream for realtime updates.",
|
|
1496
|
+
operationId: "mobile.session.message",
|
|
1497
|
+
responses: {
|
|
1498
|
+
202: {
|
|
1499
|
+
description: "Message accepted",
|
|
1500
|
+
content: { "application/json": { schema: resolver(z.object({ accepted: z.literal(true) })) } },
|
|
1501
|
+
},
|
|
1502
|
+
...errors(400, 404),
|
|
1503
|
+
},
|
|
1504
|
+
}),
|
|
1505
|
+
validator("param", z.object({ sessionID: z.string() })),
|
|
1506
|
+
validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
|
|
1507
|
+
async (c) => {
|
|
1508
|
+
const sessionID = c.req.valid("param").sessionID
|
|
1509
|
+
const body = c.req.valid("json")
|
|
1510
|
+
const session = await Session.getAnyProject(sessionID)
|
|
1511
|
+
if (session.github?.worktree.cleanedAt) {
|
|
1512
|
+
return c.json({ error: "Session worktree has been cleaned up" }, 400)
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
const defaults = !body.agent || !body.model ? await resolveMobilePromptDefaults(session) : undefined
|
|
1516
|
+
const promptBody = {
|
|
1517
|
+
...body,
|
|
1518
|
+
agent: body.agent ?? defaults?.agent,
|
|
1519
|
+
model: body.model ?? defaults?.model,
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
if (session.workspaceID) {
|
|
1523
|
+
const response = await proxyWorkspaceRequest({
|
|
1524
|
+
workspaceID: session.workspaceID,
|
|
1525
|
+
method: "POST",
|
|
1526
|
+
url: `/session/${encodeURIComponent(sessionID)}/prompt_async`,
|
|
1527
|
+
body: JSON.stringify(promptBody),
|
|
1528
|
+
headers: {
|
|
1529
|
+
"content-type": "application/json",
|
|
1530
|
+
},
|
|
1531
|
+
signal: c.req.raw.signal,
|
|
1532
|
+
})
|
|
1533
|
+
|
|
1534
|
+
if (response) {
|
|
1535
|
+
if (!response.ok) {
|
|
1536
|
+
return new Response(response.body, {
|
|
1537
|
+
status: response.status,
|
|
1538
|
+
headers: toHeadersObject(response.headers),
|
|
1539
|
+
})
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
return c.json({ accepted: true as const }, 202)
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
void Instance.provide({
|
|
1547
|
+
directory: session.directory,
|
|
1548
|
+
async fn() {
|
|
1549
|
+
return SessionPrompt.prompt({
|
|
1550
|
+
...promptBody,
|
|
1551
|
+
sessionID,
|
|
1552
|
+
})
|
|
1553
|
+
},
|
|
1554
|
+
}).catch((error) => {
|
|
1555
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1556
|
+
SessionStatus.set(sessionID, { type: "idle" })
|
|
1557
|
+
Bus.publish(Session.Event.Error, {
|
|
1558
|
+
sessionID,
|
|
1559
|
+
error: new NamedError.Unknown({ message }).toObject(),
|
|
1560
|
+
})
|
|
1561
|
+
log.error("mobile session prompt failed", {
|
|
1562
|
+
sessionID,
|
|
1563
|
+
error: message,
|
|
1564
|
+
})
|
|
1565
|
+
})
|
|
1566
|
+
return c.json({ accepted: true as const }, 202)
|
|
1567
|
+
},
|
|
1568
|
+
)
|
|
1569
|
+
.post(
|
|
1570
|
+
"/session/:sessionID/command",
|
|
1571
|
+
describeRoute({
|
|
1572
|
+
summary: "Run mobile session command",
|
|
1573
|
+
description: "Execute a slash-style command against the current session.",
|
|
1574
|
+
operationId: "mobile.session.command",
|
|
1575
|
+
responses: {
|
|
1576
|
+
200: {
|
|
1577
|
+
description: "Command result",
|
|
1578
|
+
content: { "application/json": { schema: resolver(MessageV2.WithParts) } },
|
|
1579
|
+
},
|
|
1580
|
+
...errors(400, 404),
|
|
1581
|
+
},
|
|
1582
|
+
}),
|
|
1583
|
+
validator("param", z.object({ sessionID: z.string() })),
|
|
1584
|
+
validator("json", MobileSessionCommandInput),
|
|
1585
|
+
async (c) => {
|
|
1586
|
+
const sessionID = c.req.valid("param").sessionID
|
|
1587
|
+
const body = c.req.valid("json")
|
|
1588
|
+
const session = await Session.getAnyProject(sessionID)
|
|
1589
|
+
if (session.github?.worktree.cleanedAt) {
|
|
1590
|
+
return c.json({ error: "Session worktree has been cleaned up" }, 400)
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
const commandBody = {
|
|
1594
|
+
command: body.command,
|
|
1595
|
+
arguments: body.arguments,
|
|
1596
|
+
agent: body.agent,
|
|
1597
|
+
model: body.model ? `${body.model.providerID}/${body.model.modelID}` : undefined,
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
if (session.workspaceID) {
|
|
1601
|
+
const response = await proxyWorkspaceRequest({
|
|
1602
|
+
workspaceID: session.workspaceID,
|
|
1603
|
+
method: "POST",
|
|
1604
|
+
url: `/session/${encodeURIComponent(sessionID)}/command`,
|
|
1605
|
+
body: JSON.stringify(commandBody),
|
|
1606
|
+
headers: {
|
|
1607
|
+
"content-type": "application/json",
|
|
1608
|
+
},
|
|
1609
|
+
signal: c.req.raw.signal,
|
|
1610
|
+
})
|
|
1611
|
+
|
|
1612
|
+
if (response) {
|
|
1613
|
+
return new Response(response.body, {
|
|
1614
|
+
status: response.status,
|
|
1615
|
+
headers: toHeadersObject(response.headers),
|
|
1616
|
+
})
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
const result = await Instance.provide({
|
|
1621
|
+
directory: session.directory,
|
|
1622
|
+
async fn() {
|
|
1623
|
+
return SessionPrompt.command({
|
|
1624
|
+
...commandBody,
|
|
1625
|
+
sessionID,
|
|
1626
|
+
})
|
|
1627
|
+
},
|
|
1628
|
+
})
|
|
1629
|
+
|
|
1630
|
+
return c.json(result)
|
|
1631
|
+
},
|
|
1632
|
+
)
|
|
1633
|
+
.post(
|
|
1634
|
+
"/session/:sessionID/abort",
|
|
1635
|
+
describeRoute({
|
|
1636
|
+
summary: "Abort mobile session",
|
|
1637
|
+
description: "Abort the active run for a session.",
|
|
1638
|
+
operationId: "mobile.session.abort",
|
|
1639
|
+
responses: {
|
|
1640
|
+
200: {
|
|
1641
|
+
description: "Session aborted",
|
|
1642
|
+
content: { "application/json": { schema: resolver(z.object({ success: z.literal(true) })) } },
|
|
1643
|
+
},
|
|
1644
|
+
},
|
|
1645
|
+
}),
|
|
1646
|
+
validator("param", z.object({ sessionID: z.string() })),
|
|
1647
|
+
async (c) => {
|
|
1648
|
+
const sessionID = c.req.valid("param").sessionID
|
|
1649
|
+
const session = await Session.getAnyProject(sessionID)
|
|
1650
|
+
if (session.workspaceID) {
|
|
1651
|
+
const response = await proxyWorkspaceRequest({
|
|
1652
|
+
workspaceID: session.workspaceID,
|
|
1653
|
+
method: "POST",
|
|
1654
|
+
url: `/session/${encodeURIComponent(sessionID)}/abort`,
|
|
1655
|
+
signal: c.req.raw.signal,
|
|
1656
|
+
})
|
|
1657
|
+
|
|
1658
|
+
if (response) {
|
|
1659
|
+
if (!response.ok) {
|
|
1660
|
+
return new Response(response.body, {
|
|
1661
|
+
status: response.status,
|
|
1662
|
+
headers: toHeadersObject(response.headers),
|
|
1663
|
+
})
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
return c.json({ success: true as const })
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
SessionPrompt.cancel(sessionID)
|
|
1670
|
+
return c.json({ success: true as const })
|
|
1671
|
+
},
|
|
1672
|
+
)
|
|
1673
|
+
.post(
|
|
1674
|
+
"/session/:sessionID/permissions/:permissionID",
|
|
1675
|
+
describeRoute({
|
|
1676
|
+
summary: "Respond to permission from mobile",
|
|
1677
|
+
description: "Approve, always approve, or reject a pending permission request.",
|
|
1678
|
+
operationId: "mobile.permission.respond",
|
|
1679
|
+
responses: {
|
|
1680
|
+
200: {
|
|
1681
|
+
description: "Permission processed",
|
|
1682
|
+
content: { "application/json": { schema: resolver(z.object({ success: z.literal(true) })) } },
|
|
1683
|
+
},
|
|
1684
|
+
},
|
|
1685
|
+
}),
|
|
1686
|
+
validator("param", z.object({ sessionID: z.string(), permissionID: z.string() })),
|
|
1687
|
+
validator("json", z.object({ response: PermissionNext.Reply })),
|
|
1688
|
+
async (c) => {
|
|
1689
|
+
const params = c.req.valid("param")
|
|
1690
|
+
const session = await Session.getAnyProject(params.sessionID)
|
|
1691
|
+
if (session.workspaceID) {
|
|
1692
|
+
const response = await proxyWorkspaceRequest({
|
|
1693
|
+
workspaceID: session.workspaceID,
|
|
1694
|
+
method: "POST",
|
|
1695
|
+
url: `/session/${encodeURIComponent(params.sessionID)}/permissions/${encodeURIComponent(params.permissionID)}`,
|
|
1696
|
+
body: JSON.stringify({ response: c.req.valid("json").response }),
|
|
1697
|
+
headers: {
|
|
1698
|
+
"content-type": "application/json",
|
|
1699
|
+
},
|
|
1700
|
+
signal: c.req.raw.signal,
|
|
1701
|
+
})
|
|
1702
|
+
|
|
1703
|
+
if (response) {
|
|
1704
|
+
if (!response.ok) {
|
|
1705
|
+
return new Response(response.body, {
|
|
1706
|
+
status: response.status,
|
|
1707
|
+
headers: toHeadersObject(response.headers),
|
|
1708
|
+
})
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
return c.json({ success: true as const })
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
PermissionNext.reply({ requestID: params.permissionID, reply: c.req.valid("json").response })
|
|
1715
|
+
return c.json({ success: true as const })
|
|
1716
|
+
},
|
|
1717
|
+
)
|
|
1718
|
+
.post(
|
|
1719
|
+
"/session/:sessionID/publish",
|
|
1720
|
+
describeRoute({
|
|
1721
|
+
summary: "Publish GitHub session",
|
|
1722
|
+
description: "Commit the current worktree, push the session branch, and create or reuse a pull request.",
|
|
1723
|
+
operationId: "mobile.github.session.publish",
|
|
1724
|
+
responses: {
|
|
1725
|
+
200: {
|
|
1726
|
+
description: "Published pull request",
|
|
1727
|
+
content: { "application/json": { schema: resolver(MobileGithubPublishResult) } },
|
|
1728
|
+
},
|
|
1729
|
+
...errors(400, 401, 404),
|
|
1730
|
+
},
|
|
1731
|
+
}),
|
|
1732
|
+
validator("param", z.object({ sessionID: z.string() })),
|
|
1733
|
+
validator("json", MobileGithubPublishInput),
|
|
1734
|
+
async (c) => {
|
|
1735
|
+
const token = await githubToken()
|
|
1736
|
+
if (!token) return c.json({ error: "GitHub token not configured" }, 401)
|
|
1737
|
+
|
|
1738
|
+
const body = c.req.valid("json") ?? {}
|
|
1739
|
+
const sessionInfo = await Session.getAnyProject(c.req.valid("param").sessionID)
|
|
1740
|
+
if (!sessionInfo.github) return c.json({ error: "Session is not linked to GitHub" }, 400)
|
|
1741
|
+
if (sessionInfo.github.worktree.cleanedAt)
|
|
1742
|
+
return c.json({ error: "Session worktree has already been cleaned" }, 400)
|
|
1743
|
+
|
|
1744
|
+
return Instance.provide({
|
|
1745
|
+
directory: sessionInfo.directory,
|
|
1746
|
+
async fn() {
|
|
1747
|
+
const session = await Session.get(sessionInfo.id)
|
|
1748
|
+
const github = session.github
|
|
1749
|
+
if (!github) return c.json({ error: "Session is not linked to GitHub" }, 400)
|
|
1750
|
+
if (SessionStatus.get(session.id).type !== "idle") {
|
|
1751
|
+
return c.json({ error: "Wait for the session to become idle before publishing" }, 400)
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
await MobileGithubRepo.runGit(["fetch", "origin", github.baseBranch, "--prune"], {
|
|
1755
|
+
cwd: session.directory,
|
|
1756
|
+
token,
|
|
1757
|
+
})
|
|
1758
|
+
|
|
1759
|
+
const dirty = await MobileGithubRepo.runGit(["status", "--porcelain"], {
|
|
1760
|
+
cwd: session.directory,
|
|
1761
|
+
token,
|
|
1762
|
+
})
|
|
1763
|
+
|
|
1764
|
+
if (dirty.trim()) {
|
|
1765
|
+
await MobileGithubRepo.runGit(["add", "-A"], { cwd: session.directory, token })
|
|
1766
|
+
await MobileGithubRepo.runGit(
|
|
1767
|
+
["commit", "-m", body.commitMessage?.trim() || session.title.trim() || `Update ${github.fullName}`],
|
|
1768
|
+
{
|
|
1769
|
+
cwd: session.directory,
|
|
1770
|
+
token,
|
|
1771
|
+
},
|
|
1772
|
+
)
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
await MobileGithubRepo.runGit(["push", "--set-upstream", "origin", github.headBranch], {
|
|
1776
|
+
cwd: session.directory,
|
|
1777
|
+
token,
|
|
1778
|
+
})
|
|
1779
|
+
|
|
1780
|
+
const ahead = await MobileGithubRepo.runGit(
|
|
1781
|
+
["rev-list", "--left-right", "--count", `origin/${github.baseBranch}...HEAD`],
|
|
1782
|
+
{
|
|
1783
|
+
cwd: session.directory,
|
|
1784
|
+
token,
|
|
1785
|
+
},
|
|
1786
|
+
)
|
|
1787
|
+
const [, aheadCountText = "0"] = ahead.trim().split(/\s+/)
|
|
1788
|
+
const aheadCount = Number.parseInt(aheadCountText, 10) || 0
|
|
1789
|
+
|
|
1790
|
+
const commitSha = await MobileGithubRepo.runGit(["rev-parse", "HEAD"], {
|
|
1791
|
+
cwd: session.directory,
|
|
1792
|
+
token,
|
|
1793
|
+
})
|
|
1794
|
+
|
|
1795
|
+
const existingPullRequest =
|
|
1796
|
+
github.pullRequest ||
|
|
1797
|
+
(await GithubApi.findPullRequestByHead(
|
|
1798
|
+
token,
|
|
1799
|
+
github.owner,
|
|
1800
|
+
github.repo,
|
|
1801
|
+
`${github.owner}:${github.headBranch}`,
|
|
1802
|
+
)
|
|
1803
|
+
.then((value) =>
|
|
1804
|
+
value
|
|
1805
|
+
? {
|
|
1806
|
+
number: value.number,
|
|
1807
|
+
url: value.html_url,
|
|
1808
|
+
title: value.title,
|
|
1809
|
+
}
|
|
1810
|
+
: undefined,
|
|
1811
|
+
)
|
|
1812
|
+
.catch(() => undefined))
|
|
1813
|
+
|
|
1814
|
+
const pullRequest =
|
|
1815
|
+
existingPullRequest ||
|
|
1816
|
+
(aheadCount > 0
|
|
1817
|
+
? await GithubApi.createPullRequest(
|
|
1818
|
+
token,
|
|
1819
|
+
github.owner,
|
|
1820
|
+
github.repo,
|
|
1821
|
+
body.title?.trim() || session.title.trim() || `${github.fullName} changes`,
|
|
1822
|
+
github.headBranch,
|
|
1823
|
+
github.baseBranch,
|
|
1824
|
+
body.body?.trim() || defaultPullRequestBody(session),
|
|
1825
|
+
).then((value) => ({
|
|
1826
|
+
number: value.number,
|
|
1827
|
+
url: value.html_url,
|
|
1828
|
+
title: value.title,
|
|
1829
|
+
}))
|
|
1830
|
+
: undefined)
|
|
1831
|
+
|
|
1832
|
+
if (!pullRequest) {
|
|
1833
|
+
return c.json({ error: "Create changes in the worktree before publishing a pull request" }, 400)
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
await Session.update(session.id, (draft) => {
|
|
1837
|
+
if (!draft.github) return
|
|
1838
|
+
draft.github.pullRequest = pullRequest
|
|
1839
|
+
draft.github.lastCommitSha = commitSha.trim()
|
|
1840
|
+
draft.github.publishedAt = Date.now()
|
|
1841
|
+
draft.github.publishError = undefined
|
|
1842
|
+
})
|
|
1843
|
+
|
|
1844
|
+
return c.json({
|
|
1845
|
+
commitSha: commitSha.trim(),
|
|
1846
|
+
branch: github.headBranch,
|
|
1847
|
+
pullRequest,
|
|
1848
|
+
})
|
|
1849
|
+
},
|
|
1850
|
+
})
|
|
1851
|
+
},
|
|
1852
|
+
)
|
|
1853
|
+
.post(
|
|
1854
|
+
"/session/:sessionID/cleanup",
|
|
1855
|
+
describeRoute({
|
|
1856
|
+
summary: "Cleanup GitHub session worktree",
|
|
1857
|
+
description: "Remove the isolated worktree created for a GitHub-backed mobile session.",
|
|
1858
|
+
operationId: "mobile.github.session.cleanup",
|
|
1859
|
+
responses: {
|
|
1860
|
+
200: {
|
|
1861
|
+
description: "Worktree cleaned",
|
|
1862
|
+
content: {
|
|
1863
|
+
"application/json": { schema: resolver(z.object({ success: z.literal(true) })) },
|
|
1864
|
+
},
|
|
1865
|
+
},
|
|
1866
|
+
...errors(400, 404),
|
|
1867
|
+
},
|
|
1868
|
+
}),
|
|
1869
|
+
validator("param", z.object({ sessionID: z.string() })),
|
|
1870
|
+
async (c) => {
|
|
1871
|
+
const sessionInfo = await Session.getAnyProject(c.req.valid("param").sessionID)
|
|
1872
|
+
if (!sessionInfo.github) return c.json({ error: "Session is not linked to GitHub" }, 400)
|
|
1873
|
+
if (sessionInfo.github.worktree.cleanedAt) return c.json({ success: true as const })
|
|
1874
|
+
|
|
1875
|
+
const repositoryDirectory = sessionInfo.github.repositoryDirectory || sessionInfo.github.worktree.directory
|
|
1876
|
+
const idle = await Instance.provide({
|
|
1877
|
+
directory: sessionInfo.directory,
|
|
1878
|
+
async fn() {
|
|
1879
|
+
return SessionStatus.get(sessionInfo.id).type === "idle"
|
|
1880
|
+
},
|
|
1881
|
+
})
|
|
1882
|
+
if (!idle) {
|
|
1883
|
+
return c.json({ error: "Wait for the session to become idle before cleaning up the worktree" }, 400)
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
await Instance.provide({
|
|
1887
|
+
directory: repositoryDirectory,
|
|
1888
|
+
async fn() {
|
|
1889
|
+
if (sessionInfo.workspaceID) {
|
|
1890
|
+
await Workspace.remove(sessionInfo.workspaceID).catch(() => undefined)
|
|
1891
|
+
}
|
|
1892
|
+
await Worktree.remove({ directory: sessionInfo.github!.worktree.directory })
|
|
1893
|
+
},
|
|
1894
|
+
})
|
|
1895
|
+
|
|
1896
|
+
await Instance.provide({
|
|
1897
|
+
directory: repositoryDirectory,
|
|
1898
|
+
async fn() {
|
|
1899
|
+
await Session.update(sessionInfo.id, (draft) => {
|
|
1900
|
+
if (!draft.github) return
|
|
1901
|
+
draft.github.worktree.cleanedAt = Date.now()
|
|
1902
|
+
})
|
|
1903
|
+
},
|
|
1904
|
+
})
|
|
1905
|
+
|
|
1906
|
+
return c.json({ success: true as const })
|
|
1907
|
+
},
|
|
1908
|
+
)
|
|
1909
|
+
.get(
|
|
1910
|
+
"/session/:sessionID/stream",
|
|
1911
|
+
describeRoute({
|
|
1912
|
+
summary: "Stream mobile session events",
|
|
1913
|
+
description: "Subscribe to session-scoped realtime events for the mobile chat UI.",
|
|
1914
|
+
operationId: "mobile.session.stream",
|
|
1915
|
+
responses: {
|
|
1916
|
+
200: {
|
|
1917
|
+
description: "Session event stream",
|
|
1918
|
+
content: { "text/event-stream": { schema: resolver(z.any()) } },
|
|
1919
|
+
},
|
|
1920
|
+
},
|
|
1921
|
+
}),
|
|
1922
|
+
validator("param", z.object({ sessionID: z.string() })),
|
|
1923
|
+
async (c) => {
|
|
1924
|
+
const sessionID = c.req.valid("param").sessionID
|
|
1925
|
+
return streamSSE(c, async (stream) => {
|
|
1926
|
+
await stream.writeSSE({
|
|
1927
|
+
data: JSON.stringify({ type: "server.connected", properties: { sessionID } }),
|
|
1928
|
+
})
|
|
1929
|
+
|
|
1930
|
+
const onEvent = async (event: any) => {
|
|
1931
|
+
const payload = event?.payload
|
|
1932
|
+
if (!payload?.type) return
|
|
1933
|
+
const ids = extractSessionIDs(payload.properties)
|
|
1934
|
+
if (!ids.includes(sessionID)) return
|
|
1935
|
+
await stream.writeSSE({
|
|
1936
|
+
data: JSON.stringify(payload),
|
|
1937
|
+
})
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
GlobalBus.on("event", onEvent)
|
|
1941
|
+
|
|
1942
|
+
const heartbeat = setInterval(() => {
|
|
1943
|
+
void stream.writeSSE({
|
|
1944
|
+
data: JSON.stringify({ type: "server.heartbeat", properties: { sessionID } }),
|
|
1945
|
+
})
|
|
1946
|
+
}, 30000)
|
|
1947
|
+
|
|
1948
|
+
await new Promise<void>((resolve) => {
|
|
1949
|
+
stream.onAbort(() => {
|
|
1950
|
+
clearInterval(heartbeat)
|
|
1951
|
+
GlobalBus.off("event", onEvent)
|
|
1952
|
+
resolve()
|
|
1953
|
+
})
|
|
1954
|
+
})
|
|
1955
|
+
})
|
|
1956
|
+
},
|
|
1957
|
+
)
|
|
1958
|
+
.post(
|
|
1959
|
+
"/worktree",
|
|
1960
|
+
describeRoute({
|
|
1961
|
+
summary: "Create mobile worktree",
|
|
1962
|
+
description: "Create a git worktree for sandboxed mobile work.",
|
|
1963
|
+
operationId: "mobile.worktree.create",
|
|
1964
|
+
responses: {
|
|
1965
|
+
200: {
|
|
1966
|
+
description: "Worktree created",
|
|
1967
|
+
content: { "application/json": { schema: resolver(Worktree.Info) } },
|
|
1968
|
+
},
|
|
1969
|
+
...errors(400),
|
|
1970
|
+
},
|
|
1971
|
+
}),
|
|
1972
|
+
validator("json", Worktree.CreateInput.optional()),
|
|
1973
|
+
async (c) => {
|
|
1974
|
+
const worktree = await Worktree.create(c.req.valid("json") ?? undefined)
|
|
1975
|
+
return c.json(worktree)
|
|
1976
|
+
},
|
|
1977
|
+
)
|
|
1978
|
+
.post(
|
|
1979
|
+
"/worktree/reset",
|
|
1980
|
+
describeRoute({
|
|
1981
|
+
summary: "Reset mobile worktree",
|
|
1982
|
+
description: "Reset a worktree back to the default branch state.",
|
|
1983
|
+
operationId: "mobile.worktree.reset",
|
|
1984
|
+
responses: {
|
|
1985
|
+
200: {
|
|
1986
|
+
description: "Worktree reset",
|
|
1987
|
+
content: { "application/json": { schema: resolver(z.object({ success: z.literal(true) })) } },
|
|
1988
|
+
},
|
|
1989
|
+
},
|
|
1990
|
+
}),
|
|
1991
|
+
validator("json", Worktree.ResetInput),
|
|
1992
|
+
async (c) => {
|
|
1993
|
+
await Worktree.reset(c.req.valid("json"))
|
|
1994
|
+
return c.json({ success: true as const })
|
|
1995
|
+
},
|
|
1996
|
+
)
|
|
1997
|
+
.post(
|
|
1998
|
+
"/session/:sessionID/rename",
|
|
1999
|
+
describeRoute({
|
|
2000
|
+
summary: "Rename a session",
|
|
2001
|
+
description: "Update the title of an existing session.",
|
|
2002
|
+
operationId: "mobile.session.rename",
|
|
2003
|
+
responses: {
|
|
2004
|
+
200: {
|
|
2005
|
+
description: "Session renamed",
|
|
2006
|
+
content: { "application/json": { schema: resolver(z.object({ success: z.literal(true) })) } },
|
|
2007
|
+
},
|
|
2008
|
+
},
|
|
2009
|
+
}),
|
|
2010
|
+
validator("param", z.object({ sessionID: z.string() })),
|
|
2011
|
+
validator("json", z.object({ title: z.string().min(1) })),
|
|
2012
|
+
async (c) => {
|
|
2013
|
+
const { sessionID } = c.req.valid("param")
|
|
2014
|
+
const { title } = c.req.valid("json")
|
|
2015
|
+
const session = await Session.get(sessionID)
|
|
2016
|
+
if (!session) return c.json({ error: "not found" }, 404)
|
|
2017
|
+
await Session.update(sessionID, (draft) => {
|
|
2018
|
+
draft.title = title.trim()
|
|
2019
|
+
})
|
|
2020
|
+
return c.json({ success: true as const })
|
|
2021
|
+
},
|
|
2022
|
+
)
|
|
2023
|
+
.delete(
|
|
2024
|
+
"/worktree",
|
|
2025
|
+
describeRoute({
|
|
2026
|
+
summary: "Remove mobile worktree",
|
|
2027
|
+
description: "Remove an existing worktree sandbox.",
|
|
2028
|
+
operationId: "mobile.worktree.remove",
|
|
2029
|
+
responses: {
|
|
2030
|
+
200: {
|
|
2031
|
+
description: "Worktree removed",
|
|
2032
|
+
content: { "application/json": { schema: resolver(z.object({ success: z.literal(true) })) } },
|
|
2033
|
+
},
|
|
2034
|
+
},
|
|
2035
|
+
}),
|
|
2036
|
+
validator("json", Worktree.RemoveInput),
|
|
2037
|
+
async (c) => {
|
|
2038
|
+
const input = c.req.valid("json")
|
|
2039
|
+
await Worktree.remove(input)
|
|
2040
|
+
await Project.removeSandbox(Instance.project.id, input.directory).catch(() => undefined)
|
|
2041
|
+
return c.json({ success: true as const })
|
|
2042
|
+
},
|
|
2043
|
+
),
|
|
2044
|
+
)
|