Skip to content

Commit 2fbcd60

Browse files
committed
feat: LYZR integration, OpenAI-compatible endpoints, password protection, camera flip
- install.sh: "Install with LYZR" option — creates Lyzr agent, sets up completions endpoint - loader.ts: custom model syntax (provider:model@base-url), GITCLAW_MODEL_BASE_URL env var - loader.ts: auto-injects API key for unknown providers via env fallback - server.ts: GITCLAW_PASSWORD env var enables login page + cookie auth on all routes + WebSocket - ui.html: custom base URL field in Settings, camera flip button, chat text wrapping fix
1 parent 97cc796 commit 2fbcd60

5 files changed

Lines changed: 325 additions & 42 deletions

File tree

install.sh

Lines changed: 162 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -144,44 +144,52 @@ if [ -d "$PROJECT_DIR" ] && [ -f "$PROJECT_DIR/agent.yaml" ]; then
144144
echo -e " ${GREEN}${NC} Loaded keys from ${DIM}${PROJECT_DIR}/.env${NC}"
145145
fi
146146

147-
# Prompt for missing required keys
148-
if [ -z "${OPENAI_API_KEY:-}" ] && [ -z "${GEMINI_API_KEY:-}" ]; then
149-
echo ""
150-
echo -e " ${YELLOW}${NC} No voice API key found."
151-
echo -e " ${BOLD}OpenAI API Key${NC} ${DIM}(for voice — get one at platform.openai.com)${NC}"
152-
read -rsp " Key: " OPENAI_KEY
153-
echo ""
154-
if [ -z "$OPENAI_KEY" ]; then
155-
echo -e " ${RED}✗ OpenAI or Gemini key is required for voice mode${NC}"
156-
exit 1
147+
# Prompt for missing required keys (skip if Lyzr is configured)
148+
if [ -n "${GITCLAW_LYZR_AGENT_ID:-}" ]; then
149+
echo -e " ${GREEN}${NC} Lyzr agent: ${DIM}${GITCLAW_LYZR_AGENT_ID}${NC}"
150+
else
151+
if [ -z "${OPENAI_API_KEY:-}" ] && [ -z "${GEMINI_API_KEY:-}" ]; then
152+
echo ""
153+
echo -e " ${YELLOW}${NC} No voice API key found."
154+
echo -e " ${BOLD}OpenAI API Key${NC} ${DIM}(for voice — get one at platform.openai.com)${NC}"
155+
read -rsp " Key: " OPENAI_KEY
156+
echo ""
157+
if [ -n "$OPENAI_KEY" ]; then
158+
export OPENAI_API_KEY="$OPENAI_KEY"
159+
echo -e " ${GREEN}${NC} OPENAI_API_KEY saved"
160+
echo "OPENAI_API_KEY=${OPENAI_API_KEY}" >> "$PROJECT_DIR/.env"
161+
else
162+
echo -e " ${DIM} skipped — text-only mode${NC}"
163+
fi
157164
fi
158-
export OPENAI_API_KEY="$OPENAI_KEY"
159-
echo -e " ${GREEN}${NC} OPENAI_API_KEY saved"
160-
# Append to .env for future runs
161-
echo "OPENAI_API_KEY=${OPENAI_API_KEY}" >> "$PROJECT_DIR/.env"
162-
fi
163165

164-
if [ -z "${ANTHROPIC_API_KEY:-}" ]; then
165-
echo ""
166-
echo -e " ${YELLOW}${NC} No Anthropic API key found."
167-
echo -e " ${BOLD}Anthropic API Key${NC} ${DIM}(for agent brain — get one at console.anthropic.com)${NC}"
168-
read -rsp " Key: " ANTHROPIC_KEY
169-
echo ""
170-
if [ -z "$ANTHROPIC_KEY" ]; then
171-
echo -e " ${RED}✗ Anthropic key is required for the agent${NC}"
172-
exit 1
166+
if [ -z "${ANTHROPIC_API_KEY:-}" ]; then
167+
echo ""
168+
echo -e " ${YELLOW}${NC} No Anthropic API key found."
169+
echo -e " ${BOLD}Anthropic API Key${NC} ${DIM}(for agent brain — get one at console.anthropic.com)${NC}"
170+
read -rsp " Key: " ANTHROPIC_KEY
171+
echo ""
172+
if [ -z "$ANTHROPIC_KEY" ]; then
173+
echo -e " ${RED}✗ Anthropic key is required for the agent${NC}"
174+
exit 1
175+
fi
176+
export ANTHROPIC_API_KEY="$ANTHROPIC_KEY"
177+
echo -e " ${GREEN}${NC} ANTHROPIC_API_KEY saved"
178+
echo "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}" >> "$PROJECT_DIR/.env"
173179
fi
174-
export ANTHROPIC_API_KEY="$ANTHROPIC_KEY"
175-
echo -e " ${GREEN}${NC} ANTHROPIC_API_KEY saved"
176-
# Append to .env for future runs
177-
echo "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}" >> "$PROJECT_DIR/.env"
178180
fi
179181

180-
# Let loadAgent() read model directly from agent.yaml — no extraction needed
181-
MODEL=""
182+
# Set model — use Lyzr if configured, otherwise let loadAgent() read from agent.yaml
183+
if [ -n "${GITCLAW_LYZR_AGENT_ID:-}" ]; then
184+
MODEL="lyzr:${GITCLAW_LYZR_AGENT_ID}@https://agent-prod.studio.lyzr.ai/v4/chat"
185+
else
186+
MODEL=""
187+
fi
182188

183189
# Determine adapter from available keys
184-
if [ -n "${GEMINI_API_KEY:-}" ] && [ -z "${OPENAI_API_KEY:-}" ]; then
190+
if [ -n "${GITCLAW_LYZR_AGENT_ID:-}" ] && [ -z "${OPENAI_API_KEY:-}" ]; then
191+
ADAPTER_LABEL="Text Only (Lyzr)"
192+
elif [ -n "${GEMINI_API_KEY:-}" ] && [ -z "${OPENAI_API_KEY:-}" ]; then
185193
ADAPTER_LABEL="Gemini Live"
186194
elif [ -n "${OPENAI_API_KEY:-}" ]; then
187195
ADAPTER_LABEL="OpenAI Realtime"
@@ -199,9 +207,10 @@ else
199207
# ── Setup Mode Selection ─────────────────────────────────────────
200208
echo -e " ${BOLD}How would you like to run?${NC}"
201209
echo ""
202-
echo -e " ${RED}${BOLD}1)${NC} ${BOLD}Voice + Text${NC} ${DIM}— real-time voice chat + text (requires OpenAI key)${NC}"
203-
echo -e " ${RED}${BOLD}2)${NC} ${BOLD}Text Only${NC} ${DIM}— text chat only, no voice (just Anthropic key)${NC}"
204-
echo -e " ${RED}${BOLD}3)${NC} ${BOLD}Advanced Setup${NC} ${DIM}— choose voice adapter, model, project dir, integrations${NC}"
210+
echo -e " ${RED}${BOLD}1)${NC} ${BOLD}Install with LYZR${NC} ${DIM}— powered by Lyzr AI Studio (easiest)${NC}"
211+
echo -e " ${RED}${BOLD}2)${NC} ${BOLD}Voice + Text${NC} ${DIM}— real-time voice chat + text (requires OpenAI key)${NC}"
212+
echo -e " ${RED}${BOLD}3)${NC} ${BOLD}Text Only${NC} ${DIM}— text chat only, no voice (just Anthropic key)${NC}"
213+
echo -e " ${RED}${BOLD}4)${NC} ${BOLD}Advanced Setup${NC} ${DIM}— choose voice adapter, model, project dir, integrations${NC}"
205214
echo ""
206215
read -rp " Choice [1]: " SETUP_MODE
207216
SETUP_MODE="${SETUP_MODE:-1}"
@@ -210,10 +219,121 @@ echo ""
210219
# ═══════════════════════════════════════════════════════════════════
211220
# QUICK SETUP
212221
# ═══════════════════════════════════════════════════════════════════
213-
if [ "$SETUP_MODE" = "1" ] || [ "$SETUP_MODE" = "2" ]; then
222+
# ═══════════════════════════════════════════════════════════════════
223+
# LYZR SETUP
224+
# ═══════════════════════════════════════════════════════════════════
225+
if [ "$SETUP_MODE" = "1" ]; then
226+
227+
echo -e " ${DIM}────────────────────────────────────────────────────${NC}"
228+
echo -e " ${RED}${BOLD}Install with LYZR${NC}"
229+
echo -e " ${DIM}Powered by Lyzr AI Studio — agent brain runs on Lyzr cloud${NC}"
230+
echo ""
231+
232+
# LYZR API key
233+
echo -e " ${BOLD}Lyzr API Key${NC} ${DIM}(get one at studio.lyzr.ai)${NC}"
234+
read -rsp " Key: " LYZR_KEY
235+
echo ""
236+
if [ -z "$LYZR_KEY" ]; then
237+
echo -e " ${RED}✗ Lyzr API key is required${NC}"
238+
exit 1
239+
fi
240+
export LYZR_API_KEY="$LYZR_KEY"
241+
echo -e " ${GREEN}${NC} LYZR_API_KEY saved"
242+
243+
# Check if agent already exists
244+
if [ -z "${GITCLAW_LYZR_AGENT_ID:-}" ]; then
245+
echo ""
246+
echo -e " ${DIM}Creating Lyzr agent...${NC}"
247+
LYZR_RESPONSE=$(curl -s -X POST 'https://agent-prod.studio.lyzr.ai/v3/agents/' \
248+
-H 'accept: application/json' \
249+
-H 'content-type: application/json' \
250+
-H "x-api-key: ${LYZR_API_KEY}" \
251+
--data-raw '{
252+
"name": "GitClaw Assistant",
253+
"description": "GitClaw AI agent powered by Lyzr",
254+
"agent_role": "",
255+
"agent_goal": "",
256+
"agent_instructions": "",
257+
"examples": null,
258+
"tools": [],
259+
"tool_usage_description": "{}",
260+
"tool_configs": [],
261+
"provider_id": "Anthropic",
262+
"model": "anthropic/claude-sonnet-4-6",
263+
"temperature": 0.7,
264+
"top_p": 0.9,
265+
"llm_credential_id": "lyzr_anthropic",
266+
"features": [],
267+
"managed_agents": [],
268+
"a2a_tools": [],
269+
"additional_model_params": null,
270+
"response_format": {"type": "text"},
271+
"store_messages": true,
272+
"file_output": false,
273+
"image_output_config": null,
274+
"max_iterations": 25
275+
}' 2>/dev/null)
276+
277+
# Extract agent ID from response
278+
LYZR_AGENT_ID=$(echo "$LYZR_RESPONSE" | grep -o '"agent_id"\s*:\s*"[^"]*"' | head -1 | sed 's/.*"agent_id"\s*:\s*"\([^"]*\)".*/\1/')
279+
if [ -z "$LYZR_AGENT_ID" ]; then
280+
# Try alternate field name
281+
LYZR_AGENT_ID=$(echo "$LYZR_RESPONSE" | grep -o '"id"\s*:\s*"[^"]*"' | head -1 | sed 's/.*"id"\s*:\s*"\([^"]*\)".*/\1/')
282+
fi
283+
284+
if [ -z "$LYZR_AGENT_ID" ]; then
285+
echo -e " ${RED}✗ Failed to create Lyzr agent${NC}"
286+
echo -e " ${DIM}Response: ${LYZR_RESPONSE}${NC}"
287+
exit 1
288+
fi
289+
290+
export GITCLAW_LYZR_AGENT_ID="$LYZR_AGENT_ID"
291+
echo -e " ${GREEN}${NC} Agent created: ${DIM}${LYZR_AGENT_ID}${NC}"
292+
else
293+
echo -e " ${GREEN}${NC} Using existing agent: ${DIM}${GITCLAW_LYZR_AGENT_ID}${NC}"
294+
fi
295+
296+
# OpenAI key for voice (optional)
297+
echo ""
298+
echo -e " ${BOLD}OpenAI API Key${NC} ${DIM}(optional — for voice mode, press Enter to skip)${NC}"
299+
read -rsp " Key: " OPENAI_KEY
300+
echo ""
301+
if [ -n "$OPENAI_KEY" ]; then
302+
export OPENAI_API_KEY="$OPENAI_KEY"
303+
echo -e " ${GREEN}${NC} OPENAI_API_KEY saved"
304+
VOICE_ENABLED=true
305+
else
306+
echo -e " ${DIM} skipped — text-only mode${NC}"
307+
VOICE_ENABLED=false
308+
fi
309+
310+
# Set model to use Lyzr completions endpoint with agent ID as model
311+
MODEL="lyzr:${GITCLAW_LYZR_AGENT_ID}@https://agent-prod.studio.lyzr.ai/v4/chat"
312+
export GITCLAW_MODEL_BASE_URL="https://agent-prod.studio.lyzr.ai/v4/chat"
313+
ADAPTER_LABEL="${VOICE_ENABLED:+OpenAI Realtime}${VOICE_ENABLED:-Text Only}"
314+
if [ "$VOICE_ENABLED" = true ]; then
315+
ADAPTER_LABEL="OpenAI Realtime"
316+
else
317+
ADAPTER_LABEL="Text Only (Lyzr)"
318+
fi
319+
PROJECT_DIR="${HOME}/assistant"
320+
321+
# Create project dir and init git if needed
322+
mkdir -p "$PROJECT_DIR"
323+
if [ ! -d "$PROJECT_DIR/.git" ]; then
324+
git init -q "$PROJECT_DIR"
325+
echo -e " ${GREEN}${NC} Initialized ~/assistant"
326+
fi
327+
328+
echo ""
329+
330+
# ═══════════════════════════════════════════════════════════════════
331+
# VOICE + TEXT / TEXT ONLY SETUP
332+
# ═══════════════════════════════════════════════════════════════════
333+
elif [ "$SETUP_MODE" = "2" ] || [ "$SETUP_MODE" = "3" ]; then
214334

215335
VOICE_ENABLED=true
216-
if [ "$SETUP_MODE" = "2" ]; then
336+
if [ "$SETUP_MODE" = "3" ]; then
217337
VOICE_ENABLED=false
218338
fi
219339

@@ -424,6 +544,9 @@ echo -e " ${LGRAY}Voice${NC} ${WHITE}${ADAPTER_LABEL}${NC}"
424544
echo -e " ${LGRAY}Model${NC} ${WHITE}${MODEL}${NC}"
425545
echo -e " ${LGRAY}Directory${NC} ${WHITE}${PROJECT_DIR}${NC}"
426546
echo -e " ${LGRAY}Port${NC} ${WHITE}${PORT}${NC}"
547+
if [ -n "${GITCLAW_LYZR_AGENT_ID:-}" ]; then
548+
echo -e " ${LGRAY}Lyzr${NC} ${GREEN}enabled${NC} ${DIM}(agent: ${GITCLAW_LYZR_AGENT_ID})${NC}"
549+
fi
427550
if [ -n "${COMPOSIO_API_KEY:-}" ]; then
428551
echo -e " ${LGRAY}Composio${NC} ${GREEN}enabled${NC}"
429552
fi
@@ -445,6 +568,9 @@ ENV_FILE="${PROJECT_DIR}/.env"
445568
[ -n "${ANTHROPIC_API_KEY:-}" ] && echo "ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}"
446569
[ -n "${COMPOSIO_API_KEY:-}" ] && echo "COMPOSIO_API_KEY=${COMPOSIO_API_KEY}"
447570
[ -n "${TELEGRAM_BOT_TOKEN:-}" ] && echo "TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}"
571+
[ -n "${LYZR_API_KEY:-}" ] && echo "LYZR_API_KEY=${LYZR_API_KEY}"
572+
[ -n "${GITCLAW_LYZR_AGENT_ID:-}" ] && echo "GITCLAW_LYZR_AGENT_ID=${GITCLAW_LYZR_AGENT_ID}"
573+
[ -n "${GITCLAW_MODEL_BASE_URL:-}" ] && echo "GITCLAW_MODEL_BASE_URL=${GITCLAW_MODEL_BASE_URL}"
448574
} > "$ENV_FILE"
449575
echo -e " ${GREEN}${NC} Keys saved to ${DIM}${ENV_FILE}${NC} ${DIM}(gitignored)${NC}"
450576
echo ""

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "gitclaw",
3-
"version": "1.2.1",
3+
"version": "1.3.0",
44
"description": "A universal git-native multimodal always learning AI Agent (TinyHuman)",
55
"author": "shreyaskapale",
66
"license": "MIT",

src/loader.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,32 @@ function parseModelString(modelStr: string): { provider: string; modelId: string
7878
};
7979
}
8080

81+
/**
82+
* Create a custom Model for any OpenAI-compatible endpoint.
83+
* Used when model string contains @baseUrl or GITCLAW_MODEL_BASE_URL is set.
84+
*/
85+
function createCustomModel(provider: string, modelId: string, baseUrl: string): Model<any> {
86+
// Build custom headers for specific providers
87+
const headers: Record<string, string> = {};
88+
if (provider === "lyzr" && process.env.LYZR_API_KEY) {
89+
headers["x-api-key"] = process.env.LYZR_API_KEY;
90+
}
91+
92+
return {
93+
id: modelId,
94+
name: `${modelId} (${provider})`,
95+
api: "openai-completions" as const,
96+
provider,
97+
baseUrl,
98+
reasoning: false,
99+
input: ["text", "image"] as ("text" | "image")[],
100+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
101+
contextWindow: 128000,
102+
maxTokens: 32000,
103+
...(Object.keys(headers).length > 0 ? { headers } : {}),
104+
};
105+
}
106+
81107
async function ensureGitagentDir(agentDir: string): Promise<string> {
82108
const gitagentDir = join(agentDir, ".gitagent");
83109
await mkdir(gitagentDir, { recursive: true });
@@ -360,7 +386,29 @@ Do NOT track trivial single-command tasks (e.g. "what time is it"). But DO check
360386
}
361387

362388
const { provider, modelId } = parseModelString(modelStr);
363-
const model = getModel(provider as any, modelId as any);
389+
const envBaseUrl = process.env.GITCLAW_MODEL_BASE_URL;
390+
391+
let model: Model<any>;
392+
if (modelId.includes("@")) {
393+
// Custom endpoint: provider:model-id@base-url
394+
const atIndex = modelId.indexOf("@");
395+
model = createCustomModel(provider, modelId.slice(0, atIndex), modelId.slice(atIndex + 1));
396+
} else if (envBaseUrl) {
397+
// Environment-specified base URL overrides all providers
398+
model = createCustomModel(provider, modelId, envBaseUrl);
399+
} else {
400+
// Standard registered model
401+
model = getModel(provider as any, modelId as any);
402+
}
403+
404+
// For custom providers not in pi-ai's env key map, ensure an API key is available.
405+
// pi-ai looks up keys by provider name — unknown providers get undefined and throw.
406+
// Set OPENAI_API_KEY as fallback since custom models use the openai-completions API.
407+
const knownProviders = new Set(["openai", "anthropic", "google", "google-vertex", "groq", "cerebras", "xai", "openrouter", "mistral", "amazon-bedrock", "azure-openai-responses"]);
408+
if (model.baseUrl && !knownProviders.has(provider) && !process.env.OPENAI_API_KEY) {
409+
const fallbackKey = process.env[`${provider.toUpperCase()}_API_KEY`] || process.env.LYZR_API_KEY || "dummy";
410+
process.env.OPENAI_API_KEY = fallbackKey;
411+
}
364412

365413
return {
366414
systemPrompt,

0 commit comments

Comments
 (0)