mirror of
https://github.com/coder/registry.git
synced 2026-06-03 04:58:15 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d745117782 | |||
| a99d3385c3 | |||
| c62fe569a0 | |||
| ce2087bc09 | |||
| 67f18cd4de | |||
| e0697562c1 | |||
| 499aaa676c |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="250" cy="250" r="250" fill="#fff"/>
|
||||
<path d="m335 150h40v200h-40zm-130 0a100 100 0 1 0 0 200 100 100 0 1 0 0-200zm0 40a60 60 0 1 1 0 120 60 60 0 1 1 0-120z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 293 B |
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "registry",
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.21",
|
||||
"bun-types": "^1.2.21",
|
||||
"dedent": "^1.6.0",
|
||||
"@types/bun": "^1.3.4",
|
||||
"bun-types": "^1.3.4",
|
||||
"dedent": "^1.7.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"marked": "^16.2.0",
|
||||
"prettier": "^3.6.2",
|
||||
"marked": "^16.4.2",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-sh": "^0.18.0",
|
||||
"prettier-plugin-terraform-formatter": "^1.2.1",
|
||||
"shellcheck": "^4.1.0",
|
||||
@@ -30,12 +31,10 @@
|
||||
|
||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
|
||||
"@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
|
||||
|
||||
"@types/node": ["@types/node@24.0.14", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
||||
|
||||
"@xhmikosr/decompress-tar": ["@xhmikosr/decompress-tar@8.1.0", "", { "dependencies": { "file-type": "^20.5.0", "is-stream": "^2.0.1", "tar-stream": "^3.1.7" } }, "sha512-m0q8x6lwxenh1CrsTby0Jrjq4vzW/QU1OLhTHMQLEdHpmjR1lgahGz++seZI0bXF3XcZw3U3xHfqZSz+JPP2Gg=="],
|
||||
|
||||
"@xhmikosr/decompress-unzip": ["@xhmikosr/decompress-unzip@7.1.0", "", { "dependencies": { "file-type": "^20.5.0", "get-stream": "^6.0.1", "yauzl": "^3.1.2" } }, "sha512-oqTYAcObqTlg8owulxFTqiaJkfv2SHsxxxz9Wg4krJAHVzGWlZsU8tAB30R6ow+aHrfv4Kub6WQ8u04NWVPUpA=="],
|
||||
@@ -64,7 +63,7 @@
|
||||
|
||||
"buffer-fill": ["buffer-fill@1.0.0", "", {}, "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
|
||||
"bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
@@ -76,8 +75,6 @@
|
||||
|
||||
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decompress": ["decompress@4.2.1", "", { "dependencies": { "decompress-tar": "^4.0.0", "decompress-tarbz2": "^4.0.0", "decompress-targz": "^4.0.0", "decompress-unzip": "^4.0.1", "graceful-fs": "^4.1.10", "make-dir": "^1.0.0", "pify": "^2.3.0", "strip-dirs": "^2.0.0" } }, "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ=="],
|
||||
@@ -90,7 +87,7 @@
|
||||
|
||||
"decompress-unzip": ["decompress-unzip@4.0.1", "", { "dependencies": { "file-type": "^3.8.0", "get-stream": "^2.2.0", "pify": "^2.3.0", "yauzl": "^2.4.2" } }, "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw=="],
|
||||
|
||||
"dedent": ["dedent@1.6.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA=="],
|
||||
"dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
|
||||
@@ -182,7 +179,7 @@
|
||||
|
||||
"make-dir": ["make-dir@1.3.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ=="],
|
||||
|
||||
"marked": ["marked@16.2.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-LbbTuye+0dWRz2TS9KJ7wsnD4KAtpj0MVkWc90XvBa6AslXsT0hTBVH5k32pcSyHH1fst9XEFJunXHktVy0zlg=="],
|
||||
"marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
|
||||
|
||||
"matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="],
|
||||
|
||||
@@ -206,7 +203,7 @@
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
|
||||
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
|
||||
|
||||
"prettier-plugin-sh": ["prettier-plugin-sh@0.18.0", "", { "dependencies": { "@reteps/dockerfmt": "^0.3.6", "sh-syntax": "^0.5.8" }, "peerDependencies": { "prettier": "^3.6.0" } }, "sha512-cW1XL27FOJQ/qGHOW6IHwdCiNWQsAgK+feA8V6+xUTaH0cD3Mh+tFAtBvEEWvuY6hTDzRV943Fzeii+qMOh7nQ=="],
|
||||
|
||||
|
||||
+5
-5
@@ -10,12 +10,12 @@
|
||||
"update-version": "./update-version.sh"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.21",
|
||||
"bun-types": "^1.2.21",
|
||||
"dedent": "^1.6.0",
|
||||
"@types/bun": "^1.3.4",
|
||||
"bun-types": "^1.3.4",
|
||||
"dedent": "^1.7.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"marked": "^16.2.0",
|
||||
"prettier": "^3.6.2",
|
||||
"marked": "^16.4.2",
|
||||
"prettier": "^3.7.4",
|
||||
"prettier-plugin-sh": "^0.18.0",
|
||||
"prettier-plugin-terraform-formatter": "^1.2.1",
|
||||
"shellcheck": "^4.1.0"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 407 KiB |
@@ -0,0 +1,64 @@
|
||||
---
|
||||
display_name: Open WebUI
|
||||
description: A self-hosted AI chat interface supporting various LLM providers
|
||||
icon: ../../../../.icons/openwebui.svg
|
||||
verified: false
|
||||
tags: [ai, llm, chat, web, python]
|
||||
---
|
||||
|
||||
# Open WebUI
|
||||
|
||||
Open WebUI is a user-friendly web interface for interacting with Large Language Models. It provides a ChatGPT-like interface that can connect to various LLM providers including OpenAI, Ollama, and more.
|
||||
|
||||
```tf
|
||||
module "open-webui" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/open-webui/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Python 3.11 or higher** must be installed in your image (with `venv` module)
|
||||
- Port 7800 (default) or your custom port must be available
|
||||
|
||||
For Ubuntu/Debian, you can install Python 3.11 from [deadsnakes PPA](https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa):
|
||||
|
||||
```shell
|
||||
sudo add-apt-repository -y ppa:deadsnakes/ppa
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3.11 python3.11-venv
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### With OpenAI API Key
|
||||
|
||||
```tf
|
||||
module "open-webui" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/open-webui/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
|
||||
openai_api_key = var.openai_api_key
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Port and Data Directory
|
||||
|
||||
```tf
|
||||
module "open-webui" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/open-webui/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.main.id
|
||||
|
||||
http_server_port = 8080
|
||||
data_dir = "/home/coder/open-webui-data"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,94 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "http_server_log_path" {
|
||||
type = string
|
||||
description = "The path to log Open WebUI to."
|
||||
default = "/tmp/open-webui.log"
|
||||
}
|
||||
|
||||
variable "http_server_port" {
|
||||
type = number
|
||||
description = "The port to run Open WebUI on."
|
||||
default = 7800
|
||||
}
|
||||
|
||||
variable "open_webui_version" {
|
||||
type = string
|
||||
description = "The version of Open WebUI to install"
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "data_dir" {
|
||||
type = string
|
||||
description = "The directory where Open WebUI stores its data (database, uploads, vector_db, cache)."
|
||||
default = ".open-webui"
|
||||
}
|
||||
|
||||
variable "openai_api_key" {
|
||||
type = string
|
||||
description = "OpenAI API key for accessing OpenAI models. If not provided, OpenAI integration will need to be configured manually in the UI."
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "share" {
|
||||
type = string
|
||||
description = "The sharing level for the Open WebUI app. Set to 'owner' for private access, 'authenticated' for access by any authenticated user, or 'public' for public access."
|
||||
default = "owner"
|
||||
validation {
|
||||
condition = var.share == "owner" || var.share == "authenticated" || var.share == "public"
|
||||
error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
resource "coder_script" "open-webui" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "open-webui"
|
||||
icon = "/icon/openwebui.svg"
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
HTTP_SERVER_LOG_PATH : var.http_server_log_path,
|
||||
HTTP_SERVER_PORT : var.http_server_port,
|
||||
VERSION : var.open_webui_version,
|
||||
DATA_DIR : var.data_dir,
|
||||
OPENAI_API_KEY : var.openai_api_key,
|
||||
})
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "open-webui" {
|
||||
agent_id = var.agent_id
|
||||
slug = "open-webui"
|
||||
display_name = "Open WebUI"
|
||||
url = "http://localhost:${var.http_server_port}"
|
||||
icon = "/icon/openwebui.svg"
|
||||
subdomain = true
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
mock_provider "coder" {}
|
||||
|
||||
run "test_defaults" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-123"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.http_server_port == 7800
|
||||
error_message = "Default port should be 7800"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.http_server_log_path == "/tmp/open-webui.log"
|
||||
error_message = "Default log path should be /tmp/open-webui.log"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.share == "owner"
|
||||
error_message = "Default share should be 'owner'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.open_webui_version == "latest"
|
||||
error_message = "Default version should be 'latest'"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.subdomain == true
|
||||
error_message = "App should use subdomain"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.display_name == "Open WebUI"
|
||||
error_message = "App display name should be 'Open WebUI'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_custom_port" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-456"
|
||||
http_server_port = 9000
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.http_server_port == 9000
|
||||
error_message = "Custom port should be 9000"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.url == "http://localhost:9000"
|
||||
error_message = "App URL should use custom port"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_custom_log_path" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-789"
|
||||
http_server_log_path = "/var/log/open-webui.log"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.http_server_log_path == "/var/log/open-webui.log"
|
||||
error_message = "Custom log path should be set"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_share_authenticated" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-auth"
|
||||
share = "authenticated"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.share == "authenticated"
|
||||
error_message = "Share should be 'authenticated'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_share_public" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-public"
|
||||
share = "public"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.share == "public"
|
||||
error_message = "Share should be 'public'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_order_and_group" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-order"
|
||||
order = 10
|
||||
group = "AI Tools"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.order == 10
|
||||
error_message = "Order should be 10"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = coder_app.open-webui.group == "AI Tools"
|
||||
error_message = "Group should be 'AI Tools'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_custom_version" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-version"
|
||||
open_webui_version = "0.5.0"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.open_webui_version == "0.5.0"
|
||||
error_message = "Custom version should be '0.5.0'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_custom_data_dir" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-data"
|
||||
data_dir = "/home/coder/open-webui-data"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.data_dir == "/home/coder/open-webui-data"
|
||||
error_message = "Custom data_dir should be set"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_default_data_dir" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-data-default"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.data_dir == ".open-webui"
|
||||
error_message = "Default data_dir should be '.open-webui'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_openai_api_key" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-openai"
|
||||
openai_api_key = "sk-test-key-123"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.openai_api_key == "sk-test-key-123"
|
||||
error_message = "OpenAI API key should be set"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_default_openai_api_key" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-openai-default"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.openai_api_key == ""
|
||||
error_message = "Default OpenAI API key should be empty"
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
set -eu
|
||||
|
||||
printf '\033[0;1mInstalling Open WebUI %s...\n\n' "${VERSION}"
|
||||
|
||||
check_python_version() {
|
||||
python_cmd="$1"
|
||||
if command -v "$python_cmd" > /dev/null 2>&1; then
|
||||
version=$("$python_cmd" --version 2>&1 | awk '{print $2}')
|
||||
major=$(echo "$version" | cut -d. -f1)
|
||||
minor=$(echo "$version" | cut -d. -f2)
|
||||
if [ "$major" -eq 3 ] && [ "$minor" -ge 11 ]; then
|
||||
echo "$python_cmd"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
PYTHON_CMD=""
|
||||
for cmd in python3.13 python3.12 python3.11 python3 python; do
|
||||
if result=$(check_python_version "$cmd"); then
|
||||
PYTHON_CMD="$result"
|
||||
echo "✅ Found suitable Python: $PYTHON_CMD ($($PYTHON_CMD --version 2>&1))"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$PYTHON_CMD" ]; then
|
||||
echo "❌ Python 3.11 or higher is required but not found."
|
||||
echo ""
|
||||
echo "Please install Python 3.11+ in your image. For example on Ubuntu/Debian:"
|
||||
echo " sudo add-apt-repository -y ppa:deadsnakes/ppa"
|
||||
echo " sudo apt-get update"
|
||||
echo " sudo apt-get install -y python3.11 python3.11-venv"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VENV_DIR="$HOME/.open-webui-venv"
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
echo "📦 Creating virtual environment..."
|
||||
"$PYTHON_CMD" -m venv "$VENV_DIR"
|
||||
fi
|
||||
. "$VENV_DIR/bin/activate"
|
||||
|
||||
if ! pip show open-webui > /dev/null 2>&1; then
|
||||
echo "📦 Installing Open WebUI version ${VERSION}..."
|
||||
if [ "${VERSION}" = "latest" ]; then
|
||||
pip install open-webui
|
||||
else
|
||||
pip install "open-webui==${VERSION}"
|
||||
fi
|
||||
echo "🥳 Open WebUI has been installed"
|
||||
else
|
||||
echo "✅ Open WebUI is already installed"
|
||||
fi
|
||||
|
||||
echo "👷 Starting Open WebUI in background..."
|
||||
echo "Check logs at ${HTTP_SERVER_LOG_PATH}"
|
||||
|
||||
DATA_DIR="${DATA_DIR}" \
|
||||
OPENAI_API_KEY="${OPENAI_API_KEY}" \
|
||||
open-webui serve --host 0.0.0.0 --port "${HTTP_SERVER_PORT}" > "${HTTP_SERVER_LOG_PATH}" 2>&1 &
|
||||
|
||||
echo "🥳 Open WebUI is ready. HTTP server is listening on port ${HTTP_SERVER_PORT}"
|
||||
@@ -13,8 +13,8 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
version = "4.2.6"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
||||
}
|
||||
@@ -45,13 +45,15 @@ This example shows how to configure the Claude Code module to run the agent behi
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "dev.registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.6"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
enable_boundary = true
|
||||
boundary_version = "4.2.2"
|
||||
boundary_version = "main"
|
||||
boundary_log_dir = "/tmp/boundary_logs"
|
||||
boundary_log_level = "WARN"
|
||||
boundary_additional_allowed_urls = ["GET *google.com"]
|
||||
boundary_proxy_port = "8087"
|
||||
version = "4.2.2"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -70,16 +72,16 @@ data "coder_parameter" "ai_prompt" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
version = "4.2.6"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
claude_api_key = "xxxx-xxxxx-xxxx"
|
||||
# OR
|
||||
claude_code_oauth_token = "xxxxx-xxxx-xxxx"
|
||||
|
||||
claude_code_version = "4.2.2" # Pin to a specific version
|
||||
agentapi_version = "4.2.2"
|
||||
claude_code_version = "2.0.62" # Pin to a specific version
|
||||
agentapi_version = "0.11.4"
|
||||
|
||||
ai_prompt = data.coder_parameter.ai_prompt.value
|
||||
model = "sonnet"
|
||||
@@ -93,6 +95,7 @@ module "claude-code" {
|
||||
"command": "my-tool-server"
|
||||
"args": ["--port", "8080"]
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
EOF
|
||||
@@ -106,13 +109,12 @@ Run and configure Claude Code as a standalone CLI in your workspace.
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
workdir = "/home/coder"
|
||||
version = "4.2.6"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
install_claude_code = true
|
||||
claude_code_version = "4.2.2"
|
||||
claude_code_version = "2.0.62"
|
||||
report_tasks = false
|
||||
cli_app = true
|
||||
}
|
||||
```
|
||||
|
||||
@@ -129,8 +131,8 @@ variable "claude_code_oauth_token" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
version = "4.2.6"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
claude_code_oauth_token = var.claude_code_oauth_token
|
||||
}
|
||||
@@ -146,13 +148,13 @@ Configure Claude Code to use AWS Bedrock for accessing Claude models through you
|
||||
|
||||
```tf
|
||||
resource "coder_env" "bedrock_use" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CLAUDE_CODE_USE_BEDROCK"
|
||||
value = "1"
|
||||
}
|
||||
|
||||
resource "coder_env" "aws_region" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "AWS_REGION"
|
||||
value = "us-east-1" # Choose your preferred region
|
||||
}
|
||||
@@ -174,13 +176,13 @@ variable "aws_secret_access_key" {
|
||||
}
|
||||
|
||||
resource "coder_env" "aws_access_key_id" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "AWS_ACCESS_KEY_ID"
|
||||
value = var.aws_access_key_id
|
||||
}
|
||||
|
||||
resource "coder_env" "aws_secret_access_key" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "AWS_SECRET_ACCESS_KEY"
|
||||
value = var.aws_secret_access_key
|
||||
}
|
||||
@@ -195,15 +197,15 @@ variable "aws_bearer_token_bedrock" {
|
||||
}
|
||||
|
||||
resource "coder_env" "bedrock_api_key" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "AWS_BEARER_TOKEN_BEDROCK"
|
||||
value = var.aws_bearer_token_bedrock
|
||||
}
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
version = "4.2.6"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
}
|
||||
@@ -228,39 +230,39 @@ variable "vertex_sa_json" {
|
||||
}
|
||||
|
||||
resource "coder_env" "vertex_use" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CLAUDE_CODE_USE_VERTEX"
|
||||
value = "1"
|
||||
}
|
||||
|
||||
resource "coder_env" "vertex_project_id" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "ANTHROPIC_VERTEX_PROJECT_ID"
|
||||
value = "your-gcp-project-id"
|
||||
}
|
||||
|
||||
resource "coder_env" "cloud_ml_region" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "CLOUD_ML_REGION"
|
||||
value = "global"
|
||||
}
|
||||
|
||||
resource "coder_env" "vertex_sa_json" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "VERTEX_SA_JSON"
|
||||
value = var.vertex_sa_json
|
||||
}
|
||||
|
||||
resource "coder_env" "google_application_credentials" {
|
||||
agent_id = coder_agent.example.id
|
||||
agent_id = coder_agent.main.id
|
||||
name = "GOOGLE_APPLICATION_CREDENTIALS"
|
||||
value = "/tmp/gcp-sa.json"
|
||||
}
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "4.2.2"
|
||||
agent_id = coder_agent.example.id
|
||||
version = "4.2.6"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "claude-sonnet-4@20250514"
|
||||
|
||||
|
||||
@@ -208,13 +208,17 @@ describe("claude-code", async () => {
|
||||
});
|
||||
|
||||
// Create a mock task session file with the hardcoded task session ID
|
||||
// Note: Claude CLI creates files without "session-" prefix when using --session-id
|
||||
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
|
||||
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
|
||||
await execContainer(id, ["mkdir", "-p", sessionDir]);
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`touch ${sessionDir}/session-${taskSessionId}.jsonl`,
|
||||
`cat > ${sessionDir}/${taskSessionId}.jsonl << 'SESSIONEOF'
|
||||
{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"}
|
||||
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"}
|
||||
SESSIONEOF`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
@@ -226,46 +230,10 @@ describe("claude-code", async () => {
|
||||
]);
|
||||
expect(startLog.stdout).toContain("--resume");
|
||||
expect(startLog.stdout).toContain(taskSessionId);
|
||||
expect(startLog.stdout).toContain("Resuming existing task session");
|
||||
expect(startLog.stdout).toContain("Resuming task session");
|
||||
expect(startLog.stdout).toContain("--dangerously-skip-permissions");
|
||||
});
|
||||
|
||||
test("claude-continue-resume-standalone-session", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
report_tasks: "false",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
|
||||
const sessionId = "some-random-session-id";
|
||||
const workdir = "/home/coder/project";
|
||||
const claudeJson = {
|
||||
projects: {
|
||||
[workdir]: {
|
||||
lastSessionId: sessionId,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`echo '${JSON.stringify(claudeJson)}' > /home/coder/.claude.json`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
expect(startLog.stdout).toContain("--continue");
|
||||
expect(startLog.stdout).toContain("Resuming existing session");
|
||||
});
|
||||
|
||||
test("pre-post-install-scripts", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
@@ -360,4 +328,140 @@ describe("claude-code", async () => {
|
||||
"ARG_AGENTAPI_CHAT_BASE_PATH=/@default/default.foo/apps/ccw/chat",
|
||||
);
|
||||
});
|
||||
|
||||
test("partial-initialization-detection", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
report_tasks: "true",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
|
||||
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
|
||||
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
|
||||
await execContainer(id, ["mkdir", "-p", sessionDir]);
|
||||
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`echo '{"sessionId":"${taskSessionId}"}' > ${sessionDir}/${taskSessionId}.jsonl`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
|
||||
// Should start new session, not try to resume invalid one
|
||||
expect(startLog.stdout).toContain("Starting new task session");
|
||||
expect(startLog.stdout).toContain("--session-id");
|
||||
});
|
||||
|
||||
test("standalone-first-build-no-sessions", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
report_tasks: "false",
|
||||
},
|
||||
});
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
|
||||
// Should start fresh, not try to continue
|
||||
expect(startLog.stdout).toContain("No sessions found");
|
||||
expect(startLog.stdout).toContain("starting fresh standalone session");
|
||||
expect(startLog.stdout).not.toContain("--continue");
|
||||
});
|
||||
|
||||
test("standalone-with-sessions-continues", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
report_tasks: "false",
|
||||
},
|
||||
});
|
||||
|
||||
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
|
||||
await execContainer(id, ["mkdir", "-p", sessionDir]);
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`cat > ${sessionDir}/generic-123.jsonl << 'EOF'
|
||||
{"sessionId":"generic-123","message":{"content":"User session"},"timestamp":"2020-01-01T10:00:00.000Z"}
|
||||
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"}
|
||||
EOF`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
|
||||
// Should continue existing session
|
||||
expect(startLog.stdout).toContain("Sessions found");
|
||||
expect(startLog.stdout).toContain(
|
||||
"Continuing most recent standalone session",
|
||||
);
|
||||
expect(startLog.stdout).toContain("--continue");
|
||||
});
|
||||
|
||||
test("task-mode-ignores-manual-sessions", async () => {
|
||||
const { id } = await setup({
|
||||
moduleVariables: {
|
||||
continue: "true",
|
||||
report_tasks: "true",
|
||||
ai_prompt: "test prompt",
|
||||
},
|
||||
});
|
||||
|
||||
const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2";
|
||||
const sessionDir = `/home/coder/.claude/projects/-home-coder-project`;
|
||||
await execContainer(id, ["mkdir", "-p", sessionDir]);
|
||||
|
||||
// Create task session (without "session-" prefix, as CLI does)
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`cat > ${sessionDir}/${taskSessionId}.jsonl << 'EOF'
|
||||
{"sessionId":"${taskSessionId}","message":{"content":"Task"},"timestamp":"2020-01-01T10:00:00.000Z"}
|
||||
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-01T10:00:05.000Z"}
|
||||
EOF`,
|
||||
]);
|
||||
|
||||
// Create manual session (newer)
|
||||
await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
`cat > ${sessionDir}/manual-456.jsonl << 'EOF'
|
||||
{"sessionId":"manual-456","message":{"content":"Manual"},"timestamp":"2020-01-02T10:00:00.000Z"}
|
||||
{"type":"assistant","message":{"content":"Response"},"timestamp":"2020-01-02T10:00:05.000Z"}
|
||||
EOF`,
|
||||
]);
|
||||
|
||||
await execModuleScript(id);
|
||||
|
||||
const startLog = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /home/coder/.claude-module/agentapi-start.log",
|
||||
]);
|
||||
|
||||
// Should resume task session, not manual session
|
||||
expect(startLog.stdout).toContain("Resuming task session");
|
||||
expect(startLog.stdout).toContain(taskSessionId);
|
||||
expect(startLog.stdout).not.toContain("manual-456");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,7 +86,7 @@ variable "install_agentapi" {
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.10.0"
|
||||
default = "v0.11.4"
|
||||
}
|
||||
|
||||
variable "ai_prompt" {
|
||||
@@ -291,12 +291,11 @@ resource "coder_env" "disable_autoupdater" {
|
||||
locals {
|
||||
# we have to trim the slash because otherwise coder exp mcp will
|
||||
# set up an invalid claude config
|
||||
workdir = trimsuffix(var.workdir, "/")
|
||||
app_slug = "ccw"
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".claude-module"
|
||||
remove_last_session_id_script_b64 = base64encode(file("${path.module}/scripts/remove-last-session-id.sh"))
|
||||
workdir = trimsuffix(var.workdir, "/")
|
||||
app_slug = "ccw"
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".claude-module"
|
||||
# Extract hostname from access_url for boundary --allow flag
|
||||
coder_host = replace(replace(data.coder_workspace.me.access_url, "https://", ""), "http://", "")
|
||||
|
||||
@@ -357,9 +356,7 @@ module "agentapi" {
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh
|
||||
echo -n "${local.remove_last_session_id_script_b64}" | base64 -d > "/tmp/remove-last-session-id.sh"
|
||||
chmod +x /tmp/start.sh
|
||||
chmod +x /tmp/remove-last-session-id.sh
|
||||
|
||||
ARG_MODEL='${var.model}' \
|
||||
ARG_RESUME_SESSION_ID='${var.resume_session_id}' \
|
||||
|
||||
@@ -90,12 +90,63 @@ function setup_claude_configurations() {
|
||||
|
||||
}
|
||||
|
||||
function configure_standalone_mode() {
|
||||
echo "Configuring Claude Code for standalone mode..."
|
||||
|
||||
if [ -z "${CLAUDE_API_KEY:-}" ]; then
|
||||
echo "Note: CLAUDE_API_KEY not set, skipping authentication setup"
|
||||
return
|
||||
fi
|
||||
|
||||
local claude_config="$HOME/.claude.json"
|
||||
local workdir_normalized
|
||||
workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-')
|
||||
|
||||
# Create or update .claude.json with minimal configuration for API key auth
|
||||
# This skips the interactive login prompt and onboarding screens
|
||||
if [ -f "$claude_config" ]; then
|
||||
echo "Updating existing Claude configuration at $claude_config"
|
||||
|
||||
jq --arg apikey "${CLAUDE_API_KEY:-}" \
|
||||
--arg workdir "$ARG_WORKDIR" \
|
||||
'.autoUpdaterStatus = "disabled" |
|
||||
.bypassPermissionsModeAccepted = true |
|
||||
.hasAcknowledgedCostThreshold = true |
|
||||
.hasCompletedOnboarding = true |
|
||||
.primaryApiKey = $apikey |
|
||||
.projects[$workdir].hasCompletedProjectOnboarding = true |
|
||||
.projects[$workdir].hasTrustDialogAccepted = true' \
|
||||
"$claude_config" > "${claude_config}.tmp" && mv "${claude_config}.tmp" "$claude_config"
|
||||
else
|
||||
echo "Creating new Claude configuration at $claude_config"
|
||||
cat > "$claude_config" << EOF
|
||||
{
|
||||
"autoUpdaterStatus": "disabled",
|
||||
"bypassPermissionsModeAccepted": true,
|
||||
"hasAcknowledgedCostThreshold": true,
|
||||
"hasCompletedOnboarding": true,
|
||||
"primaryApiKey": "${CLAUDE_API_KEY:-}",
|
||||
"projects": {
|
||||
"$ARG_WORKDIR": {
|
||||
"hasCompletedProjectOnboarding": true,
|
||||
"hasTrustDialogAccepted": true
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo "Standalone mode configured successfully"
|
||||
}
|
||||
|
||||
function report_tasks() {
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
echo "Configuring Claude Code to report tasks via Coder MCP..."
|
||||
export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG"
|
||||
export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284"
|
||||
coder exp mcp configure claude-code "$ARG_WORKDIR"
|
||||
else
|
||||
configure_standalone_mode
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# If lastSessionId is present in .claude.json, claude --continue will start a
|
||||
# conversation starting from that session. The problem is that lastSessionId
|
||||
# doesn't always point to the last session. The field is updated by claude only
|
||||
# at the point of normal CLI exit. If Claude exits with an error, or if the user
|
||||
# restarts the Coder workspace, lastSessionId will be stale, and claude --continue
|
||||
# will start from an old session.
|
||||
#
|
||||
# If lastSessionId is missing, claude seems to accurately figure out where to
|
||||
# start using the conversation history - even if the CLI previously exited with
|
||||
# an error.
|
||||
#
|
||||
# This script removes the lastSessionId field from .claude.json.
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "No working directory provided - it must be the first argument"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get absolute path of working directory
|
||||
working_dir=$(realpath "$1")
|
||||
echo "workingDir $working_dir"
|
||||
|
||||
# Path to .claude.json
|
||||
claude_json_path="$HOME/.claude.json"
|
||||
echo ".claude.json path $claude_json_path"
|
||||
|
||||
# Check if .claude.json exists
|
||||
if [ ! -f "$claude_json_path" ]; then
|
||||
echo "No .claude.json file found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Use jq to check if lastSessionId exists for the working directory and remove it
|
||||
|
||||
if jq -e ".projects[\"$working_dir\"].lastSessionId" "$claude_json_path" > /dev/null 2>&1; then
|
||||
# Remove lastSessionId and update the file
|
||||
if jq "del(.projects[\"$working_dir\"].lastSessionId)" "$claude_json_path" > "${claude_json_path}.tmp" && mv "${claude_json_path}.tmp" "$claude_json_path"; then
|
||||
echo "Removed lastSessionId from .claude.json"
|
||||
exit 0
|
||||
else
|
||||
echo "Failed to remove lastSessionId from .claude.json"
|
||||
fi
|
||||
else
|
||||
echo "No lastSessionId found in .claude.json - nothing to do"
|
||||
fi
|
||||
@@ -51,19 +51,6 @@ printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST"
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
# Clean up stale session data (see remove-last-session-id.sh for details)
|
||||
CAN_CONTINUE_CONVERSATION=false
|
||||
set +e
|
||||
bash "/tmp/remove-last-session-id.sh" "$(pwd)" 2> /dev/null
|
||||
session_cleanup_exit_code=$?
|
||||
set -e
|
||||
|
||||
case $session_cleanup_exit_code in
|
||||
0)
|
||||
CAN_CONTINUE_CONVERSATION=true
|
||||
;;
|
||||
esac
|
||||
|
||||
function install_boundary() {
|
||||
if [ "${ARG_COMPILE_FROM_SOURCE:-false}" = "true" ]; then
|
||||
# Install boundary by compiling from source
|
||||
@@ -99,18 +86,85 @@ function validate_claude_installation() {
|
||||
# This ensures all task sessions use a consistent, predictable ID
|
||||
TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2"
|
||||
|
||||
task_session_exists() {
|
||||
get_project_dir() {
|
||||
local workdir_normalized
|
||||
workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-')
|
||||
local project_dir="$HOME/.claude/projects/${workdir_normalized}"
|
||||
echo "$HOME/.claude/projects/${workdir_normalized}"
|
||||
}
|
||||
|
||||
printf "PROJECT_DIR: %s, workdir_normalized: %s\n" "$project_dir" "$workdir_normalized"
|
||||
get_task_session_file() {
|
||||
echo "$(get_project_dir)/${TASK_SESSION_ID}.jsonl"
|
||||
}
|
||||
|
||||
if [ -d "$project_dir" ] && find "$project_dir" -type f -name "*${TASK_SESSION_ID}*" 2> /dev/null | grep -q .; then
|
||||
printf "TASK_SESSION_ID: %s file found\n" "$TASK_SESSION_ID"
|
||||
task_session_exists() {
|
||||
local session_file
|
||||
session_file=$(get_task_session_file)
|
||||
|
||||
if [ -f "$session_file" ]; then
|
||||
printf "Task session file found: %s\n" "$session_file"
|
||||
return 0
|
||||
else
|
||||
printf "TASK_SESSION_ID: %s file not found\n" "$TASK_SESSION_ID"
|
||||
printf "Task session file not found: %s\n" "$session_file"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
is_valid_session() {
|
||||
local session_file="$1"
|
||||
|
||||
# Check if file exists and is not empty
|
||||
# Empty files indicate the session was created but never used so they need to be removed
|
||||
if [ ! -f "$session_file" ]; then
|
||||
printf "Session validation failed: file does not exist\n"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -s "$session_file" ]; then
|
||||
printf "Session validation failed: file is empty, removing stale file\n"
|
||||
rm -f "$session_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check for minimum session content
|
||||
# Valid sessions need at least 2 lines: initial message and first response
|
||||
local line_count
|
||||
line_count=$(wc -l < "$session_file")
|
||||
if [ "$line_count" -lt 2 ]; then
|
||||
printf "Session validation failed: incomplete (only %s lines), removing incomplete file\n" "$line_count"
|
||||
rm -f "$session_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Validate JSONL format by checking first 3 lines
|
||||
# Claude session files use JSONL (JSON Lines) format where each line is valid JSON
|
||||
if ! head -3 "$session_file" | jq empty 2> /dev/null; then
|
||||
printf "Session validation failed: invalid JSONL format, removing corrupt file\n"
|
||||
rm -f "$session_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Verify the session has a valid sessionId field
|
||||
# This ensures the file structure matches Claude's session format
|
||||
if ! grep -q '"sessionId"' "$session_file" \
|
||||
|| ! grep -m 1 '"sessionId"' "$session_file" | jq -e '.sessionId' > /dev/null 2>&1; then
|
||||
printf "Session validation failed: no valid sessionId found, removing malformed file\n"
|
||||
rm -f "$session_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf "Session validation passed: %s\n" "$session_file"
|
||||
return 0
|
||||
}
|
||||
|
||||
has_any_sessions() {
|
||||
local project_dir
|
||||
project_dir=$(get_project_dir)
|
||||
|
||||
if [ -d "$project_dir" ] && find "$project_dir" -maxdepth 1 -name "*.jsonl" -size +0c 2> /dev/null | grep -q .; then
|
||||
printf "Sessions found in: %s\n" "$project_dir"
|
||||
return 0
|
||||
else
|
||||
printf "No sessions found in: %s\n" "$project_dir"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
@@ -133,75 +187,41 @@ function start_agentapi() {
|
||||
fi
|
||||
|
||||
if [ -n "$ARG_RESUME_SESSION_ID" ]; then
|
||||
echo "Resuming task session by ID: $ARG_RESUME_SESSION_ID"
|
||||
echo "Resuming specified session: $ARG_RESUME_SESSION_ID"
|
||||
ARGS+=(--resume "$ARG_RESUME_SESSION_ID")
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
|
||||
|
||||
elif [ "$ARG_CONTINUE" = "true" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ] && task_session_exists; then
|
||||
echo "Task session detected (ID: $TASK_SESSION_ID)"
|
||||
ARGS+=(--resume "$TASK_SESSION_ID")
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
echo "Resuming existing task session"
|
||||
elif [ "$ARG_REPORT_TASKS" = "false" ] && [ "$CAN_CONTINUE_CONVERSATION" = true ]; then
|
||||
echo "Previous session exists"
|
||||
ARGS+=(--continue)
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
echo "Resuming existing session"
|
||||
else
|
||||
echo "No existing session found"
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
if task_session_exists; then
|
||||
ARGS+=(--resume "$TASK_SESSION_ID")
|
||||
else
|
||||
ARGS+=(--session-id "$TASK_SESSION_ID")
|
||||
fi
|
||||
fi
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions -- "$ARG_AI_PROMPT")
|
||||
else
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
ARGS+=(-- "$ARG_AI_PROMPT")
|
||||
fi
|
||||
echo "Starting new session with prompt"
|
||||
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
local session_file
|
||||
session_file=$(get_task_session_file)
|
||||
|
||||
if task_session_exists && is_valid_session "$session_file"; then
|
||||
echo "Resuming task session: $TASK_SESSION_ID"
|
||||
ARGS+=(--resume "$TASK_SESSION_ID" --dangerously-skip-permissions)
|
||||
else
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ] || [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
echo "Starting new session"
|
||||
echo "Starting new task session: $TASK_SESSION_ID"
|
||||
ARGS+=(--session-id "$TASK_SESSION_ID" --dangerously-skip-permissions)
|
||||
[ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT")
|
||||
fi
|
||||
|
||||
else
|
||||
if has_any_sessions; then
|
||||
echo "Continuing most recent standalone session"
|
||||
ARGS+=(--continue)
|
||||
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
|
||||
else
|
||||
echo "No sessions found, starting fresh standalone session"
|
||||
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
|
||||
[ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT")
|
||||
fi
|
||||
fi
|
||||
|
||||
else
|
||||
echo "Continue disabled, starting fresh session"
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
if task_session_exists; then
|
||||
ARGS+=(--resume "$TASK_SESSION_ID")
|
||||
else
|
||||
ARGS+=(--session-id "$TASK_SESSION_ID")
|
||||
fi
|
||||
fi
|
||||
if [ -n "$ARG_AI_PROMPT" ]; then
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions -- "$ARG_AI_PROMPT")
|
||||
else
|
||||
if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
ARGS+=(-- "$ARG_AI_PROMPT")
|
||||
fi
|
||||
echo "Starting new session with prompt"
|
||||
else
|
||||
if [ "$ARG_REPORT_TASKS" = "true" ] || [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then
|
||||
ARGS+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
echo "Starting claude code session"
|
||||
fi
|
||||
[ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ] && ARGS+=(--dangerously-skip-permissions)
|
||||
[ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT")
|
||||
fi
|
||||
|
||||
printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
display_name: Positron Desktop
|
||||
description: Add a one-click button to launch Positron Desktop
|
||||
icon: ../../../../.icons/positron.svg
|
||||
verified: true
|
||||
verified: false
|
||||
tags: [ide, positron]
|
||||
---
|
||||
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "positron" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/cytoshahar/positron/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user