@dreb/coding-agent 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/buddy/buddy-manager.d.ts.map +1 -1
- package/dist/core/buddy/buddy-manager.js +1 -1
- package/dist/core/buddy/buddy-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +0 -3
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +0 -15
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/modes/interactive/components/buddy-component.d.ts.map +1 -1
- package/dist/modes/interactive/components/buddy-component.js +46 -39
- package/dist/modes/interactive/components/buddy-component.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +0 -2
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +0 -12
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +0 -7
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/settings.md +1 -1
- package/package.json +5 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buddy-manager.d.ts","sourceRoot":"","sources":["../../../src/core/buddy/buddy-manager.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAW,KAAK,EAAE,MAAM,UAAU,CAAC;AAQ/C,OAAO,KAAK,EAAE,UAAU,EAAmC,MAAM,kBAAkB,CAAC;AA4DpF,MAAM,WAAW,YAAY;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,YAAY,CAAC,CAezD;AAaD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIvE;AAiID,qBAAa,YAAY;IACxB,OAAO,CAAC,KAAK,CAA2B;IACxC,OAAO,CAAC,YAAY,CAA6B;IAEjD,mDAAmD;IACnD,QAAQ,IAAI,UAAU,GAAG,IAAI,CAE5B;IAED,oCAAoC;IACpC,cAAc,IAAI,OAAO,CAExB;IAED;;;;OAIG;IACH,IAAI,IAAI,UAAU,GAAG,IAAI,CAOxB;IAED;;;OAGG;IACG,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAmBzF;IAED;;OAEG;IACG,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAmB1F;IAED,iDAAiD;IACjD,OAAO,IAAI,MAAM,GAAG,IAAI,CAEvB;YAMa,UAAU;IAmDxB;;;OAGG;IACG,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAejD;IAED;;;OAGG;IACG,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgB1F;IAED,+DAA+D;IAC/D,cAAc,IAAI,MAAM,GAAG,IAAI,CAE9B;IAED,kEAAkE;IAClE,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAWtC;IAED,2EAA2E;IAC3E,gBAAgB,IAAI,IAAI,CAEvB;IAED,kDAAkD;IAClD,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAU/B;CACD","sourcesContent":["/**\n * BuddyManager — Core state machine for the buddy companion.\n *\n * Handles: bone rolling, soul generation, persistence, Ollama availability checks.\n * Bones are deterministic from hash(username + hostname + salt + rerollCount).\n * Soul is LLM-generated once on first hatch and persisted to buddy.json.\n */\n\nimport type { Context, Model } from \"@dreb/ai\";\nimport { completeSimple } from \"@dreb/ai\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { hostname } from \"os\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../../config.js\";\nimport { createBuddyRng } from \"./buddy-prng.js\";\nimport { rollEyes, rollHat, rollSpecies, rollStats } from \"./buddy-species.js\";\nimport type { BuddyState, CompanionBones, StoredCompanion } from \"./buddy-types.js\";\nimport { STAT_NAMES } from \"./buddy-types.js\";\n\nconst BUDDY_SALT = \"dreb-buddy-v1\";\nconst BUDDY_FILENAME = \"buddy.json\";\nconst DEFAULT_BACKSTORY = \"A mysterious past shrouded in legend.\";\n\n/** Base Ollama model config — id/name are set dynamically from available models */\nconst OLLAMA_MODEL_BASE: Omit<Model<\"openai-completions\">, \"id\" | \"name\"> = {\n\tapi: \"openai-completions\",\n\tprovider: \"ollama\",\n\tbaseUrl: \"http://localhost:11434/v1\",\n\treasoning: false,\n\tinput: [\"text\"],\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\tcontextWindow: 128000,\n\tmaxTokens: 2048,\n\tcompat: {\n\t\tsupportsDeveloperRole: false,\n\t\tsupportsReasoningEffort: false,\n\t},\n};\n\n/** Max words for buddy response before truncation */\nconst MAX_RESPONSE_WORDS = 300;\n\n/** Prompt for soul generation (uses parent LLM, not Ollama) */\nconst SOUL_GENERATION_PROMPT = `You are generating a companion character for a coding assistant terminal app. Based on the species, rarity, and stats below, generate a creative name, a one-sentence personality description, and a funny fictional backstory.\n\nSpecies: {species}\nRarity: {rarity}\nStats: {stats}\nShiny: {shiny}\n\nThe name must NOT be a common English word, programming keyword, tool name, or command. It should be unique and distinctive — a proper noun that won't appear in normal conversation. The name must be 4-8 characters and easy to type on a QWERTY keyboard — use only common letters (a-z, avoid q, x, z, j). Do not use species name as the name.\n\nRespond in EXACTLY this format:\nNAME: <name>\nPERSONALITY: <one sentence personality>\nBACKSTORY: <2-3 sentence elaborate fictional backstory — funny, absurd, or dramatic. Include specific events, places, former occupations>`;\n\n/** Prompt for buddy reactions via Ollama */\nconst REACTION_PROMPT = `You are {name}, a {species} companion in a terminal coding app. You are {personality}. Your backstory: {backstory}\n\nSomething just happened. React with a short, in-character quip based on the context below. Be specific — reference what actually happened, not just that something happened. Max 20 words. No quotes, no prefixes, just the quip.\n\nContext:\n{event}`;\n\nconst NAME_CALL_PROMPT = `You are {name}, a {species} companion in a terminal coding app. You are {personality}. Your backstory: {backstory}\n\nThe user just said: \"{message}\"\nRecent context: {context}\n\nRespond to what the user said directly. Be in-character, reference your backstory occasionally. Max 30 words. No quotes, no prefixes, just your response.`;\n\n// =============================================================================\n// Ollama availability\n// =============================================================================\n\nexport interface OllamaStatus {\n\tavailable: boolean;\n\tmodels: string[];\n\terror?: string;\n}\n\n/**\n * Check if Ollama is running and has models available.\n * Uses the /api/tags endpoint.\n */\nexport async function checkOllama(): Promise<OllamaStatus> {\n\ttry {\n\t\tconst res = await fetch(\"http://localhost:11434/api/tags\", { signal: AbortSignal.timeout(3000) });\n\t\tif (!res.ok) {\n\t\t\treturn { available: false, models: [], error: `Ollama returned ${res.status}` };\n\t\t}\n\t\tconst data = (await res.json()) as { models?: { name: string }[] };\n\t\tconst models = (data.models ?? []).map((m) => m.name);\n\t\tif (models.length === 0) {\n\t\t\treturn { available: false, models: [], error: \"No models installed. Run: ollama pull llama3.2\" };\n\t\t}\n\t\treturn { available: true, models };\n\t} catch {\n\t\treturn { available: false, models: [], error: \"Ollama is not running. Start it with: ollama serve\" };\n\t}\n}\n\n/**\n * Pick the Ollama model for the buddy.\n * Returns the stored model name if it's available, otherwise null.\n */\nfunction pickOllamaModel(storedModel: string | undefined, availableModels: string[]): string | null {\n\tif (!storedModel) return null;\n\t// Check if the stored model is installed (exact match or prefix match without tag)\n\tconst match = availableModels.find((m) => m === storedModel || m.startsWith(`${storedModel}:`));\n\treturn match ?? null;\n}\n\n/**\n * Truncate response to a maximum word count, appending \"...[truncated]\" if exceeded.\n */\nexport function truncateResponse(text: string, maxWords: number): string {\n\tconst words = text.split(/\\s+/);\n\tif (words.length <= maxWords) return text;\n\treturn `${words.slice(0, maxWords).join(\" \")} ...[truncated]`;\n}\n\n// =============================================================================\n// Persistence\n// =============================================================================\n\nfunction getBuddyPath(): string {\n\treturn join(getAgentDir(), BUDDY_FILENAME);\n}\n\nfunction loadStored(): StoredCompanion | null {\n\tconst path = getBuddyPath();\n\tif (!existsSync(path)) return null;\n\ttry {\n\t\tconst data = JSON.parse(readFileSync(path, \"utf-8\"));\n\t\t// Validate required fields\n\t\tif (\n\t\t\ttypeof data.rerollCount === \"number\" &&\n\t\t\ttypeof data.name === \"string\" &&\n\t\t\ttypeof data.personality === \"string\"\n\t\t) {\n\t\t\treturn {\n\t\t\t\trerollCount: data.rerollCount,\n\t\t\t\tname: data.name,\n\t\t\t\tpersonality: data.personality,\n\t\t\t\tbackstory: typeof data.backstory === \"string\" ? data.backstory : DEFAULT_BACKSTORY,\n\t\t\t\thatchedAt: data.hatchedAt ?? new Date().toISOString(),\n\t\t\t\t...(data.hidden !== undefined ? { hidden: data.hidden } : {}),\n\t\t\t\t...(typeof data.ollamaModel === \"string\" ? { ollamaModel: data.ollamaModel } : {}),\n\t\t\t};\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction saveStored(stored: StoredCompanion): void {\n\tconst path = getBuddyPath();\n\tconst dir = join(path, \"..\");\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n\twriteFileSync(path, JSON.stringify(stored, null, 2));\n}\n\n// =============================================================================\n// Bone rolling\n// =============================================================================\n\nfunction rollBones(rerollCount: number): CompanionBones {\n\tconst username = process.env.USER ?? process.env.LOGNAME ?? \"user\";\n\tconst host = hostname();\n\tconst rng = createBuddyRng(username, host, BUDDY_SALT, rerollCount);\n\n\t// Roll species + rarity\n\tconst { species, rarity } = rollSpecies(rng);\n\n\t// Roll shiny (1% chance)\n\tconst shiny = rng() < 0.01;\n\n\t// Roll eyes and hat\n\tconst eyes = rollEyes(rng);\n\tconst hat = rollHat(rng);\n\n\t// Roll stats\n\tconst stats = rollStats(rng, rarity);\n\n\treturn { species, rarity, shiny, stats, eyeStyle: eyes, hat };\n}\n\n// =============================================================================\n// Soul generation\n// =============================================================================\n\n/**\n * Generate a soul (name + personality + backstory) using the parent LLM.\n * Only called on first hatch or reroll.\n */\nasync function generateSoul(\n\tbones: CompanionBones,\n\tparentModel: Model<\"openai-completions\">,\n\tapiKey: string,\n): Promise<{ name: string; personality: string; backstory: string }> {\n\tconst statsStr = STAT_NAMES.map((s) => `${s}: ${bones.stats[s]}`).join(\", \");\n\tconst prompt = SOUL_GENERATION_PROMPT.replace(\"{species}\", bones.species)\n\t\t.replace(\"{rarity}\", bones.rarity)\n\t\t.replace(\"{stats}\", statsStr)\n\t\t.replace(\"{shiny}\", bones.shiny ? \"YES ✨\" : \"no\");\n\n\tconst context: Context = {\n\t\tsystemPrompt: \"Generate a companion character. Respond in the exact format requested.\",\n\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t};\n\n\ttry {\n\t\tconst response = await completeSimple(parentModel, context, { apiKey });\n\t\tconst text = response.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\");\n\n\t\t// Parse NAME: ... and PERSONALITY: ... and BACKSTORY: ...\n\t\tconst nameMatch = text.match(/NAME:\\s*(.+)/i);\n\t\tconst personalityMatch = text.match(/PERSONALITY:\\s*(.+)/i);\n\t\tconst backstoryMatch = text.match(/BACKSTORY:\\s*([\\s\\S]+)/i);\n\n\t\tlet name = nameMatch?.[1]?.trim() ?? bones.species;\n\t\tconst personality = personalityMatch?.[1]?.trim() ?? `A ${bones.rarity} ${bones.species} companion.`;\n\t\tconst backstory = backstoryMatch?.[1]?.trim() ?? DEFAULT_BACKSTORY;\n\n\t\t// Enforce name length\n\t\tif (name.length > 8) name = name.slice(0, 8);\n\n\t\treturn { name, personality, backstory };\n\t} catch {\n\t\t// Fallback if LLM fails\n\t\treturn {\n\t\t\tname: bones.species,\n\t\t\tpersonality: `A ${bones.rarity} ${bones.species} companion.`,\n\t\t\tbackstory: DEFAULT_BACKSTORY,\n\t\t};\n\t}\n}\n\n// =============================================================================\n// BuddyManager\n// =============================================================================\n\nexport class BuddyManager {\n\tprivate state: BuddyState | null = null;\n\tprivate ollamaStatus: OllamaStatus | null = null;\n\n\t/** Get current buddy state (null if not loaded) */\n\tgetState(): BuddyState | null {\n\t\treturn this.state;\n\t}\n\n\t/** Check if buddy exists on disk */\n\thasStoredBuddy(): boolean {\n\t\treturn loadStored() !== null;\n\t}\n\n\t/**\n\t * Load or create buddy state.\n\t * If stored buddy exists, loads soul and re-rolls bones.\n\t * If no stored buddy, returns null (need to hatch first).\n\t */\n\tload(): BuddyState | null {\n\t\tconst stored = loadStored();\n\t\tif (!stored) return null;\n\n\t\tconst bones = rollBones(stored.rerollCount);\n\t\tthis.state = { ...bones, ...stored };\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Hatch a new buddy. Generates bones, then uses parent LLM for soul.\n\t * Returns the new state.\n\t */\n\tasync hatch(parentModel: Model<\"openai-completions\">, apiKey: string): Promise<BuddyState> {\n\t\tconst stored = loadStored();\n\t\tconst rerollCount = stored?.rerollCount ?? 0;\n\n\t\tconst bones = rollBones(rerollCount);\n\t\tconst { name, personality, backstory } = await generateSoul(bones, parentModel, apiKey);\n\n\t\tconst newStored: StoredCompanion = {\n\t\t\trerollCount,\n\t\t\tname,\n\t\t\tpersonality,\n\t\t\tbackstory,\n\t\t\thatchedAt: new Date().toISOString(),\n\t\t\t...(stored?.ollamaModel ? { ollamaModel: stored.ollamaModel } : {}),\n\t\t};\n\n\t\tsaveStored(newStored);\n\t\tthis.state = { ...bones, ...newStored };\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Reroll the buddy — new bones + new soul.\n\t */\n\tasync reroll(parentModel: Model<\"openai-completions\">, apiKey: string): Promise<BuddyState> {\n\t\tconst stored = loadStored();\n\t\tconst newRerollCount = (stored?.rerollCount ?? 0) + 1;\n\n\t\tconst bones = rollBones(newRerollCount);\n\t\tconst { name, personality, backstory } = await generateSoul(bones, parentModel, apiKey);\n\n\t\tconst newStored: StoredCompanion = {\n\t\t\trerollCount: newRerollCount,\n\t\t\tname,\n\t\t\tpersonality,\n\t\t\tbackstory,\n\t\t\thatchedAt: new Date().toISOString(),\n\t\t\t...(stored?.ollamaModel ? { ollamaModel: stored.ollamaModel } : {}),\n\t\t};\n\n\t\tsaveStored(newStored);\n\t\tthis.state = { ...bones, ...newStored };\n\t\treturn this.state;\n\t}\n\n\t/** Get buddy's name (for name-call detection) */\n\tgetName(): string | null {\n\t\treturn this.state?.name ?? loadStored()?.name ?? null;\n\t}\n\n\t/**\n\t * Shared Ollama chat helper. Checks availability, picks model, runs completion.\n\t * Returns the response text, or null if Ollama is unavailable or no model configured.\n\t */\n\tprivate async ollamaChat(context: Context): Promise<string | null> {\n\t\t// Check Ollama lazily, retry if previously unavailable\n\t\tif (!this.ollamaStatus || !this.ollamaStatus.available) {\n\t\t\tthis.ollamaStatus = await checkOllama();\n\t\t}\n\t\tif (!this.ollamaStatus.available) return null;\n\n\t\tconst modelName = pickOllamaModel(this.state?.ollamaModel, this.ollamaStatus.models);\n\t\tif (!modelName) return null;\n\t\tconst model: Model<\"openai-completions\"> = {\n\t\t\t...OLLAMA_MODEL_BASE,\n\t\t\tid: modelName,\n\t\t\tname: `${modelName} (Ollama)`,\n\t\t};\n\n\t\tlet response: import(\"@dreb/ai\").AssistantMessage;\n\t\ttry {\n\t\t\tresponse = await completeSimple(model, context, {\n\t\t\t\tapiKey: \"ollama\",\n\t\t\t\tsignal: AbortSignal.timeout(120000),\n\t\t\t});\n\t\t} catch {\n\t\t\t// Safety net for unexpected sync errors (e.g. provider not found).\n\t\t\t// Normal runtime errors (timeout, connection) are handled via stopReason below.\n\t\t\tthis.ollamaStatus = null;\n\t\t\treturn null;\n\t\t}\n\n\t\t// Connection error — invalidate cache so next attempt re-checks Ollama\n\t\tif (response.stopReason === \"error\") {\n\t\t\tthis.ollamaStatus = null;\n\t\t\treturn null;\n\t\t}\n\n\t\t// Timeout or abort — preserve cache (model is just slow, Ollama is fine)\n\t\tif (response.stopReason === \"aborted\") {\n\t\t\treturn null;\n\t\t}\n\n\t\tlet text = response.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\")\n\t\t\t.trim();\n\n\t\t// Truncate overly long responses\n\t\ttext = truncateResponse(text, MAX_RESPONSE_WORDS);\n\n\t\treturn text || null;\n\t}\n\n\t/**\n\t * Generate a reaction to an event using Ollama.\n\t * Returns null if Ollama is unavailable.\n\t */\n\tasync react(event: string): Promise<string | null> {\n\t\tif (!this.state) return null;\n\n\t\tconst prompt = REACTION_PROMPT.replace(\"{name}\", this.state.name)\n\t\t\t.replace(\"{species}\", this.state.species)\n\t\t\t.replace(\"{personality}\", this.state.personality)\n\t\t\t.replace(\"{backstory}\", this.state.backstory)\n\t\t\t.replace(\"{event}\", event);\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: \"Respond with a short in-character quip. Max 20 words.\",\n\t\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t\t};\n\n\t\treturn this.ollamaChat(context);\n\t}\n\n\t/**\n\t * Respond to the user calling the buddy's name.\n\t * Uses Ollama for the response.\n\t */\n\tasync respondToNameCall(userMessage: string, recentContext: string): Promise<string | null> {\n\t\tif (!this.state) return null;\n\n\t\tconst prompt = NAME_CALL_PROMPT.replace(\"{name}\", this.state.name)\n\t\t\t.replace(\"{species}\", this.state.species)\n\t\t\t.replace(\"{personality}\", this.state.personality)\n\t\t\t.replace(\"{backstory}\", this.state.backstory)\n\t\t\t.replace(\"{message}\", userMessage)\n\t\t\t.replace(\"{context}\", recentContext);\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: \"Respond with a short friendly greeting. Max 30 words.\",\n\t\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t\t};\n\n\t\treturn this.ollamaChat(context);\n\t}\n\n\t/** Get the configured Ollama model name, or null if not set */\n\tgetOllamaModel(): string | null {\n\t\treturn this.state?.ollamaModel ?? loadStored()?.ollamaModel ?? null;\n\t}\n\n\t/** Set the Ollama model for buddy reactions. Persists to disk. */\n\tsetOllamaModel(modelName: string): void {\n\t\tconst stored = loadStored();\n\t\tif (stored) {\n\t\t\tstored.ollamaModel = modelName;\n\t\t\tsaveStored(stored);\n\t\t}\n\t\tif (this.state) {\n\t\t\tthis.state.ollamaModel = modelName;\n\t\t}\n\t\t// Invalidate Ollama status cache so next call picks up the new model\n\t\tthis.ollamaStatus = null;\n\t}\n\n\t/** Reset Ollama status cache (e.g. after detecting it became available) */\n\tresetOllamaCache(): void {\n\t\tthis.ollamaStatus = null;\n\t}\n\n\t/** Update the hidden flag in persisted storage */\n\tsetHidden(hidden: boolean): void {\n\t\tconst stored = loadStored();\n\t\tif (stored) {\n\t\t\tstored.hidden = hidden;\n\t\t\tsaveStored(stored);\n\t\t}\n\t\t// Keep in-memory state in sync so reset() reads current hidden flag\n\t\tif (this.state) {\n\t\t\tthis.state.hidden = hidden;\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"buddy-manager.d.ts","sourceRoot":"","sources":["../../../src/core/buddy/buddy-manager.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAW,KAAK,EAAE,MAAM,UAAU,CAAC;AAQ/C,OAAO,KAAK,EAAE,UAAU,EAAmC,MAAM,kBAAkB,CAAC;AA4DpF,MAAM,WAAW,YAAY;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,YAAY,CAAC,CAezD;AAaD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIvE;AAiID,qBAAa,YAAY;IACxB,OAAO,CAAC,KAAK,CAA2B;IACxC,OAAO,CAAC,YAAY,CAA6B;IAEjD,mDAAmD;IACnD,QAAQ,IAAI,UAAU,GAAG,IAAI,CAE5B;IAED,oCAAoC;IACpC,cAAc,IAAI,OAAO,CAExB;IAED;;;;OAIG;IACH,IAAI,IAAI,UAAU,GAAG,IAAI,CAOxB;IAED;;;OAGG;IACG,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAmBzF;IAED;;OAEG;IACG,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,oBAAoB,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAmB1F;IAED,iDAAiD;IACjD,OAAO,IAAI,MAAM,GAAG,IAAI,CAEvB;YAMa,UAAU;IAmDxB;;;OAGG;IACG,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAejD;IAED;;;OAGG;IACG,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgB1F;IAED,+DAA+D;IAC/D,cAAc,IAAI,MAAM,GAAG,IAAI,CAE9B;IAED,kEAAkE;IAClE,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAWtC;IAED,2EAA2E;IAC3E,gBAAgB,IAAI,IAAI,CAEvB;IAED,kDAAkD;IAClD,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAU/B;CACD","sourcesContent":["/**\n * BuddyManager — Core state machine for the buddy companion.\n *\n * Handles: bone rolling, soul generation, persistence, Ollama availability checks.\n * Bones are deterministic from hash(username + hostname + salt + rerollCount).\n * Soul is LLM-generated once on first hatch and persisted to buddy.json.\n */\n\nimport type { Context, Model } from \"@dreb/ai\";\nimport { completeSimple } from \"@dreb/ai\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { hostname } from \"os\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../../config.js\";\nimport { createBuddyRng } from \"./buddy-prng.js\";\nimport { rollEyes, rollHat, rollSpecies, rollStats } from \"./buddy-species.js\";\nimport type { BuddyState, CompanionBones, StoredCompanion } from \"./buddy-types.js\";\nimport { STAT_NAMES } from \"./buddy-types.js\";\n\nconst BUDDY_SALT = \"dreb-buddy-v1\";\nconst BUDDY_FILENAME = \"buddy.json\";\nconst DEFAULT_BACKSTORY = \"A mysterious past shrouded in legend.\";\n\n/** Base Ollama model config — id/name are set dynamically from available models */\nconst OLLAMA_MODEL_BASE: Omit<Model<\"openai-completions\">, \"id\" | \"name\"> = {\n\tapi: \"openai-completions\",\n\tprovider: \"ollama\",\n\tbaseUrl: \"http://localhost:11434/v1\",\n\treasoning: false,\n\tinput: [\"text\"],\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\tcontextWindow: 128000,\n\tmaxTokens: 2048,\n\tcompat: {\n\t\tsupportsDeveloperRole: false,\n\t\tsupportsReasoningEffort: false,\n\t},\n};\n\n/** Max words for buddy response before truncation */\nconst MAX_RESPONSE_WORDS = 60;\n\n/** Prompt for soul generation (uses parent LLM, not Ollama) */\nconst SOUL_GENERATION_PROMPT = `You are generating a companion character for a coding assistant terminal app. Based on the species, rarity, and stats below, generate a creative name, a one-sentence personality description, and a funny fictional backstory.\n\nSpecies: {species}\nRarity: {rarity}\nStats: {stats}\nShiny: {shiny}\n\nThe name must NOT be a common English word, programming keyword, tool name, or command. It should be unique and distinctive — a proper noun that won't appear in normal conversation. The name must be 4-8 characters and easy to type on a QWERTY keyboard — use only common letters (a-z, avoid q, x, z, j). Do not use species name as the name.\n\nRespond in EXACTLY this format:\nNAME: <name>\nPERSONALITY: <one sentence personality>\nBACKSTORY: <2-3 sentence elaborate fictional backstory — funny, absurd, or dramatic. Include specific events, places, former occupations>`;\n\n/** Prompt for buddy reactions via Ollama */\nconst REACTION_PROMPT = `You are {name}, a {species} companion in a terminal coding app. You are {personality}. Your backstory: {backstory}\n\nSomething just happened. React with a short, in-character quip based on the context below. Be specific — reference what actually happened, not just that something happened. Max 20 words. No quotes, no prefixes, just the quip.\n\nContext:\n{event}`;\n\nconst NAME_CALL_PROMPT = `You are {name}, a {species} companion in a terminal coding app. You are {personality}. Your backstory: {backstory}\n\nThe user just said: \"{message}\"\nRecent context: {context}\n\nRespond to what the user said directly. Be in-character, reference your backstory occasionally. Max 30 words. No quotes, no prefixes, just your response.`;\n\n// =============================================================================\n// Ollama availability\n// =============================================================================\n\nexport interface OllamaStatus {\n\tavailable: boolean;\n\tmodels: string[];\n\terror?: string;\n}\n\n/**\n * Check if Ollama is running and has models available.\n * Uses the /api/tags endpoint.\n */\nexport async function checkOllama(): Promise<OllamaStatus> {\n\ttry {\n\t\tconst res = await fetch(\"http://localhost:11434/api/tags\", { signal: AbortSignal.timeout(3000) });\n\t\tif (!res.ok) {\n\t\t\treturn { available: false, models: [], error: `Ollama returned ${res.status}` };\n\t\t}\n\t\tconst data = (await res.json()) as { models?: { name: string }[] };\n\t\tconst models = (data.models ?? []).map((m) => m.name);\n\t\tif (models.length === 0) {\n\t\t\treturn { available: false, models: [], error: \"No models installed. Run: ollama pull llama3.2\" };\n\t\t}\n\t\treturn { available: true, models };\n\t} catch {\n\t\treturn { available: false, models: [], error: \"Ollama is not running. Start it with: ollama serve\" };\n\t}\n}\n\n/**\n * Pick the Ollama model for the buddy.\n * Returns the stored model name if it's available, otherwise null.\n */\nfunction pickOllamaModel(storedModel: string | undefined, availableModels: string[]): string | null {\n\tif (!storedModel) return null;\n\t// Check if the stored model is installed (exact match or prefix match without tag)\n\tconst match = availableModels.find((m) => m === storedModel || m.startsWith(`${storedModel}:`));\n\treturn match ?? null;\n}\n\n/**\n * Truncate response to a maximum word count, appending \"...[truncated]\" if exceeded.\n */\nexport function truncateResponse(text: string, maxWords: number): string {\n\tconst words = text.split(/\\s+/);\n\tif (words.length <= maxWords) return text;\n\treturn `${words.slice(0, maxWords).join(\" \")} ...[truncated]`;\n}\n\n// =============================================================================\n// Persistence\n// =============================================================================\n\nfunction getBuddyPath(): string {\n\treturn join(getAgentDir(), BUDDY_FILENAME);\n}\n\nfunction loadStored(): StoredCompanion | null {\n\tconst path = getBuddyPath();\n\tif (!existsSync(path)) return null;\n\ttry {\n\t\tconst data = JSON.parse(readFileSync(path, \"utf-8\"));\n\t\t// Validate required fields\n\t\tif (\n\t\t\ttypeof data.rerollCount === \"number\" &&\n\t\t\ttypeof data.name === \"string\" &&\n\t\t\ttypeof data.personality === \"string\"\n\t\t) {\n\t\t\treturn {\n\t\t\t\trerollCount: data.rerollCount,\n\t\t\t\tname: data.name,\n\t\t\t\tpersonality: data.personality,\n\t\t\t\tbackstory: typeof data.backstory === \"string\" ? data.backstory : DEFAULT_BACKSTORY,\n\t\t\t\thatchedAt: data.hatchedAt ?? new Date().toISOString(),\n\t\t\t\t...(data.hidden !== undefined ? { hidden: data.hidden } : {}),\n\t\t\t\t...(typeof data.ollamaModel === \"string\" ? { ollamaModel: data.ollamaModel } : {}),\n\t\t\t};\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction saveStored(stored: StoredCompanion): void {\n\tconst path = getBuddyPath();\n\tconst dir = join(path, \"..\");\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n\twriteFileSync(path, JSON.stringify(stored, null, 2));\n}\n\n// =============================================================================\n// Bone rolling\n// =============================================================================\n\nfunction rollBones(rerollCount: number): CompanionBones {\n\tconst username = process.env.USER ?? process.env.LOGNAME ?? \"user\";\n\tconst host = hostname();\n\tconst rng = createBuddyRng(username, host, BUDDY_SALT, rerollCount);\n\n\t// Roll species + rarity\n\tconst { species, rarity } = rollSpecies(rng);\n\n\t// Roll shiny (1% chance)\n\tconst shiny = rng() < 0.01;\n\n\t// Roll eyes and hat\n\tconst eyes = rollEyes(rng);\n\tconst hat = rollHat(rng);\n\n\t// Roll stats\n\tconst stats = rollStats(rng, rarity);\n\n\treturn { species, rarity, shiny, stats, eyeStyle: eyes, hat };\n}\n\n// =============================================================================\n// Soul generation\n// =============================================================================\n\n/**\n * Generate a soul (name + personality + backstory) using the parent LLM.\n * Only called on first hatch or reroll.\n */\nasync function generateSoul(\n\tbones: CompanionBones,\n\tparentModel: Model<\"openai-completions\">,\n\tapiKey: string,\n): Promise<{ name: string; personality: string; backstory: string }> {\n\tconst statsStr = STAT_NAMES.map((s) => `${s}: ${bones.stats[s]}`).join(\", \");\n\tconst prompt = SOUL_GENERATION_PROMPT.replace(\"{species}\", bones.species)\n\t\t.replace(\"{rarity}\", bones.rarity)\n\t\t.replace(\"{stats}\", statsStr)\n\t\t.replace(\"{shiny}\", bones.shiny ? \"YES ✨\" : \"no\");\n\n\tconst context: Context = {\n\t\tsystemPrompt: \"Generate a companion character. Respond in the exact format requested.\",\n\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t};\n\n\ttry {\n\t\tconst response = await completeSimple(parentModel, context, { apiKey });\n\t\tconst text = response.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\");\n\n\t\t// Parse NAME: ... and PERSONALITY: ... and BACKSTORY: ...\n\t\tconst nameMatch = text.match(/NAME:\\s*(.+)/i);\n\t\tconst personalityMatch = text.match(/PERSONALITY:\\s*(.+)/i);\n\t\tconst backstoryMatch = text.match(/BACKSTORY:\\s*([\\s\\S]+)/i);\n\n\t\tlet name = nameMatch?.[1]?.trim() ?? bones.species;\n\t\tconst personality = personalityMatch?.[1]?.trim() ?? `A ${bones.rarity} ${bones.species} companion.`;\n\t\tconst backstory = backstoryMatch?.[1]?.trim() ?? DEFAULT_BACKSTORY;\n\n\t\t// Enforce name length\n\t\tif (name.length > 8) name = name.slice(0, 8);\n\n\t\treturn { name, personality, backstory };\n\t} catch {\n\t\t// Fallback if LLM fails\n\t\treturn {\n\t\t\tname: bones.species,\n\t\t\tpersonality: `A ${bones.rarity} ${bones.species} companion.`,\n\t\t\tbackstory: DEFAULT_BACKSTORY,\n\t\t};\n\t}\n}\n\n// =============================================================================\n// BuddyManager\n// =============================================================================\n\nexport class BuddyManager {\n\tprivate state: BuddyState | null = null;\n\tprivate ollamaStatus: OllamaStatus | null = null;\n\n\t/** Get current buddy state (null if not loaded) */\n\tgetState(): BuddyState | null {\n\t\treturn this.state;\n\t}\n\n\t/** Check if buddy exists on disk */\n\thasStoredBuddy(): boolean {\n\t\treturn loadStored() !== null;\n\t}\n\n\t/**\n\t * Load or create buddy state.\n\t * If stored buddy exists, loads soul and re-rolls bones.\n\t * If no stored buddy, returns null (need to hatch first).\n\t */\n\tload(): BuddyState | null {\n\t\tconst stored = loadStored();\n\t\tif (!stored) return null;\n\n\t\tconst bones = rollBones(stored.rerollCount);\n\t\tthis.state = { ...bones, ...stored };\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Hatch a new buddy. Generates bones, then uses parent LLM for soul.\n\t * Returns the new state.\n\t */\n\tasync hatch(parentModel: Model<\"openai-completions\">, apiKey: string): Promise<BuddyState> {\n\t\tconst stored = loadStored();\n\t\tconst rerollCount = stored?.rerollCount ?? 0;\n\n\t\tconst bones = rollBones(rerollCount);\n\t\tconst { name, personality, backstory } = await generateSoul(bones, parentModel, apiKey);\n\n\t\tconst newStored: StoredCompanion = {\n\t\t\trerollCount,\n\t\t\tname,\n\t\t\tpersonality,\n\t\t\tbackstory,\n\t\t\thatchedAt: new Date().toISOString(),\n\t\t\t...(stored?.ollamaModel ? { ollamaModel: stored.ollamaModel } : {}),\n\t\t};\n\n\t\tsaveStored(newStored);\n\t\tthis.state = { ...bones, ...newStored };\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Reroll the buddy — new bones + new soul.\n\t */\n\tasync reroll(parentModel: Model<\"openai-completions\">, apiKey: string): Promise<BuddyState> {\n\t\tconst stored = loadStored();\n\t\tconst newRerollCount = (stored?.rerollCount ?? 0) + 1;\n\n\t\tconst bones = rollBones(newRerollCount);\n\t\tconst { name, personality, backstory } = await generateSoul(bones, parentModel, apiKey);\n\n\t\tconst newStored: StoredCompanion = {\n\t\t\trerollCount: newRerollCount,\n\t\t\tname,\n\t\t\tpersonality,\n\t\t\tbackstory,\n\t\t\thatchedAt: new Date().toISOString(),\n\t\t\t...(stored?.ollamaModel ? { ollamaModel: stored.ollamaModel } : {}),\n\t\t};\n\n\t\tsaveStored(newStored);\n\t\tthis.state = { ...bones, ...newStored };\n\t\treturn this.state;\n\t}\n\n\t/** Get buddy's name (for name-call detection) */\n\tgetName(): string | null {\n\t\treturn this.state?.name ?? loadStored()?.name ?? null;\n\t}\n\n\t/**\n\t * Shared Ollama chat helper. Checks availability, picks model, runs completion.\n\t * Returns the response text, or null if Ollama is unavailable or no model configured.\n\t */\n\tprivate async ollamaChat(context: Context): Promise<string | null> {\n\t\t// Check Ollama lazily, retry if previously unavailable\n\t\tif (!this.ollamaStatus || !this.ollamaStatus.available) {\n\t\t\tthis.ollamaStatus = await checkOllama();\n\t\t}\n\t\tif (!this.ollamaStatus.available) return null;\n\n\t\tconst modelName = pickOllamaModel(this.state?.ollamaModel, this.ollamaStatus.models);\n\t\tif (!modelName) return null;\n\t\tconst model: Model<\"openai-completions\"> = {\n\t\t\t...OLLAMA_MODEL_BASE,\n\t\t\tid: modelName,\n\t\t\tname: `${modelName} (Ollama)`,\n\t\t};\n\n\t\tlet response: import(\"@dreb/ai\").AssistantMessage;\n\t\ttry {\n\t\t\tresponse = await completeSimple(model, context, {\n\t\t\t\tapiKey: \"ollama\",\n\t\t\t\tsignal: AbortSignal.timeout(120000),\n\t\t\t});\n\t\t} catch {\n\t\t\t// Safety net for unexpected sync errors (e.g. provider not found).\n\t\t\t// Normal runtime errors (timeout, connection) are handled via stopReason below.\n\t\t\tthis.ollamaStatus = null;\n\t\t\treturn null;\n\t\t}\n\n\t\t// Connection error — invalidate cache so next attempt re-checks Ollama\n\t\tif (response.stopReason === \"error\") {\n\t\t\tthis.ollamaStatus = null;\n\t\t\treturn null;\n\t\t}\n\n\t\t// Timeout or abort — preserve cache (model is just slow, Ollama is fine)\n\t\tif (response.stopReason === \"aborted\") {\n\t\t\treturn null;\n\t\t}\n\n\t\tlet text = response.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\")\n\t\t\t.trim();\n\n\t\t// Truncate overly long responses\n\t\ttext = truncateResponse(text, MAX_RESPONSE_WORDS);\n\n\t\treturn text || null;\n\t}\n\n\t/**\n\t * Generate a reaction to an event using Ollama.\n\t * Returns null if Ollama is unavailable.\n\t */\n\tasync react(event: string): Promise<string | null> {\n\t\tif (!this.state) return null;\n\n\t\tconst prompt = REACTION_PROMPT.replace(\"{name}\", this.state.name)\n\t\t\t.replace(\"{species}\", this.state.species)\n\t\t\t.replace(\"{personality}\", this.state.personality)\n\t\t\t.replace(\"{backstory}\", this.state.backstory)\n\t\t\t.replace(\"{event}\", event);\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: \"Respond with a short in-character quip. Max 20 words.\",\n\t\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t\t};\n\n\t\treturn this.ollamaChat(context);\n\t}\n\n\t/**\n\t * Respond to the user calling the buddy's name.\n\t * Uses Ollama for the response.\n\t */\n\tasync respondToNameCall(userMessage: string, recentContext: string): Promise<string | null> {\n\t\tif (!this.state) return null;\n\n\t\tconst prompt = NAME_CALL_PROMPT.replace(\"{name}\", this.state.name)\n\t\t\t.replace(\"{species}\", this.state.species)\n\t\t\t.replace(\"{personality}\", this.state.personality)\n\t\t\t.replace(\"{backstory}\", this.state.backstory)\n\t\t\t.replace(\"{message}\", userMessage)\n\t\t\t.replace(\"{context}\", recentContext);\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: \"Respond with a short friendly greeting. Max 30 words.\",\n\t\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t\t};\n\n\t\treturn this.ollamaChat(context);\n\t}\n\n\t/** Get the configured Ollama model name, or null if not set */\n\tgetOllamaModel(): string | null {\n\t\treturn this.state?.ollamaModel ?? loadStored()?.ollamaModel ?? null;\n\t}\n\n\t/** Set the Ollama model for buddy reactions. Persists to disk. */\n\tsetOllamaModel(modelName: string): void {\n\t\tconst stored = loadStored();\n\t\tif (stored) {\n\t\t\tstored.ollamaModel = modelName;\n\t\t\tsaveStored(stored);\n\t\t}\n\t\tif (this.state) {\n\t\t\tthis.state.ollamaModel = modelName;\n\t\t}\n\t\t// Invalidate Ollama status cache so next call picks up the new model\n\t\tthis.ollamaStatus = null;\n\t}\n\n\t/** Reset Ollama status cache (e.g. after detecting it became available) */\n\tresetOllamaCache(): void {\n\t\tthis.ollamaStatus = null;\n\t}\n\n\t/** Update the hidden flag in persisted storage */\n\tsetHidden(hidden: boolean): void {\n\t\tconst stored = loadStored();\n\t\tif (stored) {\n\t\t\tstored.hidden = hidden;\n\t\t\tsaveStored(stored);\n\t\t}\n\t\t// Keep in-memory state in sync so reset() reads current hidden flag\n\t\tif (this.state) {\n\t\t\tthis.state.hidden = hidden;\n\t\t}\n\t}\n}\n"]}
|
|
@@ -32,7 +32,7 @@ const OLLAMA_MODEL_BASE = {
|
|
|
32
32
|
},
|
|
33
33
|
};
|
|
34
34
|
/** Max words for buddy response before truncation */
|
|
35
|
-
const MAX_RESPONSE_WORDS =
|
|
35
|
+
const MAX_RESPONSE_WORDS = 60;
|
|
36
36
|
/** Prompt for soul generation (uses parent LLM, not Ollama) */
|
|
37
37
|
const SOUL_GENERATION_PROMPT = `You are generating a companion character for a coding assistant terminal app. Based on the species, rarity, and stats below, generate a creative name, a one-sentence personality description, and a funny fictional backstory.
|
|
38
38
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"buddy-manager.js","sourceRoot":"","sources":["../../../src/core/buddy/buddy-manager.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AACxE,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AAC9B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/E,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE9C,MAAM,UAAU,GAAG,eAAe,CAAC;AACnC,MAAM,cAAc,GAAG,YAAY,CAAC;AACpC,MAAM,iBAAiB,GAAG,uCAAuC,CAAC;AAElE,qFAAmF;AACnF,MAAM,iBAAiB,GAAqD;IAC3E,GAAG,EAAE,oBAAoB;IACzB,QAAQ,EAAE,QAAQ;IAClB,OAAO,EAAE,2BAA2B;IACpC,SAAS,EAAE,KAAK;IAChB,KAAK,EAAE,CAAC,MAAM,CAAC;IACf,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE;IAC1D,aAAa,EAAE,MAAM;IACrB,SAAS,EAAE,IAAI;IACf,MAAM,EAAE;QACP,qBAAqB,EAAE,KAAK;QAC5B,uBAAuB,EAAE,KAAK;KAC9B;CACD,CAAC;AAEF,qDAAqD;AACrD,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAE/B,+DAA+D;AAC/D,MAAM,sBAAsB,GAAG;;;;;;;;;;;;4IAY2G,CAAC;AAE3I,4CAA4C;AAC5C,MAAM,eAAe,GAAG;;;;;QAKhB,CAAC;AAET,MAAM,gBAAgB,GAAG;;;;;0JAKiI,CAAC;AAY3J;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,GAA0B;IAC1D,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,iCAAiC,EAAE,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAClG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACb,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,mBAAmB,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC;QACjF,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAoC,CAAC;QACnE,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACtD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,gDAAgD,EAAE,CAAC;QAClG,CAAC;QACD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,oDAAoD,EAAE,CAAC;IACtG,CAAC;AAAA,CACD;AAED;;;GAGG;AACH,SAAS,eAAe,CAAC,WAA+B,EAAE,eAAyB,EAAiB;IACnG,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAC;IAC9B,mFAAmF;IACnF,MAAM,KAAK,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,WAAW,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC;IAChG,OAAO,KAAK,IAAI,IAAI,CAAC;AAAA,CACrB;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,QAAgB,EAAU;IACxE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAChC,IAAI,KAAK,CAAC,MAAM,IAAI,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC1C,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC;AAAA,CAC9D;AAED,gFAAgF;AAChF,cAAc;AACd,gFAAgF;AAEhF,SAAS,YAAY,GAAW;IAC/B,OAAO,IAAI,CAAC,WAAW,EAAE,EAAE,cAAc,CAAC,CAAC;AAAA,CAC3C;AAED,SAAS,UAAU,GAA2B;IAC7C,MAAM,IAAI,GAAG,YAAY,EAAE,CAAC;IAC5B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,IAAI,CAAC;QACJ,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;QACrD,2BAA2B;QAC3B,IACC,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ;YACpC,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ;YAC7B,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ,EACnC,CAAC;YACF,OAAO;gBACN,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,WAAW,EAAE,IAAI,CAAC,WAAW;gBAC7B,SAAS,EAAE,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,iBAAiB;gBAClF,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACrD,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC7D,GAAG,CAAC,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAClF,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC;IACb,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAC;IACb,CAAC;AAAA,CACD;AAED,SAAS,UAAU,CAAC,MAAuB,EAAQ;IAClD,MAAM,IAAI,GAAG,YAAY,EAAE,CAAC;IAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAC7B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACtB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrC,CAAC;IACD,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAAA,CACrD;AAED,gFAAgF;AAChF,eAAe;AACf,gFAAgF;AAEhF,SAAS,SAAS,CAAC,WAAmB,EAAkB;IACvD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,MAAM,CAAC;IACnE,MAAM,IAAI,GAAG,QAAQ,EAAE,CAAC;IACxB,MAAM,GAAG,GAAG,cAAc,CAAC,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC;IAEpE,wBAAwB;IACxB,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAE7C,yBAAyB;IACzB,MAAM,KAAK,GAAG,GAAG,EAAE,GAAG,IAAI,CAAC;IAE3B,oBAAoB;IACpB,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC3B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAEzB,aAAa;IACb,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAErC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;AAAA,CAC9D;AAED,gFAAgF;AAChF,kBAAkB;AAClB,gFAAgF;AAEhF;;;GAGG;AACH,KAAK,UAAU,YAAY,CAC1B,KAAqB,EACrB,WAAwC,EACxC,MAAc,EACsD;IACpE,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7E,MAAM,MAAM,GAAG,sBAAsB,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,CAAC,OAAO,CAAC;SACvE,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC;SACjC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC;SAC5B,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,SAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAEnD,MAAM,OAAO,GAAY;QACxB,YAAY,EAAE,wEAAwE;QACtF,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;KACpE,CAAC;IAEF,IAAI,CAAC;QACJ,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,WAAW,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;QACxE,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO;aAC3B,MAAM,CAAC,CAAC,CAAC,EAAuC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;aACrE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;aAClB,IAAI,CAAC,EAAE,CAAC,CAAC;QAEX,0DAA0D;QAC1D,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;QAC9C,MAAM,gBAAgB,GAAG,IAAI,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;QAC5D,MAAM,cAAc,GAAG,IAAI,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAE7D,IAAI,IAAI,GAAG,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,KAAK,CAAC,OAAO,CAAC;QACnD,MAAM,WAAW,GAAG,gBAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,KAAK,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,OAAO,aAAa,CAAC;QACrG,MAAM,SAAS,GAAG,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,iBAAiB,CAAC;QAEnE,sBAAsB;QACtB,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAE7C,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACR,wBAAwB;QACxB,OAAO;YACN,IAAI,EAAE,KAAK,CAAC,OAAO;YACnB,WAAW,EAAE,KAAK,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,OAAO,aAAa;YAC5D,SAAS,EAAE,iBAAiB;SAC5B,CAAC;IACH,CAAC;AAAA,CACD;AAED,gFAAgF;AAChF,eAAe;AACf,gFAAgF;AAEhF,MAAM,OAAO,YAAY;IAChB,KAAK,GAAsB,IAAI,CAAC;IAChC,YAAY,GAAwB,IAAI,CAAC;IAEjD,mDAAmD;IACnD,QAAQ,GAAsB;QAC7B,OAAO,IAAI,CAAC,KAAK,CAAC;IAAA,CAClB;IAED,oCAAoC;IACpC,cAAc,GAAY;QACzB,OAAO,UAAU,EAAE,KAAK,IAAI,CAAC;IAAA,CAC7B;IAED;;;;OAIG;IACH,IAAI,GAAsB;QACzB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QAEzB,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QAC5C,IAAI,CAAC,KAAK,GAAG,EAAE,GAAG,KAAK,EAAE,GAAG,MAAM,EAAE,CAAC;QACrC,OAAO,IAAI,CAAC,KAAK,CAAC;IAAA,CAClB;IAED;;;OAGG;IACH,KAAK,CAAC,KAAK,CAAC,WAAwC,EAAE,MAAc,EAAuB;QAC1F,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,WAAW,GAAG,MAAM,EAAE,WAAW,IAAI,CAAC,CAAC;QAE7C,MAAM,KAAK,GAAG,SAAS,CAAC,WAAW,CAAC,CAAC;QACrC,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,MAAM,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;QAExF,MAAM,SAAS,GAAoB;YAClC,WAAW;YACX,IAAI;YACJ,WAAW;YACX,SAAS;YACT,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACnE,CAAC;QAEF,UAAU,CAAC,SAAS,CAAC,CAAC;QACtB,IAAI,CAAC,KAAK,GAAG,EAAE,GAAG,KAAK,EAAE,GAAG,SAAS,EAAE,CAAC;QACxC,OAAO,IAAI,CAAC,KAAK,CAAC;IAAA,CAClB;IAED;;OAEG;IACH,KAAK,CAAC,MAAM,CAAC,WAAwC,EAAE,MAAc,EAAuB;QAC3F,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,WAAW,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAEtD,MAAM,KAAK,GAAG,SAAS,CAAC,cAAc,CAAC,CAAC;QACxC,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,MAAM,YAAY,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;QAExF,MAAM,SAAS,GAAoB;YAClC,WAAW,EAAE,cAAc;YAC3B,IAAI;YACJ,WAAW;YACX,SAAS;YACT,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACnE,CAAC;QAEF,UAAU,CAAC,SAAS,CAAC,CAAC;QACtB,IAAI,CAAC,KAAK,GAAG,EAAE,GAAG,KAAK,EAAE,GAAG,SAAS,EAAE,CAAC;QACxC,OAAO,IAAI,CAAC,KAAK,CAAC;IAAA,CAClB;IAED,iDAAiD;IACjD,OAAO,GAAkB;QACxB,OAAO,IAAI,CAAC,KAAK,EAAE,IAAI,IAAI,UAAU,EAAE,EAAE,IAAI,IAAI,IAAI,CAAC;IAAA,CACtD;IAED;;;OAGG;IACK,KAAK,CAAC,UAAU,CAAC,OAAgB,EAA0B;QAClE,uDAAuD;QACvD,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,SAAS,EAAE,CAAC;YACxD,IAAI,CAAC,YAAY,GAAG,MAAM,WAAW,EAAE,CAAC;QACzC,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,SAAS;YAAE,OAAO,IAAI,CAAC;QAE9C,MAAM,SAAS,GAAG,eAAe,CAAC,IAAI,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QACrF,IAAI,CAAC,SAAS;YAAE,OAAO,IAAI,CAAC;QAC5B,MAAM,KAAK,GAAgC;YAC1C,GAAG,iBAAiB;YACpB,EAAE,EAAE,SAAS;YACb,IAAI,EAAE,GAAG,SAAS,WAAW;SAC7B,CAAC;QAEF,IAAI,QAA6C,CAAC;QAClD,IAAI,CAAC;YACJ,QAAQ,GAAG,MAAM,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE;gBAC/C,MAAM,EAAE,QAAQ;gBAChB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC;aACnC,CAAC,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACR,mEAAmE;YACnE,gFAAgF;YAChF,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;YACzB,OAAO,IAAI,CAAC;QACb,CAAC;QAED,yEAAuE;QACvE,IAAI,QAAQ,CAAC,UAAU,KAAK,OAAO,EAAE,CAAC;YACrC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;YACzB,OAAO,IAAI,CAAC;QACb,CAAC;QAED,2EAAyE;QACzE,IAAI,QAAQ,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;YACvC,OAAO,IAAI,CAAC;QACb,CAAC;QAED,IAAI,IAAI,GAAG,QAAQ,CAAC,OAAO;aACzB,MAAM,CAAC,CAAC,CAAC,EAAuC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC;aACrE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;aAClB,IAAI,CAAC,EAAE,CAAC;aACR,IAAI,EAAE,CAAC;QAET,iCAAiC;QACjC,IAAI,GAAG,gBAAgB,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC;QAElD,OAAO,IAAI,IAAI,IAAI,CAAC;IAAA,CACpB;IAED;;;OAGG;IACH,KAAK,CAAC,KAAK,CAAC,KAAa,EAA0B;QAClD,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QAE7B,MAAM,MAAM,GAAG,eAAe,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;aAC/D,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;aACxC,OAAO,CAAC,eAAe,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC;aAChD,OAAO,CAAC,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC;aAC5C,OAAO,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QAE5B,MAAM,OAAO,GAAY;YACxB,YAAY,EAAE,uDAAuD;YACrE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;SACpE,CAAC;QAEF,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAAA,CAChC;IAED;;;OAGG;IACH,KAAK,CAAC,iBAAiB,CAAC,WAAmB,EAAE,aAAqB,EAA0B;QAC3F,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QAE7B,MAAM,MAAM,GAAG,gBAAgB,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;aAChE,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;aACxC,OAAO,CAAC,eAAe,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC;aAChD,OAAO,CAAC,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC;aAC5C,OAAO,CAAC,WAAW,EAAE,WAAW,CAAC;aACjC,OAAO,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;QAEtC,MAAM,OAAO,GAAY;YACxB,YAAY,EAAE,uDAAuD;YACrE,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;SACpE,CAAC;QAEF,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAAA,CAChC;IAED,+DAA+D;IAC/D,cAAc,GAAkB;QAC/B,OAAO,IAAI,CAAC,KAAK,EAAE,WAAW,IAAI,UAAU,EAAE,EAAE,WAAW,IAAI,IAAI,CAAC;IAAA,CACpE;IAED,kEAAkE;IAClE,cAAc,CAAC,SAAiB,EAAQ;QACvC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,IAAI,MAAM,EAAE,CAAC;YACZ,MAAM,CAAC,WAAW,GAAG,SAAS,CAAC;YAC/B,UAAU,CAAC,MAAM,CAAC,CAAC;QACpB,CAAC;QACD,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,SAAS,CAAC;QACpC,CAAC;QACD,qEAAqE;QACrE,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;IAAA,CACzB;IAED,2EAA2E;IAC3E,gBAAgB,GAAS;QACxB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;IAAA,CACzB;IAED,kDAAkD;IAClD,SAAS,CAAC,MAAe,EAAQ;QAChC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;QAC5B,IAAI,MAAM,EAAE,CAAC;YACZ,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC;YACvB,UAAU,CAAC,MAAM,CAAC,CAAC;QACpB,CAAC;QACD,oEAAoE;QACpE,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAChB,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC;QAC5B,CAAC;IAAA,CACD;CACD","sourcesContent":["/**\n * BuddyManager — Core state machine for the buddy companion.\n *\n * Handles: bone rolling, soul generation, persistence, Ollama availability checks.\n * Bones are deterministic from hash(username + hostname + salt + rerollCount).\n * Soul is LLM-generated once on first hatch and persisted to buddy.json.\n */\n\nimport type { Context, Model } from \"@dreb/ai\";\nimport { completeSimple } from \"@dreb/ai\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { hostname } from \"os\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../../config.js\";\nimport { createBuddyRng } from \"./buddy-prng.js\";\nimport { rollEyes, rollHat, rollSpecies, rollStats } from \"./buddy-species.js\";\nimport type { BuddyState, CompanionBones, StoredCompanion } from \"./buddy-types.js\";\nimport { STAT_NAMES } from \"./buddy-types.js\";\n\nconst BUDDY_SALT = \"dreb-buddy-v1\";\nconst BUDDY_FILENAME = \"buddy.json\";\nconst DEFAULT_BACKSTORY = \"A mysterious past shrouded in legend.\";\n\n/** Base Ollama model config — id/name are set dynamically from available models */\nconst OLLAMA_MODEL_BASE: Omit<Model<\"openai-completions\">, \"id\" | \"name\"> = {\n\tapi: \"openai-completions\",\n\tprovider: \"ollama\",\n\tbaseUrl: \"http://localhost:11434/v1\",\n\treasoning: false,\n\tinput: [\"text\"],\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\tcontextWindow: 128000,\n\tmaxTokens: 2048,\n\tcompat: {\n\t\tsupportsDeveloperRole: false,\n\t\tsupportsReasoningEffort: false,\n\t},\n};\n\n/** Max words for buddy response before truncation */\nconst MAX_RESPONSE_WORDS = 300;\n\n/** Prompt for soul generation (uses parent LLM, not Ollama) */\nconst SOUL_GENERATION_PROMPT = `You are generating a companion character for a coding assistant terminal app. Based on the species, rarity, and stats below, generate a creative name, a one-sentence personality description, and a funny fictional backstory.\n\nSpecies: {species}\nRarity: {rarity}\nStats: {stats}\nShiny: {shiny}\n\nThe name must NOT be a common English word, programming keyword, tool name, or command. It should be unique and distinctive — a proper noun that won't appear in normal conversation. The name must be 4-8 characters and easy to type on a QWERTY keyboard — use only common letters (a-z, avoid q, x, z, j). Do not use species name as the name.\n\nRespond in EXACTLY this format:\nNAME: <name>\nPERSONALITY: <one sentence personality>\nBACKSTORY: <2-3 sentence elaborate fictional backstory — funny, absurd, or dramatic. Include specific events, places, former occupations>`;\n\n/** Prompt for buddy reactions via Ollama */\nconst REACTION_PROMPT = `You are {name}, a {species} companion in a terminal coding app. You are {personality}. Your backstory: {backstory}\n\nSomething just happened. React with a short, in-character quip based on the context below. Be specific — reference what actually happened, not just that something happened. Max 20 words. No quotes, no prefixes, just the quip.\n\nContext:\n{event}`;\n\nconst NAME_CALL_PROMPT = `You are {name}, a {species} companion in a terminal coding app. You are {personality}. Your backstory: {backstory}\n\nThe user just said: \"{message}\"\nRecent context: {context}\n\nRespond to what the user said directly. Be in-character, reference your backstory occasionally. Max 30 words. No quotes, no prefixes, just your response.`;\n\n// =============================================================================\n// Ollama availability\n// =============================================================================\n\nexport interface OllamaStatus {\n\tavailable: boolean;\n\tmodels: string[];\n\terror?: string;\n}\n\n/**\n * Check if Ollama is running and has models available.\n * Uses the /api/tags endpoint.\n */\nexport async function checkOllama(): Promise<OllamaStatus> {\n\ttry {\n\t\tconst res = await fetch(\"http://localhost:11434/api/tags\", { signal: AbortSignal.timeout(3000) });\n\t\tif (!res.ok) {\n\t\t\treturn { available: false, models: [], error: `Ollama returned ${res.status}` };\n\t\t}\n\t\tconst data = (await res.json()) as { models?: { name: string }[] };\n\t\tconst models = (data.models ?? []).map((m) => m.name);\n\t\tif (models.length === 0) {\n\t\t\treturn { available: false, models: [], error: \"No models installed. Run: ollama pull llama3.2\" };\n\t\t}\n\t\treturn { available: true, models };\n\t} catch {\n\t\treturn { available: false, models: [], error: \"Ollama is not running. Start it with: ollama serve\" };\n\t}\n}\n\n/**\n * Pick the Ollama model for the buddy.\n * Returns the stored model name if it's available, otherwise null.\n */\nfunction pickOllamaModel(storedModel: string | undefined, availableModels: string[]): string | null {\n\tif (!storedModel) return null;\n\t// Check if the stored model is installed (exact match or prefix match without tag)\n\tconst match = availableModels.find((m) => m === storedModel || m.startsWith(`${storedModel}:`));\n\treturn match ?? null;\n}\n\n/**\n * Truncate response to a maximum word count, appending \"...[truncated]\" if exceeded.\n */\nexport function truncateResponse(text: string, maxWords: number): string {\n\tconst words = text.split(/\\s+/);\n\tif (words.length <= maxWords) return text;\n\treturn `${words.slice(0, maxWords).join(\" \")} ...[truncated]`;\n}\n\n// =============================================================================\n// Persistence\n// =============================================================================\n\nfunction getBuddyPath(): string {\n\treturn join(getAgentDir(), BUDDY_FILENAME);\n}\n\nfunction loadStored(): StoredCompanion | null {\n\tconst path = getBuddyPath();\n\tif (!existsSync(path)) return null;\n\ttry {\n\t\tconst data = JSON.parse(readFileSync(path, \"utf-8\"));\n\t\t// Validate required fields\n\t\tif (\n\t\t\ttypeof data.rerollCount === \"number\" &&\n\t\t\ttypeof data.name === \"string\" &&\n\t\t\ttypeof data.personality === \"string\"\n\t\t) {\n\t\t\treturn {\n\t\t\t\trerollCount: data.rerollCount,\n\t\t\t\tname: data.name,\n\t\t\t\tpersonality: data.personality,\n\t\t\t\tbackstory: typeof data.backstory === \"string\" ? data.backstory : DEFAULT_BACKSTORY,\n\t\t\t\thatchedAt: data.hatchedAt ?? new Date().toISOString(),\n\t\t\t\t...(data.hidden !== undefined ? { hidden: data.hidden } : {}),\n\t\t\t\t...(typeof data.ollamaModel === \"string\" ? { ollamaModel: data.ollamaModel } : {}),\n\t\t\t};\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction saveStored(stored: StoredCompanion): void {\n\tconst path = getBuddyPath();\n\tconst dir = join(path, \"..\");\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n\twriteFileSync(path, JSON.stringify(stored, null, 2));\n}\n\n// =============================================================================\n// Bone rolling\n// =============================================================================\n\nfunction rollBones(rerollCount: number): CompanionBones {\n\tconst username = process.env.USER ?? process.env.LOGNAME ?? \"user\";\n\tconst host = hostname();\n\tconst rng = createBuddyRng(username, host, BUDDY_SALT, rerollCount);\n\n\t// Roll species + rarity\n\tconst { species, rarity } = rollSpecies(rng);\n\n\t// Roll shiny (1% chance)\n\tconst shiny = rng() < 0.01;\n\n\t// Roll eyes and hat\n\tconst eyes = rollEyes(rng);\n\tconst hat = rollHat(rng);\n\n\t// Roll stats\n\tconst stats = rollStats(rng, rarity);\n\n\treturn { species, rarity, shiny, stats, eyeStyle: eyes, hat };\n}\n\n// =============================================================================\n// Soul generation\n// =============================================================================\n\n/**\n * Generate a soul (name + personality + backstory) using the parent LLM.\n * Only called on first hatch or reroll.\n */\nasync function generateSoul(\n\tbones: CompanionBones,\n\tparentModel: Model<\"openai-completions\">,\n\tapiKey: string,\n): Promise<{ name: string; personality: string; backstory: string }> {\n\tconst statsStr = STAT_NAMES.map((s) => `${s}: ${bones.stats[s]}`).join(\", \");\n\tconst prompt = SOUL_GENERATION_PROMPT.replace(\"{species}\", bones.species)\n\t\t.replace(\"{rarity}\", bones.rarity)\n\t\t.replace(\"{stats}\", statsStr)\n\t\t.replace(\"{shiny}\", bones.shiny ? \"YES ✨\" : \"no\");\n\n\tconst context: Context = {\n\t\tsystemPrompt: \"Generate a companion character. Respond in the exact format requested.\",\n\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t};\n\n\ttry {\n\t\tconst response = await completeSimple(parentModel, context, { apiKey });\n\t\tconst text = response.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\");\n\n\t\t// Parse NAME: ... and PERSONALITY: ... and BACKSTORY: ...\n\t\tconst nameMatch = text.match(/NAME:\\s*(.+)/i);\n\t\tconst personalityMatch = text.match(/PERSONALITY:\\s*(.+)/i);\n\t\tconst backstoryMatch = text.match(/BACKSTORY:\\s*([\\s\\S]+)/i);\n\n\t\tlet name = nameMatch?.[1]?.trim() ?? bones.species;\n\t\tconst personality = personalityMatch?.[1]?.trim() ?? `A ${bones.rarity} ${bones.species} companion.`;\n\t\tconst backstory = backstoryMatch?.[1]?.trim() ?? DEFAULT_BACKSTORY;\n\n\t\t// Enforce name length\n\t\tif (name.length > 8) name = name.slice(0, 8);\n\n\t\treturn { name, personality, backstory };\n\t} catch {\n\t\t// Fallback if LLM fails\n\t\treturn {\n\t\t\tname: bones.species,\n\t\t\tpersonality: `A ${bones.rarity} ${bones.species} companion.`,\n\t\t\tbackstory: DEFAULT_BACKSTORY,\n\t\t};\n\t}\n}\n\n// =============================================================================\n// BuddyManager\n// =============================================================================\n\nexport class BuddyManager {\n\tprivate state: BuddyState | null = null;\n\tprivate ollamaStatus: OllamaStatus | null = null;\n\n\t/** Get current buddy state (null if not loaded) */\n\tgetState(): BuddyState | null {\n\t\treturn this.state;\n\t}\n\n\t/** Check if buddy exists on disk */\n\thasStoredBuddy(): boolean {\n\t\treturn loadStored() !== null;\n\t}\n\n\t/**\n\t * Load or create buddy state.\n\t * If stored buddy exists, loads soul and re-rolls bones.\n\t * If no stored buddy, returns null (need to hatch first).\n\t */\n\tload(): BuddyState | null {\n\t\tconst stored = loadStored();\n\t\tif (!stored) return null;\n\n\t\tconst bones = rollBones(stored.rerollCount);\n\t\tthis.state = { ...bones, ...stored };\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Hatch a new buddy. Generates bones, then uses parent LLM for soul.\n\t * Returns the new state.\n\t */\n\tasync hatch(parentModel: Model<\"openai-completions\">, apiKey: string): Promise<BuddyState> {\n\t\tconst stored = loadStored();\n\t\tconst rerollCount = stored?.rerollCount ?? 0;\n\n\t\tconst bones = rollBones(rerollCount);\n\t\tconst { name, personality, backstory } = await generateSoul(bones, parentModel, apiKey);\n\n\t\tconst newStored: StoredCompanion = {\n\t\t\trerollCount,\n\t\t\tname,\n\t\t\tpersonality,\n\t\t\tbackstory,\n\t\t\thatchedAt: new Date().toISOString(),\n\t\t\t...(stored?.ollamaModel ? { ollamaModel: stored.ollamaModel } : {}),\n\t\t};\n\n\t\tsaveStored(newStored);\n\t\tthis.state = { ...bones, ...newStored };\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Reroll the buddy — new bones + new soul.\n\t */\n\tasync reroll(parentModel: Model<\"openai-completions\">, apiKey: string): Promise<BuddyState> {\n\t\tconst stored = loadStored();\n\t\tconst newRerollCount = (stored?.rerollCount ?? 0) + 1;\n\n\t\tconst bones = rollBones(newRerollCount);\n\t\tconst { name, personality, backstory } = await generateSoul(bones, parentModel, apiKey);\n\n\t\tconst newStored: StoredCompanion = {\n\t\t\trerollCount: newRerollCount,\n\t\t\tname,\n\t\t\tpersonality,\n\t\t\tbackstory,\n\t\t\thatchedAt: new Date().toISOString(),\n\t\t\t...(stored?.ollamaModel ? { ollamaModel: stored.ollamaModel } : {}),\n\t\t};\n\n\t\tsaveStored(newStored);\n\t\tthis.state = { ...bones, ...newStored };\n\t\treturn this.state;\n\t}\n\n\t/** Get buddy's name (for name-call detection) */\n\tgetName(): string | null {\n\t\treturn this.state?.name ?? loadStored()?.name ?? null;\n\t}\n\n\t/**\n\t * Shared Ollama chat helper. Checks availability, picks model, runs completion.\n\t * Returns the response text, or null if Ollama is unavailable or no model configured.\n\t */\n\tprivate async ollamaChat(context: Context): Promise<string | null> {\n\t\t// Check Ollama lazily, retry if previously unavailable\n\t\tif (!this.ollamaStatus || !this.ollamaStatus.available) {\n\t\t\tthis.ollamaStatus = await checkOllama();\n\t\t}\n\t\tif (!this.ollamaStatus.available) return null;\n\n\t\tconst modelName = pickOllamaModel(this.state?.ollamaModel, this.ollamaStatus.models);\n\t\tif (!modelName) return null;\n\t\tconst model: Model<\"openai-completions\"> = {\n\t\t\t...OLLAMA_MODEL_BASE,\n\t\t\tid: modelName,\n\t\t\tname: `${modelName} (Ollama)`,\n\t\t};\n\n\t\tlet response: import(\"@dreb/ai\").AssistantMessage;\n\t\ttry {\n\t\t\tresponse = await completeSimple(model, context, {\n\t\t\t\tapiKey: \"ollama\",\n\t\t\t\tsignal: AbortSignal.timeout(120000),\n\t\t\t});\n\t\t} catch {\n\t\t\t// Safety net for unexpected sync errors (e.g. provider not found).\n\t\t\t// Normal runtime errors (timeout, connection) are handled via stopReason below.\n\t\t\tthis.ollamaStatus = null;\n\t\t\treturn null;\n\t\t}\n\n\t\t// Connection error — invalidate cache so next attempt re-checks Ollama\n\t\tif (response.stopReason === \"error\") {\n\t\t\tthis.ollamaStatus = null;\n\t\t\treturn null;\n\t\t}\n\n\t\t// Timeout or abort — preserve cache (model is just slow, Ollama is fine)\n\t\tif (response.stopReason === \"aborted\") {\n\t\t\treturn null;\n\t\t}\n\n\t\tlet text = response.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\")\n\t\t\t.trim();\n\n\t\t// Truncate overly long responses\n\t\ttext = truncateResponse(text, MAX_RESPONSE_WORDS);\n\n\t\treturn text || null;\n\t}\n\n\t/**\n\t * Generate a reaction to an event using Ollama.\n\t * Returns null if Ollama is unavailable.\n\t */\n\tasync react(event: string): Promise<string | null> {\n\t\tif (!this.state) return null;\n\n\t\tconst prompt = REACTION_PROMPT.replace(\"{name}\", this.state.name)\n\t\t\t.replace(\"{species}\", this.state.species)\n\t\t\t.replace(\"{personality}\", this.state.personality)\n\t\t\t.replace(\"{backstory}\", this.state.backstory)\n\t\t\t.replace(\"{event}\", event);\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: \"Respond with a short in-character quip. Max 20 words.\",\n\t\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t\t};\n\n\t\treturn this.ollamaChat(context);\n\t}\n\n\t/**\n\t * Respond to the user calling the buddy's name.\n\t * Uses Ollama for the response.\n\t */\n\tasync respondToNameCall(userMessage: string, recentContext: string): Promise<string | null> {\n\t\tif (!this.state) return null;\n\n\t\tconst prompt = NAME_CALL_PROMPT.replace(\"{name}\", this.state.name)\n\t\t\t.replace(\"{species}\", this.state.species)\n\t\t\t.replace(\"{personality}\", this.state.personality)\n\t\t\t.replace(\"{backstory}\", this.state.backstory)\n\t\t\t.replace(\"{message}\", userMessage)\n\t\t\t.replace(\"{context}\", recentContext);\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: \"Respond with a short friendly greeting. Max 30 words.\",\n\t\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t\t};\n\n\t\treturn this.ollamaChat(context);\n\t}\n\n\t/** Get the configured Ollama model name, or null if not set */\n\tgetOllamaModel(): string | null {\n\t\treturn this.state?.ollamaModel ?? loadStored()?.ollamaModel ?? null;\n\t}\n\n\t/** Set the Ollama model for buddy reactions. Persists to disk. */\n\tsetOllamaModel(modelName: string): void {\n\t\tconst stored = loadStored();\n\t\tif (stored) {\n\t\t\tstored.ollamaModel = modelName;\n\t\t\tsaveStored(stored);\n\t\t}\n\t\tif (this.state) {\n\t\t\tthis.state.ollamaModel = modelName;\n\t\t}\n\t\t// Invalidate Ollama status cache so next call picks up the new model\n\t\tthis.ollamaStatus = null;\n\t}\n\n\t/** Reset Ollama status cache (e.g. after detecting it became available) */\n\tresetOllamaCache(): void {\n\t\tthis.ollamaStatus = null;\n\t}\n\n\t/** Update the hidden flag in persisted storage */\n\tsetHidden(hidden: boolean): void {\n\t\tconst stored = loadStored();\n\t\tif (stored) {\n\t\t\tstored.hidden = hidden;\n\t\t\tsaveStored(stored);\n\t\t}\n\t\t// Keep in-memory state in sync so reset() reads current hidden flag\n\t\tif (this.state) {\n\t\t\tthis.state.hidden = hidden;\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"buddy-manager.js","sourceRoot":"","sources":["../../../src/core/buddy/buddy-manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * BuddyManager — Core state machine for the buddy companion.\n *\n * Handles: bone rolling, soul generation, persistence, Ollama availability checks.\n * Bones are deterministic from hash(username + hostname + salt + rerollCount).\n * Soul is LLM-generated once on first hatch and persisted to buddy.json.\n */\n\nimport type { Context, Model } from \"@dreb/ai\";\nimport { completeSimple } from \"@dreb/ai\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { hostname } from \"os\";\nimport { join } from \"path\";\nimport { getAgentDir } from \"../../config.js\";\nimport { createBuddyRng } from \"./buddy-prng.js\";\nimport { rollEyes, rollHat, rollSpecies, rollStats } from \"./buddy-species.js\";\nimport type { BuddyState, CompanionBones, StoredCompanion } from \"./buddy-types.js\";\nimport { STAT_NAMES } from \"./buddy-types.js\";\n\nconst BUDDY_SALT = \"dreb-buddy-v1\";\nconst BUDDY_FILENAME = \"buddy.json\";\nconst DEFAULT_BACKSTORY = \"A mysterious past shrouded in legend.\";\n\n/** Base Ollama model config — id/name are set dynamically from available models */\nconst OLLAMA_MODEL_BASE: Omit<Model<\"openai-completions\">, \"id\" | \"name\"> = {\n\tapi: \"openai-completions\",\n\tprovider: \"ollama\",\n\tbaseUrl: \"http://localhost:11434/v1\",\n\treasoning: false,\n\tinput: [\"text\"],\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\tcontextWindow: 128000,\n\tmaxTokens: 2048,\n\tcompat: {\n\t\tsupportsDeveloperRole: false,\n\t\tsupportsReasoningEffort: false,\n\t},\n};\n\n/** Max words for buddy response before truncation */\nconst MAX_RESPONSE_WORDS = 60;\n\n/** Prompt for soul generation (uses parent LLM, not Ollama) */\nconst SOUL_GENERATION_PROMPT = `You are generating a companion character for a coding assistant terminal app. Based on the species, rarity, and stats below, generate a creative name, a one-sentence personality description, and a funny fictional backstory.\n\nSpecies: {species}\nRarity: {rarity}\nStats: {stats}\nShiny: {shiny}\n\nThe name must NOT be a common English word, programming keyword, tool name, or command. It should be unique and distinctive — a proper noun that won't appear in normal conversation. The name must be 4-8 characters and easy to type on a QWERTY keyboard — use only common letters (a-z, avoid q, x, z, j). Do not use species name as the name.\n\nRespond in EXACTLY this format:\nNAME: <name>\nPERSONALITY: <one sentence personality>\nBACKSTORY: <2-3 sentence elaborate fictional backstory — funny, absurd, or dramatic. Include specific events, places, former occupations>`;\n\n/** Prompt for buddy reactions via Ollama */\nconst REACTION_PROMPT = `You are {name}, a {species} companion in a terminal coding app. You are {personality}. Your backstory: {backstory}\n\nSomething just happened. React with a short, in-character quip based on the context below. Be specific — reference what actually happened, not just that something happened. Max 20 words. No quotes, no prefixes, just the quip.\n\nContext:\n{event}`;\n\nconst NAME_CALL_PROMPT = `You are {name}, a {species} companion in a terminal coding app. You are {personality}. Your backstory: {backstory}\n\nThe user just said: \"{message}\"\nRecent context: {context}\n\nRespond to what the user said directly. Be in-character, reference your backstory occasionally. Max 30 words. No quotes, no prefixes, just your response.`;\n\n// =============================================================================\n// Ollama availability\n// =============================================================================\n\nexport interface OllamaStatus {\n\tavailable: boolean;\n\tmodels: string[];\n\terror?: string;\n}\n\n/**\n * Check if Ollama is running and has models available.\n * Uses the /api/tags endpoint.\n */\nexport async function checkOllama(): Promise<OllamaStatus> {\n\ttry {\n\t\tconst res = await fetch(\"http://localhost:11434/api/tags\", { signal: AbortSignal.timeout(3000) });\n\t\tif (!res.ok) {\n\t\t\treturn { available: false, models: [], error: `Ollama returned ${res.status}` };\n\t\t}\n\t\tconst data = (await res.json()) as { models?: { name: string }[] };\n\t\tconst models = (data.models ?? []).map((m) => m.name);\n\t\tif (models.length === 0) {\n\t\t\treturn { available: false, models: [], error: \"No models installed. Run: ollama pull llama3.2\" };\n\t\t}\n\t\treturn { available: true, models };\n\t} catch {\n\t\treturn { available: false, models: [], error: \"Ollama is not running. Start it with: ollama serve\" };\n\t}\n}\n\n/**\n * Pick the Ollama model for the buddy.\n * Returns the stored model name if it's available, otherwise null.\n */\nfunction pickOllamaModel(storedModel: string | undefined, availableModels: string[]): string | null {\n\tif (!storedModel) return null;\n\t// Check if the stored model is installed (exact match or prefix match without tag)\n\tconst match = availableModels.find((m) => m === storedModel || m.startsWith(`${storedModel}:`));\n\treturn match ?? null;\n}\n\n/**\n * Truncate response to a maximum word count, appending \"...[truncated]\" if exceeded.\n */\nexport function truncateResponse(text: string, maxWords: number): string {\n\tconst words = text.split(/\\s+/);\n\tif (words.length <= maxWords) return text;\n\treturn `${words.slice(0, maxWords).join(\" \")} ...[truncated]`;\n}\n\n// =============================================================================\n// Persistence\n// =============================================================================\n\nfunction getBuddyPath(): string {\n\treturn join(getAgentDir(), BUDDY_FILENAME);\n}\n\nfunction loadStored(): StoredCompanion | null {\n\tconst path = getBuddyPath();\n\tif (!existsSync(path)) return null;\n\ttry {\n\t\tconst data = JSON.parse(readFileSync(path, \"utf-8\"));\n\t\t// Validate required fields\n\t\tif (\n\t\t\ttypeof data.rerollCount === \"number\" &&\n\t\t\ttypeof data.name === \"string\" &&\n\t\t\ttypeof data.personality === \"string\"\n\t\t) {\n\t\t\treturn {\n\t\t\t\trerollCount: data.rerollCount,\n\t\t\t\tname: data.name,\n\t\t\t\tpersonality: data.personality,\n\t\t\t\tbackstory: typeof data.backstory === \"string\" ? data.backstory : DEFAULT_BACKSTORY,\n\t\t\t\thatchedAt: data.hatchedAt ?? new Date().toISOString(),\n\t\t\t\t...(data.hidden !== undefined ? { hidden: data.hidden } : {}),\n\t\t\t\t...(typeof data.ollamaModel === \"string\" ? { ollamaModel: data.ollamaModel } : {}),\n\t\t\t};\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction saveStored(stored: StoredCompanion): void {\n\tconst path = getBuddyPath();\n\tconst dir = join(path, \"..\");\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n\twriteFileSync(path, JSON.stringify(stored, null, 2));\n}\n\n// =============================================================================\n// Bone rolling\n// =============================================================================\n\nfunction rollBones(rerollCount: number): CompanionBones {\n\tconst username = process.env.USER ?? process.env.LOGNAME ?? \"user\";\n\tconst host = hostname();\n\tconst rng = createBuddyRng(username, host, BUDDY_SALT, rerollCount);\n\n\t// Roll species + rarity\n\tconst { species, rarity } = rollSpecies(rng);\n\n\t// Roll shiny (1% chance)\n\tconst shiny = rng() < 0.01;\n\n\t// Roll eyes and hat\n\tconst eyes = rollEyes(rng);\n\tconst hat = rollHat(rng);\n\n\t// Roll stats\n\tconst stats = rollStats(rng, rarity);\n\n\treturn { species, rarity, shiny, stats, eyeStyle: eyes, hat };\n}\n\n// =============================================================================\n// Soul generation\n// =============================================================================\n\n/**\n * Generate a soul (name + personality + backstory) using the parent LLM.\n * Only called on first hatch or reroll.\n */\nasync function generateSoul(\n\tbones: CompanionBones,\n\tparentModel: Model<\"openai-completions\">,\n\tapiKey: string,\n): Promise<{ name: string; personality: string; backstory: string }> {\n\tconst statsStr = STAT_NAMES.map((s) => `${s}: ${bones.stats[s]}`).join(\", \");\n\tconst prompt = SOUL_GENERATION_PROMPT.replace(\"{species}\", bones.species)\n\t\t.replace(\"{rarity}\", bones.rarity)\n\t\t.replace(\"{stats}\", statsStr)\n\t\t.replace(\"{shiny}\", bones.shiny ? \"YES ✨\" : \"no\");\n\n\tconst context: Context = {\n\t\tsystemPrompt: \"Generate a companion character. Respond in the exact format requested.\",\n\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t};\n\n\ttry {\n\t\tconst response = await completeSimple(parentModel, context, { apiKey });\n\t\tconst text = response.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\");\n\n\t\t// Parse NAME: ... and PERSONALITY: ... and BACKSTORY: ...\n\t\tconst nameMatch = text.match(/NAME:\\s*(.+)/i);\n\t\tconst personalityMatch = text.match(/PERSONALITY:\\s*(.+)/i);\n\t\tconst backstoryMatch = text.match(/BACKSTORY:\\s*([\\s\\S]+)/i);\n\n\t\tlet name = nameMatch?.[1]?.trim() ?? bones.species;\n\t\tconst personality = personalityMatch?.[1]?.trim() ?? `A ${bones.rarity} ${bones.species} companion.`;\n\t\tconst backstory = backstoryMatch?.[1]?.trim() ?? DEFAULT_BACKSTORY;\n\n\t\t// Enforce name length\n\t\tif (name.length > 8) name = name.slice(0, 8);\n\n\t\treturn { name, personality, backstory };\n\t} catch {\n\t\t// Fallback if LLM fails\n\t\treturn {\n\t\t\tname: bones.species,\n\t\t\tpersonality: `A ${bones.rarity} ${bones.species} companion.`,\n\t\t\tbackstory: DEFAULT_BACKSTORY,\n\t\t};\n\t}\n}\n\n// =============================================================================\n// BuddyManager\n// =============================================================================\n\nexport class BuddyManager {\n\tprivate state: BuddyState | null = null;\n\tprivate ollamaStatus: OllamaStatus | null = null;\n\n\t/** Get current buddy state (null if not loaded) */\n\tgetState(): BuddyState | null {\n\t\treturn this.state;\n\t}\n\n\t/** Check if buddy exists on disk */\n\thasStoredBuddy(): boolean {\n\t\treturn loadStored() !== null;\n\t}\n\n\t/**\n\t * Load or create buddy state.\n\t * If stored buddy exists, loads soul and re-rolls bones.\n\t * If no stored buddy, returns null (need to hatch first).\n\t */\n\tload(): BuddyState | null {\n\t\tconst stored = loadStored();\n\t\tif (!stored) return null;\n\n\t\tconst bones = rollBones(stored.rerollCount);\n\t\tthis.state = { ...bones, ...stored };\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Hatch a new buddy. Generates bones, then uses parent LLM for soul.\n\t * Returns the new state.\n\t */\n\tasync hatch(parentModel: Model<\"openai-completions\">, apiKey: string): Promise<BuddyState> {\n\t\tconst stored = loadStored();\n\t\tconst rerollCount = stored?.rerollCount ?? 0;\n\n\t\tconst bones = rollBones(rerollCount);\n\t\tconst { name, personality, backstory } = await generateSoul(bones, parentModel, apiKey);\n\n\t\tconst newStored: StoredCompanion = {\n\t\t\trerollCount,\n\t\t\tname,\n\t\t\tpersonality,\n\t\t\tbackstory,\n\t\t\thatchedAt: new Date().toISOString(),\n\t\t\t...(stored?.ollamaModel ? { ollamaModel: stored.ollamaModel } : {}),\n\t\t};\n\n\t\tsaveStored(newStored);\n\t\tthis.state = { ...bones, ...newStored };\n\t\treturn this.state;\n\t}\n\n\t/**\n\t * Reroll the buddy — new bones + new soul.\n\t */\n\tasync reroll(parentModel: Model<\"openai-completions\">, apiKey: string): Promise<BuddyState> {\n\t\tconst stored = loadStored();\n\t\tconst newRerollCount = (stored?.rerollCount ?? 0) + 1;\n\n\t\tconst bones = rollBones(newRerollCount);\n\t\tconst { name, personality, backstory } = await generateSoul(bones, parentModel, apiKey);\n\n\t\tconst newStored: StoredCompanion = {\n\t\t\trerollCount: newRerollCount,\n\t\t\tname,\n\t\t\tpersonality,\n\t\t\tbackstory,\n\t\t\thatchedAt: new Date().toISOString(),\n\t\t\t...(stored?.ollamaModel ? { ollamaModel: stored.ollamaModel } : {}),\n\t\t};\n\n\t\tsaveStored(newStored);\n\t\tthis.state = { ...bones, ...newStored };\n\t\treturn this.state;\n\t}\n\n\t/** Get buddy's name (for name-call detection) */\n\tgetName(): string | null {\n\t\treturn this.state?.name ?? loadStored()?.name ?? null;\n\t}\n\n\t/**\n\t * Shared Ollama chat helper. Checks availability, picks model, runs completion.\n\t * Returns the response text, or null if Ollama is unavailable or no model configured.\n\t */\n\tprivate async ollamaChat(context: Context): Promise<string | null> {\n\t\t// Check Ollama lazily, retry if previously unavailable\n\t\tif (!this.ollamaStatus || !this.ollamaStatus.available) {\n\t\t\tthis.ollamaStatus = await checkOllama();\n\t\t}\n\t\tif (!this.ollamaStatus.available) return null;\n\n\t\tconst modelName = pickOllamaModel(this.state?.ollamaModel, this.ollamaStatus.models);\n\t\tif (!modelName) return null;\n\t\tconst model: Model<\"openai-completions\"> = {\n\t\t\t...OLLAMA_MODEL_BASE,\n\t\t\tid: modelName,\n\t\t\tname: `${modelName} (Ollama)`,\n\t\t};\n\n\t\tlet response: import(\"@dreb/ai\").AssistantMessage;\n\t\ttry {\n\t\t\tresponse = await completeSimple(model, context, {\n\t\t\t\tapiKey: \"ollama\",\n\t\t\t\tsignal: AbortSignal.timeout(120000),\n\t\t\t});\n\t\t} catch {\n\t\t\t// Safety net for unexpected sync errors (e.g. provider not found).\n\t\t\t// Normal runtime errors (timeout, connection) are handled via stopReason below.\n\t\t\tthis.ollamaStatus = null;\n\t\t\treturn null;\n\t\t}\n\n\t\t// Connection error — invalidate cache so next attempt re-checks Ollama\n\t\tif (response.stopReason === \"error\") {\n\t\t\tthis.ollamaStatus = null;\n\t\t\treturn null;\n\t\t}\n\n\t\t// Timeout or abort — preserve cache (model is just slow, Ollama is fine)\n\t\tif (response.stopReason === \"aborted\") {\n\t\t\treturn null;\n\t\t}\n\n\t\tlet text = response.content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\")\n\t\t\t.trim();\n\n\t\t// Truncate overly long responses\n\t\ttext = truncateResponse(text, MAX_RESPONSE_WORDS);\n\n\t\treturn text || null;\n\t}\n\n\t/**\n\t * Generate a reaction to an event using Ollama.\n\t * Returns null if Ollama is unavailable.\n\t */\n\tasync react(event: string): Promise<string | null> {\n\t\tif (!this.state) return null;\n\n\t\tconst prompt = REACTION_PROMPT.replace(\"{name}\", this.state.name)\n\t\t\t.replace(\"{species}\", this.state.species)\n\t\t\t.replace(\"{personality}\", this.state.personality)\n\t\t\t.replace(\"{backstory}\", this.state.backstory)\n\t\t\t.replace(\"{event}\", event);\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: \"Respond with a short in-character quip. Max 20 words.\",\n\t\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t\t};\n\n\t\treturn this.ollamaChat(context);\n\t}\n\n\t/**\n\t * Respond to the user calling the buddy's name.\n\t * Uses Ollama for the response.\n\t */\n\tasync respondToNameCall(userMessage: string, recentContext: string): Promise<string | null> {\n\t\tif (!this.state) return null;\n\n\t\tconst prompt = NAME_CALL_PROMPT.replace(\"{name}\", this.state.name)\n\t\t\t.replace(\"{species}\", this.state.species)\n\t\t\t.replace(\"{personality}\", this.state.personality)\n\t\t\t.replace(\"{backstory}\", this.state.backstory)\n\t\t\t.replace(\"{message}\", userMessage)\n\t\t\t.replace(\"{context}\", recentContext);\n\n\t\tconst context: Context = {\n\t\t\tsystemPrompt: \"Respond with a short friendly greeting. Max 30 words.\",\n\t\t\tmessages: [{ role: \"user\", content: prompt, timestamp: Date.now() }],\n\t\t};\n\n\t\treturn this.ollamaChat(context);\n\t}\n\n\t/** Get the configured Ollama model name, or null if not set */\n\tgetOllamaModel(): string | null {\n\t\treturn this.state?.ollamaModel ?? loadStored()?.ollamaModel ?? null;\n\t}\n\n\t/** Set the Ollama model for buddy reactions. Persists to disk. */\n\tsetOllamaModel(modelName: string): void {\n\t\tconst stored = loadStored();\n\t\tif (stored) {\n\t\t\tstored.ollamaModel = modelName;\n\t\t\tsaveStored(stored);\n\t\t}\n\t\tif (this.state) {\n\t\t\tthis.state.ollamaModel = modelName;\n\t\t}\n\t\t// Invalidate Ollama status cache so next call picks up the new model\n\t\tthis.ollamaStatus = null;\n\t}\n\n\t/** Reset Ollama status cache (e.g. after detecting it became available) */\n\tresetOllamaCache(): void {\n\t\tthis.ollamaStatus = null;\n\t}\n\n\t/** Update the hidden flag in persisted storage */\n\tsetHidden(hidden: boolean): void {\n\t\tconst stored = loadStored();\n\t\tif (stored) {\n\t\t\tstored.hidden = hidden;\n\t\t\tsaveStored(stored);\n\t\t}\n\t\t// Keep in-memory state in sync so reset() reads current hidden flag\n\t\tif (this.state) {\n\t\t\tthis.state.hidden = hidden;\n\t\t}\n\t}\n}\n"]}
|
|
@@ -16,7 +16,6 @@ export interface RetrySettings {
|
|
|
16
16
|
}
|
|
17
17
|
export interface TerminalSettings {
|
|
18
18
|
showImages?: boolean;
|
|
19
|
-
clearOnShrink?: boolean;
|
|
20
19
|
}
|
|
21
20
|
export interface ImageSettings {
|
|
22
21
|
autoResize?: boolean;
|
|
@@ -215,8 +214,6 @@ export declare class SettingsManager {
|
|
|
215
214
|
getThinkingBudgets(): ThinkingBudgetsSettings | undefined;
|
|
216
215
|
getShowImages(): boolean;
|
|
217
216
|
setShowImages(show: boolean): void;
|
|
218
|
-
getClearOnShrink(): boolean;
|
|
219
|
-
setClearOnShrink(enabled: boolean): void;
|
|
220
217
|
getImageAutoResize(): boolean;
|
|
221
218
|
setImageAutoResize(enabled: boolean): void;
|
|
222
219
|
getBlockImages(): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-manager.d.ts","sourceRoot":"","sources":["../../src/core/settings-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Transport } from \"@dreb/ai\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport { CONFIG_DIR_NAME, getAgentDir } from \"../config.js\";\n\nexport interface CompactionSettings {\n\tenabled?: boolean; // default: true\n\treserveTokens?: number; // default: 16384\n\tkeepRecentTokens?: number; // default: 20000\n}\n\nexport interface BranchSummarySettings {\n\treserveTokens?: number; // default: 16384 (tokens reserved for prompt + LLM response)\n\tskipPrompt?: boolean; // default: false - when true, skips \"Summarize branch?\" prompt and defaults to no summary\n}\n\nexport interface RetrySettings {\n\tenabled?: boolean; // default: true\n\tmaxRetries?: number; // default: 3\n\tbaseDelayMs?: number; // default: 2000 (exponential backoff: 2s, 4s, 8s)\n\tmaxDelayMs?: number; // default: 60000 (max server-requested delay before failing)\n}\n\nexport interface TerminalSettings {\n\tshowImages?: boolean; // default: true (only relevant if terminal supports images)\n\tclearOnShrink?: boolean; // default: false (clear empty rows when content shrinks)\n}\n\nexport interface ImageSettings {\n\tautoResize?: boolean; // default: true (resize images to 2000x2000 max for better model compatibility)\n\tblockImages?: boolean; // default: false - when true, prevents all images from being sent to LLM providers\n}\n\nexport interface ThinkingBudgetsSettings {\n\tminimal?: number;\n\tlow?: number;\n\tmedium?: number;\n\thigh?: number;\n}\n\nexport interface MarkdownSettings {\n\tcodeBlockIndent?: string; // default: \" \"\n}\n\nexport type TransportSetting = Transport;\n\n/**\n * Package source for npm/git packages.\n * - String form: load all resources from the package\n * - Object form: filter which resources to load\n */\nexport type PackageSource =\n\t| string\n\t| {\n\t\t\tsource: string;\n\t\t\textensions?: string[];\n\t\t\tskills?: string[];\n\t\t\tprompts?: string[];\n\t\t\tthemes?: string[];\n\t };\n\nexport interface Settings {\n\tlastChangelogVersion?: string;\n\tdefaultProvider?: string;\n\tdefaultModel?: string;\n\tdefaultThinkingLevel?: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\";\n\ttransport?: TransportSetting; // default: \"sse\"\n\tsteeringMode?: \"all\" | \"one-at-a-time\";\n\tfollowUpMode?: \"all\" | \"one-at-a-time\";\n\ttheme?: string;\n\tcompaction?: CompactionSettings;\n\tbranchSummary?: BranchSummarySettings;\n\tretry?: RetrySettings;\n\thideThinkingBlock?: boolean;\n\tshellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)\n\tquietStartup?: boolean;\n\tshellCommandPrefix?: string; // Prefix prepended to every bash command (e.g., \"shopt -s expand_aliases\" for alias support)\n\tnpmCommand?: string[]; // Command used for npm package lookup/install operations, argv-style (e.g., [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"])\n\tcollapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)\n\tpackages?: PackageSource[]; // Array of npm/git package sources (string or object with filtering)\n\textensions?: string[]; // Array of local extension file paths or directories\n\tskills?: string[]; // Array of local skill file paths or directories\n\tprompts?: string[]; // Array of local prompt template paths or directories\n\tthemes?: string[]; // Array of local theme file paths or directories\n\tenableSkillCommands?: boolean; // default: true - register skills as /skill:name commands\n\tterminal?: TerminalSettings;\n\timages?: ImageSettings;\n\tenabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)\n\tdoubleEscapeAction?: \"fork\" | \"tree\" | \"none\"; // Action for double-escape with empty editor (default: \"tree\")\n\ttreeFilterMode?: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\"; // Default filter when opening /tree\n\tthinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels\n\teditorPaddingX?: number; // Horizontal padding for input editor (default: 0)\n\tautocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5)\n\tshowHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME\n\tmarkdown?: MarkdownSettings;\n\tsessionDir?: string; // Custom session storage directory (same format as --session-dir CLI flag)\n\tforbiddenCommands?: string[]; // Regex patterns for commands blocked by the forbidden-commands guard\n}\n\n/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */\nfunction deepMergeSettings(base: Settings, overrides: Settings): Settings {\n\tconst result: Settings = { ...base };\n\n\tfor (const key of Object.keys(overrides) as (keyof Settings)[]) {\n\t\tconst overrideValue = overrides[key];\n\t\tconst baseValue = base[key];\n\n\t\tif (overrideValue === undefined) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// For nested objects, merge recursively\n\t\tif (\n\t\t\ttypeof overrideValue === \"object\" &&\n\t\t\toverrideValue !== null &&\n\t\t\t!Array.isArray(overrideValue) &&\n\t\t\ttypeof baseValue === \"object\" &&\n\t\t\tbaseValue !== null &&\n\t\t\t!Array.isArray(baseValue)\n\t\t) {\n\t\t\t(result as Record<string, unknown>)[key] = { ...baseValue, ...overrideValue };\n\t\t} else {\n\t\t\t// For primitives and arrays, override value wins\n\t\t\t(result as Record<string, unknown>)[key] = overrideValue;\n\t\t}\n\t}\n\n\treturn result;\n}\n\nexport type SettingsScope = \"global\" | \"project\";\n\nexport interface SettingsStorage {\n\twithLock(scope: SettingsScope, fn: (current: string | undefined) => string | undefined): void;\n}\n\nexport interface SettingsError {\n\tscope: SettingsScope;\n\terror: Error;\n}\n\nexport class FileSettingsStorage implements SettingsStorage {\n\tprivate globalSettingsPath: string;\n\tprivate projectSettingsPath: string;\n\n\tconstructor(cwd: string = process.cwd(), agentDir: string = getAgentDir()) {\n\t\tthis.globalSettingsPath = join(agentDir, \"settings.json\");\n\t\tthis.projectSettingsPath = join(cwd, CONFIG_DIR_NAME, \"settings.json\");\n\t}\n\n\tprivate acquireLockSyncWithRetry(path: string): () => void {\n\t\tconst maxAttempts = 10;\n\t\tconst delayMs = 20;\n\t\tlet lastError: unknown;\n\n\t\tfor (let attempt = 1; attempt <= maxAttempts; attempt++) {\n\t\t\ttry {\n\t\t\t\treturn lockfile.lockSync(path, { realpath: false });\n\t\t\t} catch (error) {\n\t\t\t\tconst code =\n\t\t\t\t\ttypeof error === \"object\" && error !== null && \"code\" in error\n\t\t\t\t\t\t? String((error as { code?: unknown }).code)\n\t\t\t\t\t\t: undefined;\n\t\t\t\tif (code !== \"ELOCKED\" || attempt === maxAttempts) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tlastError = error;\n\t\t\t\tconst start = Date.now();\n\t\t\t\twhile (Date.now() - start < delayMs) {\n\t\t\t\t\t// Sleep synchronously to avoid changing callers to async.\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthrow (lastError as Error) ?? new Error(\"Failed to acquire settings lock\");\n\t}\n\n\twithLock(scope: SettingsScope, fn: (current: string | undefined) => string | undefined): void {\n\t\tconst path = scope === \"global\" ? this.globalSettingsPath : this.projectSettingsPath;\n\t\tconst dir = dirname(path);\n\n\t\tlet release: (() => void) | undefined;\n\t\ttry {\n\t\t\t// Only create directory and lock if file exists or we need to write\n\t\t\tconst fileExists = existsSync(path);\n\t\t\tif (fileExists) {\n\t\t\t\trelease = this.acquireLockSyncWithRetry(path);\n\t\t\t}\n\t\t\tconst current = fileExists ? readFileSync(path, \"utf-8\") : undefined;\n\t\t\tconst next = fn(current);\n\t\t\tif (next !== undefined) {\n\t\t\t\t// Only create directory when we actually need to write\n\t\t\t\tif (!existsSync(dir)) {\n\t\t\t\t\tmkdirSync(dir, { recursive: true });\n\t\t\t\t}\n\t\t\t\tif (!release) {\n\t\t\t\t\trelease = this.acquireLockSyncWithRetry(path);\n\t\t\t\t}\n\t\t\t\twriteFileSync(path, next, \"utf-8\");\n\t\t\t}\n\t\t} finally {\n\t\t\tif (release) {\n\t\t\t\trelease();\n\t\t\t}\n\t\t}\n\t}\n}\n\nexport class InMemorySettingsStorage implements SettingsStorage {\n\tprivate global: string | undefined;\n\tprivate project: string | undefined;\n\n\twithLock(scope: SettingsScope, fn: (current: string | undefined) => string | undefined): void {\n\t\tconst current = scope === \"global\" ? this.global : this.project;\n\t\tconst next = fn(current);\n\t\tif (next !== undefined) {\n\t\t\tif (scope === \"global\") {\n\t\t\t\tthis.global = next;\n\t\t\t} else {\n\t\t\t\tthis.project = next;\n\t\t\t}\n\t\t}\n\t}\n}\n\nexport class SettingsManager {\n\tprivate storage: SettingsStorage;\n\tprivate globalSettings: Settings;\n\tprivate projectSettings: Settings;\n\tprivate settings: Settings;\n\tprivate modifiedFields = new Set<keyof Settings>(); // Track global fields modified during session\n\tprivate modifiedNestedFields = new Map<keyof Settings, Set<string>>(); // Track global nested field modifications\n\tprivate modifiedProjectFields = new Set<keyof Settings>(); // Track project fields modified during session\n\tprivate modifiedProjectNestedFields = new Map<keyof Settings, Set<string>>(); // Track project nested field modifications\n\tprivate globalSettingsLoadError: Error | null = null; // Track if global settings file had parse errors\n\tprivate projectSettingsLoadError: Error | null = null; // Track if project settings file had parse errors\n\tprivate writeQueue: Promise<void> = Promise.resolve();\n\tprivate errors: SettingsError[];\n\n\tprivate constructor(\n\t\tstorage: SettingsStorage,\n\t\tinitialGlobal: Settings,\n\t\tinitialProject: Settings,\n\t\tglobalLoadError: Error | null = null,\n\t\tprojectLoadError: Error | null = null,\n\t\tinitialErrors: SettingsError[] = [],\n\t) {\n\t\tthis.storage = storage;\n\t\tthis.globalSettings = initialGlobal;\n\t\tthis.projectSettings = initialProject;\n\t\tthis.globalSettingsLoadError = globalLoadError;\n\t\tthis.projectSettingsLoadError = projectLoadError;\n\t\tthis.errors = [...initialErrors];\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\t}\n\n\t/** Create a SettingsManager that loads from files */\n\tstatic create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): SettingsManager {\n\t\tconst storage = new FileSettingsStorage(cwd, agentDir);\n\t\treturn SettingsManager.fromStorage(storage);\n\t}\n\n\t/** Create a SettingsManager from an arbitrary storage backend */\n\tstatic fromStorage(storage: SettingsStorage): SettingsManager {\n\t\tconst globalLoad = SettingsManager.tryLoadFromStorage(storage, \"global\");\n\t\tconst projectLoad = SettingsManager.tryLoadFromStorage(storage, \"project\");\n\t\tconst initialErrors: SettingsError[] = [];\n\t\tif (globalLoad.error) {\n\t\t\tinitialErrors.push({ scope: \"global\", error: globalLoad.error });\n\t\t}\n\t\tif (projectLoad.error) {\n\t\t\tinitialErrors.push({ scope: \"project\", error: projectLoad.error });\n\t\t}\n\n\t\treturn new SettingsManager(\n\t\t\tstorage,\n\t\t\tglobalLoad.settings,\n\t\t\tprojectLoad.settings,\n\t\t\tglobalLoad.error,\n\t\t\tprojectLoad.error,\n\t\t\tinitialErrors,\n\t\t);\n\t}\n\n\t/** Create an in-memory SettingsManager (no file I/O) */\n\tstatic inMemory(settings: Partial<Settings> = {}): SettingsManager {\n\t\tconst storage = new InMemorySettingsStorage();\n\t\treturn new SettingsManager(storage, settings, {});\n\t}\n\n\tprivate static loadFromStorage(storage: SettingsStorage, scope: SettingsScope): Settings {\n\t\tlet content: string | undefined;\n\t\tstorage.withLock(scope, (current) => {\n\t\t\tcontent = current;\n\t\t\treturn undefined;\n\t\t});\n\n\t\tif (!content) {\n\t\t\treturn {};\n\t\t}\n\t\tconst settings = JSON.parse(content);\n\t\treturn SettingsManager.migrateSettings(settings);\n\t}\n\n\tprivate static tryLoadFromStorage(\n\t\tstorage: SettingsStorage,\n\t\tscope: SettingsScope,\n\t): { settings: Settings; error: Error | null } {\n\t\ttry {\n\t\t\treturn { settings: SettingsManager.loadFromStorage(storage, scope), error: null };\n\t\t} catch (error) {\n\t\t\treturn { settings: {}, error: error as Error };\n\t\t}\n\t}\n\n\t/** Migrate old settings format to new format */\n\tprivate static migrateSettings(settings: Record<string, unknown>): Settings {\n\t\t// Migrate queueMode -> steeringMode\n\t\tif (\"queueMode\" in settings && !(\"steeringMode\" in settings)) {\n\t\t\tsettings.steeringMode = settings.queueMode;\n\t\t\tdelete settings.queueMode;\n\t\t}\n\n\t\t// Migrate legacy websockets boolean -> transport enum\n\t\tif (!(\"transport\" in settings) && typeof settings.websockets === \"boolean\") {\n\t\t\tsettings.transport = settings.websockets ? \"websocket\" : \"sse\";\n\t\t\tdelete settings.websockets;\n\t\t}\n\n\t\t// Migrate old skills object format to new array format\n\t\tif (\n\t\t\t\"skills\" in settings &&\n\t\t\ttypeof settings.skills === \"object\" &&\n\t\t\tsettings.skills !== null &&\n\t\t\t!Array.isArray(settings.skills)\n\t\t) {\n\t\t\tconst skillsSettings = settings.skills as {\n\t\t\t\tenableSkillCommands?: boolean;\n\t\t\t\tcustomDirectories?: unknown;\n\t\t\t};\n\t\t\tif (skillsSettings.enableSkillCommands !== undefined && settings.enableSkillCommands === undefined) {\n\t\t\t\tsettings.enableSkillCommands = skillsSettings.enableSkillCommands;\n\t\t\t}\n\t\t\tif (Array.isArray(skillsSettings.customDirectories) && skillsSettings.customDirectories.length > 0) {\n\t\t\t\tsettings.skills = skillsSettings.customDirectories;\n\t\t\t} else {\n\t\t\t\tdelete settings.skills;\n\t\t\t}\n\t\t}\n\n\t\treturn settings as Settings;\n\t}\n\n\tgetGlobalSettings(): Settings {\n\t\treturn structuredClone(this.globalSettings);\n\t}\n\n\tgetProjectSettings(): Settings {\n\t\treturn structuredClone(this.projectSettings);\n\t}\n\n\treload(): void {\n\t\tconst globalLoad = SettingsManager.tryLoadFromStorage(this.storage, \"global\");\n\t\tif (!globalLoad.error) {\n\t\t\tthis.globalSettings = globalLoad.settings;\n\t\t\tthis.globalSettingsLoadError = null;\n\t\t} else {\n\t\t\tthis.globalSettingsLoadError = globalLoad.error;\n\t\t\tthis.recordError(\"global\", globalLoad.error);\n\t\t}\n\n\t\tthis.modifiedFields.clear();\n\t\tthis.modifiedNestedFields.clear();\n\t\tthis.modifiedProjectFields.clear();\n\t\tthis.modifiedProjectNestedFields.clear();\n\n\t\tconst projectLoad = SettingsManager.tryLoadFromStorage(this.storage, \"project\");\n\t\tif (!projectLoad.error) {\n\t\t\tthis.projectSettings = projectLoad.settings;\n\t\t\tthis.projectSettingsLoadError = null;\n\t\t} else {\n\t\t\tthis.projectSettingsLoadError = projectLoad.error;\n\t\t\tthis.recordError(\"project\", projectLoad.error);\n\t\t}\n\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\t}\n\n\t/** Apply additional overrides on top of current settings */\n\tapplyOverrides(overrides: Partial<Settings>): void {\n\t\tthis.settings = deepMergeSettings(this.settings, overrides);\n\t}\n\n\t/** Mark a global field as modified during this session */\n\tprivate markModified(field: keyof Settings, nestedKey?: string): void {\n\t\tthis.modifiedFields.add(field);\n\t\tif (nestedKey) {\n\t\t\tif (!this.modifiedNestedFields.has(field)) {\n\t\t\t\tthis.modifiedNestedFields.set(field, new Set());\n\t\t\t}\n\t\t\tthis.modifiedNestedFields.get(field)!.add(nestedKey);\n\t\t}\n\t}\n\n\t/** Mark a project field as modified during this session */\n\tprivate markProjectModified(field: keyof Settings, nestedKey?: string): void {\n\t\tthis.modifiedProjectFields.add(field);\n\t\tif (nestedKey) {\n\t\t\tif (!this.modifiedProjectNestedFields.has(field)) {\n\t\t\t\tthis.modifiedProjectNestedFields.set(field, new Set());\n\t\t\t}\n\t\t\tthis.modifiedProjectNestedFields.get(field)!.add(nestedKey);\n\t\t}\n\t}\n\n\tprivate recordError(scope: SettingsScope, error: unknown): void {\n\t\tconst normalizedError = error instanceof Error ? error : new Error(String(error));\n\t\tthis.errors.push({ scope, error: normalizedError });\n\t}\n\n\tprivate clearModifiedScope(scope: SettingsScope): void {\n\t\tif (scope === \"global\") {\n\t\t\tthis.modifiedFields.clear();\n\t\t\tthis.modifiedNestedFields.clear();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.modifiedProjectFields.clear();\n\t\tthis.modifiedProjectNestedFields.clear();\n\t}\n\n\tprivate enqueueWrite(scope: SettingsScope, task: () => void): void {\n\t\tthis.writeQueue = this.writeQueue\n\t\t\t.then(() => {\n\t\t\t\ttask();\n\t\t\t\tthis.clearModifiedScope(scope);\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tthis.recordError(scope, error);\n\t\t\t});\n\t}\n\n\tprivate cloneModifiedNestedFields(source: Map<keyof Settings, Set<string>>): Map<keyof Settings, Set<string>> {\n\t\tconst snapshot = new Map<keyof Settings, Set<string>>();\n\t\tfor (const [key, value] of source.entries()) {\n\t\t\tsnapshot.set(key, new Set(value));\n\t\t}\n\t\treturn snapshot;\n\t}\n\n\tprivate persistScopedSettings(\n\t\tscope: SettingsScope,\n\t\tsnapshotSettings: Settings,\n\t\tmodifiedFields: Set<keyof Settings>,\n\t\tmodifiedNestedFields: Map<keyof Settings, Set<string>>,\n\t): void {\n\t\tthis.storage.withLock(scope, (current) => {\n\t\t\tconst currentFileSettings = current\n\t\t\t\t? SettingsManager.migrateSettings(JSON.parse(current) as Record<string, unknown>)\n\t\t\t\t: {};\n\t\t\tconst mergedSettings: Settings = { ...currentFileSettings };\n\t\t\tfor (const field of modifiedFields) {\n\t\t\t\tconst value = snapshotSettings[field];\n\t\t\t\tif (modifiedNestedFields.has(field) && typeof value === \"object\" && value !== null) {\n\t\t\t\t\tconst nestedModified = modifiedNestedFields.get(field)!;\n\t\t\t\t\tconst baseNested = (currentFileSettings[field] as Record<string, unknown>) ?? {};\n\t\t\t\t\tconst inMemoryNested = value as Record<string, unknown>;\n\t\t\t\t\tconst mergedNested = { ...baseNested };\n\t\t\t\t\tfor (const nestedKey of nestedModified) {\n\t\t\t\t\t\tmergedNested[nestedKey] = inMemoryNested[nestedKey];\n\t\t\t\t\t}\n\t\t\t\t\t(mergedSettings as Record<string, unknown>)[field] = mergedNested;\n\t\t\t\t} else {\n\t\t\t\t\t(mergedSettings as Record<string, unknown>)[field] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn JSON.stringify(mergedSettings, null, 2);\n\t\t});\n\t}\n\n\tprivate save(): void {\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\n\t\tif (this.globalSettingsLoadError) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst snapshotGlobalSettings = structuredClone(this.globalSettings);\n\t\tconst modifiedFields = new Set(this.modifiedFields);\n\t\tconst modifiedNestedFields = this.cloneModifiedNestedFields(this.modifiedNestedFields);\n\n\t\tthis.enqueueWrite(\"global\", () => {\n\t\t\tthis.persistScopedSettings(\"global\", snapshotGlobalSettings, modifiedFields, modifiedNestedFields);\n\t\t});\n\t}\n\n\tprivate saveProjectSettings(settings: Settings): void {\n\t\tthis.projectSettings = structuredClone(settings);\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\n\t\tif (this.projectSettingsLoadError) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst snapshotProjectSettings = structuredClone(this.projectSettings);\n\t\tconst modifiedFields = new Set(this.modifiedProjectFields);\n\t\tconst modifiedNestedFields = this.cloneModifiedNestedFields(this.modifiedProjectNestedFields);\n\t\tthis.enqueueWrite(\"project\", () => {\n\t\t\tthis.persistScopedSettings(\"project\", snapshotProjectSettings, modifiedFields, modifiedNestedFields);\n\t\t});\n\t}\n\n\tasync flush(): Promise<void> {\n\t\tawait this.writeQueue;\n\t}\n\n\tdrainErrors(): SettingsError[] {\n\t\tconst drained = [...this.errors];\n\t\tthis.errors = [];\n\t\treturn drained;\n\t}\n\n\tgetLastChangelogVersion(): string | undefined {\n\t\treturn this.settings.lastChangelogVersion;\n\t}\n\n\tsetLastChangelogVersion(version: string): void {\n\t\tthis.globalSettings.lastChangelogVersion = version;\n\t\tthis.markModified(\"lastChangelogVersion\");\n\t\tthis.save();\n\t}\n\n\tgetSessionDir(): string | undefined {\n\t\treturn this.settings.sessionDir;\n\t}\n\n\tgetDefaultProvider(): string | undefined {\n\t\treturn this.settings.defaultProvider;\n\t}\n\n\tgetDefaultModel(): string | undefined {\n\t\treturn this.settings.defaultModel;\n\t}\n\n\tsetDefaultProvider(provider: string): void {\n\t\tthis.globalSettings.defaultProvider = provider;\n\t\tthis.markModified(\"defaultProvider\");\n\t\tthis.save();\n\t}\n\n\tsetDefaultModel(modelId: string): void {\n\t\tthis.globalSettings.defaultModel = modelId;\n\t\tthis.markModified(\"defaultModel\");\n\t\tthis.save();\n\t}\n\n\tsetDefaultModelAndProvider(provider: string, modelId: string): void {\n\t\tthis.globalSettings.defaultProvider = provider;\n\t\tthis.globalSettings.defaultModel = modelId;\n\t\tthis.markModified(\"defaultProvider\");\n\t\tthis.markModified(\"defaultModel\");\n\t\tthis.save();\n\t}\n\n\tgetSteeringMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn this.settings.steeringMode || \"one-at-a-time\";\n\t}\n\n\tsetSteeringMode(mode: \"all\" | \"one-at-a-time\"): void {\n\t\tthis.globalSettings.steeringMode = mode;\n\t\tthis.markModified(\"steeringMode\");\n\t\tthis.save();\n\t}\n\n\tgetFollowUpMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn this.settings.followUpMode || \"one-at-a-time\";\n\t}\n\n\tsetFollowUpMode(mode: \"all\" | \"one-at-a-time\"): void {\n\t\tthis.globalSettings.followUpMode = mode;\n\t\tthis.markModified(\"followUpMode\");\n\t\tthis.save();\n\t}\n\n\tgetTheme(): string | undefined {\n\t\treturn this.settings.theme;\n\t}\n\n\tsetTheme(theme: string): void {\n\t\tthis.globalSettings.theme = theme;\n\t\tthis.markModified(\"theme\");\n\t\tthis.save();\n\t}\n\n\tgetDefaultThinkingLevel(): \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | undefined {\n\t\treturn this.settings.defaultThinkingLevel;\n\t}\n\n\tsetDefaultThinkingLevel(level: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\"): void {\n\t\tthis.globalSettings.defaultThinkingLevel = level;\n\t\tthis.markModified(\"defaultThinkingLevel\");\n\t\tthis.save();\n\t}\n\n\tgetTransport(): TransportSetting {\n\t\treturn this.settings.transport ?? \"sse\";\n\t}\n\n\tsetTransport(transport: TransportSetting): void {\n\t\tthis.globalSettings.transport = transport;\n\t\tthis.markModified(\"transport\");\n\t\tthis.save();\n\t}\n\n\tgetCompactionEnabled(): boolean {\n\t\treturn this.settings.compaction?.enabled ?? true;\n\t}\n\n\tsetCompactionEnabled(enabled: boolean): void {\n\t\tif (!this.globalSettings.compaction) {\n\t\t\tthis.globalSettings.compaction = {};\n\t\t}\n\t\tthis.globalSettings.compaction.enabled = enabled;\n\t\tthis.markModified(\"compaction\", \"enabled\");\n\t\tthis.save();\n\t}\n\n\tgetCompactionReserveTokens(): number {\n\t\treturn this.settings.compaction?.reserveTokens ?? 16384;\n\t}\n\n\tgetCompactionKeepRecentTokens(): number {\n\t\treturn this.settings.compaction?.keepRecentTokens ?? 20000;\n\t}\n\n\tgetCompactionSettings(): { enabled: boolean; reserveTokens: number; keepRecentTokens: number } {\n\t\treturn {\n\t\t\tenabled: this.getCompactionEnabled(),\n\t\t\treserveTokens: this.getCompactionReserveTokens(),\n\t\t\tkeepRecentTokens: this.getCompactionKeepRecentTokens(),\n\t\t};\n\t}\n\n\tgetBranchSummarySettings(): { reserveTokens: number; skipPrompt: boolean } {\n\t\treturn {\n\t\t\treserveTokens: this.settings.branchSummary?.reserveTokens ?? 16384,\n\t\t\tskipPrompt: this.settings.branchSummary?.skipPrompt ?? false,\n\t\t};\n\t}\n\n\tgetBranchSummarySkipPrompt(): boolean {\n\t\treturn this.settings.branchSummary?.skipPrompt ?? false;\n\t}\n\n\tgetRetryEnabled(): boolean {\n\t\treturn this.settings.retry?.enabled ?? true;\n\t}\n\n\tsetRetryEnabled(enabled: boolean): void {\n\t\tif (!this.globalSettings.retry) {\n\t\t\tthis.globalSettings.retry = {};\n\t\t}\n\t\tthis.globalSettings.retry.enabled = enabled;\n\t\tthis.markModified(\"retry\", \"enabled\");\n\t\tthis.save();\n\t}\n\n\tgetRetrySettings(): { enabled: boolean; maxRetries: number; baseDelayMs: number; maxDelayMs: number } {\n\t\treturn {\n\t\t\tenabled: this.getRetryEnabled(),\n\t\t\tmaxRetries: this.settings.retry?.maxRetries ?? 3,\n\t\t\tbaseDelayMs: this.settings.retry?.baseDelayMs ?? 2000,\n\t\t\tmaxDelayMs: this.settings.retry?.maxDelayMs ?? 60000,\n\t\t};\n\t}\n\n\tgetHideThinkingBlock(): boolean {\n\t\treturn this.settings.hideThinkingBlock ?? false;\n\t}\n\n\tsetHideThinkingBlock(hide: boolean): void {\n\t\tthis.globalSettings.hideThinkingBlock = hide;\n\t\tthis.markModified(\"hideThinkingBlock\");\n\t\tthis.save();\n\t}\n\n\tgetShellPath(): string | undefined {\n\t\treturn this.settings.shellPath;\n\t}\n\n\tsetShellPath(path: string | undefined): void {\n\t\tthis.globalSettings.shellPath = path;\n\t\tthis.markModified(\"shellPath\");\n\t\tthis.save();\n\t}\n\n\tgetQuietStartup(): boolean {\n\t\treturn this.settings.quietStartup ?? false;\n\t}\n\n\tsetQuietStartup(quiet: boolean): void {\n\t\tthis.globalSettings.quietStartup = quiet;\n\t\tthis.markModified(\"quietStartup\");\n\t\tthis.save();\n\t}\n\n\tgetShellCommandPrefix(): string | undefined {\n\t\treturn this.settings.shellCommandPrefix;\n\t}\n\n\tsetShellCommandPrefix(prefix: string | undefined): void {\n\t\tthis.globalSettings.shellCommandPrefix = prefix;\n\t\tthis.markModified(\"shellCommandPrefix\");\n\t\tthis.save();\n\t}\n\n\tgetNpmCommand(): string[] | undefined {\n\t\treturn this.settings.npmCommand ? [...this.settings.npmCommand] : undefined;\n\t}\n\n\tsetNpmCommand(command: string[] | undefined): void {\n\t\tthis.globalSettings.npmCommand = command ? [...command] : undefined;\n\t\tthis.markModified(\"npmCommand\");\n\t\tthis.save();\n\t}\n\n\tgetCollapseChangelog(): boolean {\n\t\treturn this.settings.collapseChangelog ?? false;\n\t}\n\n\tsetCollapseChangelog(collapse: boolean): void {\n\t\tthis.globalSettings.collapseChangelog = collapse;\n\t\tthis.markModified(\"collapseChangelog\");\n\t\tthis.save();\n\t}\n\n\tgetPackages(): PackageSource[] {\n\t\treturn [...(this.settings.packages ?? [])];\n\t}\n\n\tsetPackages(packages: PackageSource[]): void {\n\t\tthis.globalSettings.packages = packages;\n\t\tthis.markModified(\"packages\");\n\t\tthis.save();\n\t}\n\n\tsetProjectPackages(packages: PackageSource[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.packages = packages;\n\t\tthis.markProjectModified(\"packages\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetExtensionPaths(): string[] {\n\t\treturn [...(this.settings.extensions ?? [])];\n\t}\n\n\tsetExtensionPaths(paths: string[]): void {\n\t\tthis.globalSettings.extensions = paths;\n\t\tthis.markModified(\"extensions\");\n\t\tthis.save();\n\t}\n\n\tsetProjectExtensionPaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.extensions = paths;\n\t\tthis.markProjectModified(\"extensions\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetSkillPaths(): string[] {\n\t\treturn [...(this.settings.skills ?? [])];\n\t}\n\n\tsetSkillPaths(paths: string[]): void {\n\t\tthis.globalSettings.skills = paths;\n\t\tthis.markModified(\"skills\");\n\t\tthis.save();\n\t}\n\n\tsetProjectSkillPaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.skills = paths;\n\t\tthis.markProjectModified(\"skills\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetPromptTemplatePaths(): string[] {\n\t\treturn [...(this.settings.prompts ?? [])];\n\t}\n\n\tsetPromptTemplatePaths(paths: string[]): void {\n\t\tthis.globalSettings.prompts = paths;\n\t\tthis.markModified(\"prompts\");\n\t\tthis.save();\n\t}\n\n\tsetProjectPromptTemplatePaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.prompts = paths;\n\t\tthis.markProjectModified(\"prompts\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetThemePaths(): string[] {\n\t\treturn [...(this.settings.themes ?? [])];\n\t}\n\n\tsetThemePaths(paths: string[]): void {\n\t\tthis.globalSettings.themes = paths;\n\t\tthis.markModified(\"themes\");\n\t\tthis.save();\n\t}\n\n\tsetProjectThemePaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.themes = paths;\n\t\tthis.markProjectModified(\"themes\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetEnableSkillCommands(): boolean {\n\t\treturn this.settings.enableSkillCommands ?? true;\n\t}\n\n\tsetEnableSkillCommands(enabled: boolean): void {\n\t\tthis.globalSettings.enableSkillCommands = enabled;\n\t\tthis.markModified(\"enableSkillCommands\");\n\t\tthis.save();\n\t}\n\n\tgetThinkingBudgets(): ThinkingBudgetsSettings | undefined {\n\t\treturn this.settings.thinkingBudgets;\n\t}\n\n\tgetShowImages(): boolean {\n\t\treturn this.settings.terminal?.showImages ?? true;\n\t}\n\n\tsetShowImages(show: boolean): void {\n\t\tif (!this.globalSettings.terminal) {\n\t\t\tthis.globalSettings.terminal = {};\n\t\t}\n\t\tthis.globalSettings.terminal.showImages = show;\n\t\tthis.markModified(\"terminal\", \"showImages\");\n\t\tthis.save();\n\t}\n\n\tgetClearOnShrink(): boolean {\n\t\t// Settings takes precedence, then env var, then default false\n\t\tif (this.settings.terminal?.clearOnShrink !== undefined) {\n\t\t\treturn this.settings.terminal.clearOnShrink;\n\t\t}\n\t\treturn process.env.DREB_CLEAR_ON_SHRINK === \"1\";\n\t}\n\n\tsetClearOnShrink(enabled: boolean): void {\n\t\tif (!this.globalSettings.terminal) {\n\t\t\tthis.globalSettings.terminal = {};\n\t\t}\n\t\tthis.globalSettings.terminal.clearOnShrink = enabled;\n\t\tthis.markModified(\"terminal\", \"clearOnShrink\");\n\t\tthis.save();\n\t}\n\n\tgetImageAutoResize(): boolean {\n\t\treturn this.settings.images?.autoResize ?? true;\n\t}\n\n\tsetImageAutoResize(enabled: boolean): void {\n\t\tif (!this.globalSettings.images) {\n\t\t\tthis.globalSettings.images = {};\n\t\t}\n\t\tthis.globalSettings.images.autoResize = enabled;\n\t\tthis.markModified(\"images\", \"autoResize\");\n\t\tthis.save();\n\t}\n\n\tgetBlockImages(): boolean {\n\t\treturn this.settings.images?.blockImages ?? false;\n\t}\n\n\tsetBlockImages(blocked: boolean): void {\n\t\tif (!this.globalSettings.images) {\n\t\t\tthis.globalSettings.images = {};\n\t\t}\n\t\tthis.globalSettings.images.blockImages = blocked;\n\t\tthis.markModified(\"images\", \"blockImages\");\n\t\tthis.save();\n\t}\n\n\tgetEnabledModels(): string[] | undefined {\n\t\treturn this.settings.enabledModels;\n\t}\n\n\tsetEnabledModels(patterns: string[] | undefined): void {\n\t\tthis.globalSettings.enabledModels = patterns;\n\t\tthis.markModified(\"enabledModels\");\n\t\tthis.save();\n\t}\n\n\tgetDoubleEscapeAction(): \"fork\" | \"tree\" | \"none\" {\n\t\treturn this.settings.doubleEscapeAction ?? \"tree\";\n\t}\n\n\tsetDoubleEscapeAction(action: \"fork\" | \"tree\" | \"none\"): void {\n\t\tthis.globalSettings.doubleEscapeAction = action;\n\t\tthis.markModified(\"doubleEscapeAction\");\n\t\tthis.save();\n\t}\n\n\tgetTreeFilterMode(): \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\" {\n\t\tconst mode = this.settings.treeFilterMode;\n\t\tconst valid = [\"default\", \"no-tools\", \"user-only\", \"labeled-only\", \"all\"];\n\t\treturn mode && valid.includes(mode) ? mode : \"default\";\n\t}\n\n\tsetTreeFilterMode(mode: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\"): void {\n\t\tthis.globalSettings.treeFilterMode = mode;\n\t\tthis.markModified(\"treeFilterMode\");\n\t\tthis.save();\n\t}\n\n\tgetShowHardwareCursor(): boolean {\n\t\treturn this.settings.showHardwareCursor ?? process.env.DREB_HARDWARE_CURSOR === \"1\";\n\t}\n\n\tsetShowHardwareCursor(enabled: boolean): void {\n\t\tthis.globalSettings.showHardwareCursor = enabled;\n\t\tthis.markModified(\"showHardwareCursor\");\n\t\tthis.save();\n\t}\n\n\tgetEditorPaddingX(): number {\n\t\treturn this.settings.editorPaddingX ?? 0;\n\t}\n\n\tsetEditorPaddingX(padding: number): void {\n\t\tthis.globalSettings.editorPaddingX = Math.max(0, Math.min(3, Math.floor(padding)));\n\t\tthis.markModified(\"editorPaddingX\");\n\t\tthis.save();\n\t}\n\n\tgetAutocompleteMaxVisible(): number {\n\t\treturn this.settings.autocompleteMaxVisible ?? 5;\n\t}\n\n\tsetAutocompleteMaxVisible(maxVisible: number): void {\n\t\tthis.globalSettings.autocompleteMaxVisible = Math.max(3, Math.min(20, Math.floor(maxVisible)));\n\t\tthis.markModified(\"autocompleteMaxVisible\");\n\t\tthis.save();\n\t}\n\n\tgetCodeBlockIndent(): string {\n\t\treturn this.settings.markdown?.codeBlockIndent ?? \" \";\n\t}\n\n\tgetForbiddenCommands(): string[] | undefined {\n\t\treturn this.settings.forbiddenCommands;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"settings-manager.d.ts","sourceRoot":"","sources":["../../src/core/settings-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Transport } from \"@dreb/ai\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport { CONFIG_DIR_NAME, getAgentDir } from \"../config.js\";\n\nexport interface CompactionSettings {\n\tenabled?: boolean; // default: true\n\treserveTokens?: number; // default: 16384\n\tkeepRecentTokens?: number; // default: 20000\n}\n\nexport interface BranchSummarySettings {\n\treserveTokens?: number; // default: 16384 (tokens reserved for prompt + LLM response)\n\tskipPrompt?: boolean; // default: false - when true, skips \"Summarize branch?\" prompt and defaults to no summary\n}\n\nexport interface RetrySettings {\n\tenabled?: boolean; // default: true\n\tmaxRetries?: number; // default: 3\n\tbaseDelayMs?: number; // default: 2000 (exponential backoff: 2s, 4s, 8s)\n\tmaxDelayMs?: number; // default: 60000 (max server-requested delay before failing)\n}\n\nexport interface TerminalSettings {\n\tshowImages?: boolean; // default: true (only relevant if terminal supports images)\n}\n\nexport interface ImageSettings {\n\tautoResize?: boolean; // default: true (resize images to 2000x2000 max for better model compatibility)\n\tblockImages?: boolean; // default: false - when true, prevents all images from being sent to LLM providers\n}\n\nexport interface ThinkingBudgetsSettings {\n\tminimal?: number;\n\tlow?: number;\n\tmedium?: number;\n\thigh?: number;\n}\n\nexport interface MarkdownSettings {\n\tcodeBlockIndent?: string; // default: \" \"\n}\n\nexport type TransportSetting = Transport;\n\n/**\n * Package source for npm/git packages.\n * - String form: load all resources from the package\n * - Object form: filter which resources to load\n */\nexport type PackageSource =\n\t| string\n\t| {\n\t\t\tsource: string;\n\t\t\textensions?: string[];\n\t\t\tskills?: string[];\n\t\t\tprompts?: string[];\n\t\t\tthemes?: string[];\n\t };\n\nexport interface Settings {\n\tlastChangelogVersion?: string;\n\tdefaultProvider?: string;\n\tdefaultModel?: string;\n\tdefaultThinkingLevel?: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\";\n\ttransport?: TransportSetting; // default: \"sse\"\n\tsteeringMode?: \"all\" | \"one-at-a-time\";\n\tfollowUpMode?: \"all\" | \"one-at-a-time\";\n\ttheme?: string;\n\tcompaction?: CompactionSettings;\n\tbranchSummary?: BranchSummarySettings;\n\tretry?: RetrySettings;\n\thideThinkingBlock?: boolean;\n\tshellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)\n\tquietStartup?: boolean;\n\tshellCommandPrefix?: string; // Prefix prepended to every bash command (e.g., \"shopt -s expand_aliases\" for alias support)\n\tnpmCommand?: string[]; // Command used for npm package lookup/install operations, argv-style (e.g., [\"mise\", \"exec\", \"node@20\", \"--\", \"npm\"])\n\tcollapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)\n\tpackages?: PackageSource[]; // Array of npm/git package sources (string or object with filtering)\n\textensions?: string[]; // Array of local extension file paths or directories\n\tskills?: string[]; // Array of local skill file paths or directories\n\tprompts?: string[]; // Array of local prompt template paths or directories\n\tthemes?: string[]; // Array of local theme file paths or directories\n\tenableSkillCommands?: boolean; // default: true - register skills as /skill:name commands\n\tterminal?: TerminalSettings;\n\timages?: ImageSettings;\n\tenabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)\n\tdoubleEscapeAction?: \"fork\" | \"tree\" | \"none\"; // Action for double-escape with empty editor (default: \"tree\")\n\ttreeFilterMode?: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\"; // Default filter when opening /tree\n\tthinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels\n\teditorPaddingX?: number; // Horizontal padding for input editor (default: 0)\n\tautocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5)\n\tshowHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME\n\tmarkdown?: MarkdownSettings;\n\tsessionDir?: string; // Custom session storage directory (same format as --session-dir CLI flag)\n\tforbiddenCommands?: string[]; // Regex patterns for commands blocked by the forbidden-commands guard\n}\n\n/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */\nfunction deepMergeSettings(base: Settings, overrides: Settings): Settings {\n\tconst result: Settings = { ...base };\n\n\tfor (const key of Object.keys(overrides) as (keyof Settings)[]) {\n\t\tconst overrideValue = overrides[key];\n\t\tconst baseValue = base[key];\n\n\t\tif (overrideValue === undefined) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// For nested objects, merge recursively\n\t\tif (\n\t\t\ttypeof overrideValue === \"object\" &&\n\t\t\toverrideValue !== null &&\n\t\t\t!Array.isArray(overrideValue) &&\n\t\t\ttypeof baseValue === \"object\" &&\n\t\t\tbaseValue !== null &&\n\t\t\t!Array.isArray(baseValue)\n\t\t) {\n\t\t\t(result as Record<string, unknown>)[key] = { ...baseValue, ...overrideValue };\n\t\t} else {\n\t\t\t// For primitives and arrays, override value wins\n\t\t\t(result as Record<string, unknown>)[key] = overrideValue;\n\t\t}\n\t}\n\n\treturn result;\n}\n\nexport type SettingsScope = \"global\" | \"project\";\n\nexport interface SettingsStorage {\n\twithLock(scope: SettingsScope, fn: (current: string | undefined) => string | undefined): void;\n}\n\nexport interface SettingsError {\n\tscope: SettingsScope;\n\terror: Error;\n}\n\nexport class FileSettingsStorage implements SettingsStorage {\n\tprivate globalSettingsPath: string;\n\tprivate projectSettingsPath: string;\n\n\tconstructor(cwd: string = process.cwd(), agentDir: string = getAgentDir()) {\n\t\tthis.globalSettingsPath = join(agentDir, \"settings.json\");\n\t\tthis.projectSettingsPath = join(cwd, CONFIG_DIR_NAME, \"settings.json\");\n\t}\n\n\tprivate acquireLockSyncWithRetry(path: string): () => void {\n\t\tconst maxAttempts = 10;\n\t\tconst delayMs = 20;\n\t\tlet lastError: unknown;\n\n\t\tfor (let attempt = 1; attempt <= maxAttempts; attempt++) {\n\t\t\ttry {\n\t\t\t\treturn lockfile.lockSync(path, { realpath: false });\n\t\t\t} catch (error) {\n\t\t\t\tconst code =\n\t\t\t\t\ttypeof error === \"object\" && error !== null && \"code\" in error\n\t\t\t\t\t\t? String((error as { code?: unknown }).code)\n\t\t\t\t\t\t: undefined;\n\t\t\t\tif (code !== \"ELOCKED\" || attempt === maxAttempts) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tlastError = error;\n\t\t\t\tconst start = Date.now();\n\t\t\t\twhile (Date.now() - start < delayMs) {\n\t\t\t\t\t// Sleep synchronously to avoid changing callers to async.\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthrow (lastError as Error) ?? new Error(\"Failed to acquire settings lock\");\n\t}\n\n\twithLock(scope: SettingsScope, fn: (current: string | undefined) => string | undefined): void {\n\t\tconst path = scope === \"global\" ? this.globalSettingsPath : this.projectSettingsPath;\n\t\tconst dir = dirname(path);\n\n\t\tlet release: (() => void) | undefined;\n\t\ttry {\n\t\t\t// Only create directory and lock if file exists or we need to write\n\t\t\tconst fileExists = existsSync(path);\n\t\t\tif (fileExists) {\n\t\t\t\trelease = this.acquireLockSyncWithRetry(path);\n\t\t\t}\n\t\t\tconst current = fileExists ? readFileSync(path, \"utf-8\") : undefined;\n\t\t\tconst next = fn(current);\n\t\t\tif (next !== undefined) {\n\t\t\t\t// Only create directory when we actually need to write\n\t\t\t\tif (!existsSync(dir)) {\n\t\t\t\t\tmkdirSync(dir, { recursive: true });\n\t\t\t\t}\n\t\t\t\tif (!release) {\n\t\t\t\t\trelease = this.acquireLockSyncWithRetry(path);\n\t\t\t\t}\n\t\t\t\twriteFileSync(path, next, \"utf-8\");\n\t\t\t}\n\t\t} finally {\n\t\t\tif (release) {\n\t\t\t\trelease();\n\t\t\t}\n\t\t}\n\t}\n}\n\nexport class InMemorySettingsStorage implements SettingsStorage {\n\tprivate global: string | undefined;\n\tprivate project: string | undefined;\n\n\twithLock(scope: SettingsScope, fn: (current: string | undefined) => string | undefined): void {\n\t\tconst current = scope === \"global\" ? this.global : this.project;\n\t\tconst next = fn(current);\n\t\tif (next !== undefined) {\n\t\t\tif (scope === \"global\") {\n\t\t\t\tthis.global = next;\n\t\t\t} else {\n\t\t\t\tthis.project = next;\n\t\t\t}\n\t\t}\n\t}\n}\n\nexport class SettingsManager {\n\tprivate storage: SettingsStorage;\n\tprivate globalSettings: Settings;\n\tprivate projectSettings: Settings;\n\tprivate settings: Settings;\n\tprivate modifiedFields = new Set<keyof Settings>(); // Track global fields modified during session\n\tprivate modifiedNestedFields = new Map<keyof Settings, Set<string>>(); // Track global nested field modifications\n\tprivate modifiedProjectFields = new Set<keyof Settings>(); // Track project fields modified during session\n\tprivate modifiedProjectNestedFields = new Map<keyof Settings, Set<string>>(); // Track project nested field modifications\n\tprivate globalSettingsLoadError: Error | null = null; // Track if global settings file had parse errors\n\tprivate projectSettingsLoadError: Error | null = null; // Track if project settings file had parse errors\n\tprivate writeQueue: Promise<void> = Promise.resolve();\n\tprivate errors: SettingsError[];\n\n\tprivate constructor(\n\t\tstorage: SettingsStorage,\n\t\tinitialGlobal: Settings,\n\t\tinitialProject: Settings,\n\t\tglobalLoadError: Error | null = null,\n\t\tprojectLoadError: Error | null = null,\n\t\tinitialErrors: SettingsError[] = [],\n\t) {\n\t\tthis.storage = storage;\n\t\tthis.globalSettings = initialGlobal;\n\t\tthis.projectSettings = initialProject;\n\t\tthis.globalSettingsLoadError = globalLoadError;\n\t\tthis.projectSettingsLoadError = projectLoadError;\n\t\tthis.errors = [...initialErrors];\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\t}\n\n\t/** Create a SettingsManager that loads from files */\n\tstatic create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): SettingsManager {\n\t\tconst storage = new FileSettingsStorage(cwd, agentDir);\n\t\treturn SettingsManager.fromStorage(storage);\n\t}\n\n\t/** Create a SettingsManager from an arbitrary storage backend */\n\tstatic fromStorage(storage: SettingsStorage): SettingsManager {\n\t\tconst globalLoad = SettingsManager.tryLoadFromStorage(storage, \"global\");\n\t\tconst projectLoad = SettingsManager.tryLoadFromStorage(storage, \"project\");\n\t\tconst initialErrors: SettingsError[] = [];\n\t\tif (globalLoad.error) {\n\t\t\tinitialErrors.push({ scope: \"global\", error: globalLoad.error });\n\t\t}\n\t\tif (projectLoad.error) {\n\t\t\tinitialErrors.push({ scope: \"project\", error: projectLoad.error });\n\t\t}\n\n\t\treturn new SettingsManager(\n\t\t\tstorage,\n\t\t\tglobalLoad.settings,\n\t\t\tprojectLoad.settings,\n\t\t\tglobalLoad.error,\n\t\t\tprojectLoad.error,\n\t\t\tinitialErrors,\n\t\t);\n\t}\n\n\t/** Create an in-memory SettingsManager (no file I/O) */\n\tstatic inMemory(settings: Partial<Settings> = {}): SettingsManager {\n\t\tconst storage = new InMemorySettingsStorage();\n\t\treturn new SettingsManager(storage, settings, {});\n\t}\n\n\tprivate static loadFromStorage(storage: SettingsStorage, scope: SettingsScope): Settings {\n\t\tlet content: string | undefined;\n\t\tstorage.withLock(scope, (current) => {\n\t\t\tcontent = current;\n\t\t\treturn undefined;\n\t\t});\n\n\t\tif (!content) {\n\t\t\treturn {};\n\t\t}\n\t\tconst settings = JSON.parse(content);\n\t\treturn SettingsManager.migrateSettings(settings);\n\t}\n\n\tprivate static tryLoadFromStorage(\n\t\tstorage: SettingsStorage,\n\t\tscope: SettingsScope,\n\t): { settings: Settings; error: Error | null } {\n\t\ttry {\n\t\t\treturn { settings: SettingsManager.loadFromStorage(storage, scope), error: null };\n\t\t} catch (error) {\n\t\t\treturn { settings: {}, error: error as Error };\n\t\t}\n\t}\n\n\t/** Migrate old settings format to new format */\n\tprivate static migrateSettings(settings: Record<string, unknown>): Settings {\n\t\t// Migrate queueMode -> steeringMode\n\t\tif (\"queueMode\" in settings && !(\"steeringMode\" in settings)) {\n\t\t\tsettings.steeringMode = settings.queueMode;\n\t\t\tdelete settings.queueMode;\n\t\t}\n\n\t\t// Migrate legacy websockets boolean -> transport enum\n\t\tif (!(\"transport\" in settings) && typeof settings.websockets === \"boolean\") {\n\t\t\tsettings.transport = settings.websockets ? \"websocket\" : \"sse\";\n\t\t\tdelete settings.websockets;\n\t\t}\n\n\t\t// Migrate old skills object format to new array format\n\t\tif (\n\t\t\t\"skills\" in settings &&\n\t\t\ttypeof settings.skills === \"object\" &&\n\t\t\tsettings.skills !== null &&\n\t\t\t!Array.isArray(settings.skills)\n\t\t) {\n\t\t\tconst skillsSettings = settings.skills as {\n\t\t\t\tenableSkillCommands?: boolean;\n\t\t\t\tcustomDirectories?: unknown;\n\t\t\t};\n\t\t\tif (skillsSettings.enableSkillCommands !== undefined && settings.enableSkillCommands === undefined) {\n\t\t\t\tsettings.enableSkillCommands = skillsSettings.enableSkillCommands;\n\t\t\t}\n\t\t\tif (Array.isArray(skillsSettings.customDirectories) && skillsSettings.customDirectories.length > 0) {\n\t\t\t\tsettings.skills = skillsSettings.customDirectories;\n\t\t\t} else {\n\t\t\t\tdelete settings.skills;\n\t\t\t}\n\t\t}\n\n\t\treturn settings as Settings;\n\t}\n\n\tgetGlobalSettings(): Settings {\n\t\treturn structuredClone(this.globalSettings);\n\t}\n\n\tgetProjectSettings(): Settings {\n\t\treturn structuredClone(this.projectSettings);\n\t}\n\n\treload(): void {\n\t\tconst globalLoad = SettingsManager.tryLoadFromStorage(this.storage, \"global\");\n\t\tif (!globalLoad.error) {\n\t\t\tthis.globalSettings = globalLoad.settings;\n\t\t\tthis.globalSettingsLoadError = null;\n\t\t} else {\n\t\t\tthis.globalSettingsLoadError = globalLoad.error;\n\t\t\tthis.recordError(\"global\", globalLoad.error);\n\t\t}\n\n\t\tthis.modifiedFields.clear();\n\t\tthis.modifiedNestedFields.clear();\n\t\tthis.modifiedProjectFields.clear();\n\t\tthis.modifiedProjectNestedFields.clear();\n\n\t\tconst projectLoad = SettingsManager.tryLoadFromStorage(this.storage, \"project\");\n\t\tif (!projectLoad.error) {\n\t\t\tthis.projectSettings = projectLoad.settings;\n\t\t\tthis.projectSettingsLoadError = null;\n\t\t} else {\n\t\t\tthis.projectSettingsLoadError = projectLoad.error;\n\t\t\tthis.recordError(\"project\", projectLoad.error);\n\t\t}\n\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\t}\n\n\t/** Apply additional overrides on top of current settings */\n\tapplyOverrides(overrides: Partial<Settings>): void {\n\t\tthis.settings = deepMergeSettings(this.settings, overrides);\n\t}\n\n\t/** Mark a global field as modified during this session */\n\tprivate markModified(field: keyof Settings, nestedKey?: string): void {\n\t\tthis.modifiedFields.add(field);\n\t\tif (nestedKey) {\n\t\t\tif (!this.modifiedNestedFields.has(field)) {\n\t\t\t\tthis.modifiedNestedFields.set(field, new Set());\n\t\t\t}\n\t\t\tthis.modifiedNestedFields.get(field)!.add(nestedKey);\n\t\t}\n\t}\n\n\t/** Mark a project field as modified during this session */\n\tprivate markProjectModified(field: keyof Settings, nestedKey?: string): void {\n\t\tthis.modifiedProjectFields.add(field);\n\t\tif (nestedKey) {\n\t\t\tif (!this.modifiedProjectNestedFields.has(field)) {\n\t\t\t\tthis.modifiedProjectNestedFields.set(field, new Set());\n\t\t\t}\n\t\t\tthis.modifiedProjectNestedFields.get(field)!.add(nestedKey);\n\t\t}\n\t}\n\n\tprivate recordError(scope: SettingsScope, error: unknown): void {\n\t\tconst normalizedError = error instanceof Error ? error : new Error(String(error));\n\t\tthis.errors.push({ scope, error: normalizedError });\n\t}\n\n\tprivate clearModifiedScope(scope: SettingsScope): void {\n\t\tif (scope === \"global\") {\n\t\t\tthis.modifiedFields.clear();\n\t\t\tthis.modifiedNestedFields.clear();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.modifiedProjectFields.clear();\n\t\tthis.modifiedProjectNestedFields.clear();\n\t}\n\n\tprivate enqueueWrite(scope: SettingsScope, task: () => void): void {\n\t\tthis.writeQueue = this.writeQueue\n\t\t\t.then(() => {\n\t\t\t\ttask();\n\t\t\t\tthis.clearModifiedScope(scope);\n\t\t\t})\n\t\t\t.catch((error) => {\n\t\t\t\tthis.recordError(scope, error);\n\t\t\t});\n\t}\n\n\tprivate cloneModifiedNestedFields(source: Map<keyof Settings, Set<string>>): Map<keyof Settings, Set<string>> {\n\t\tconst snapshot = new Map<keyof Settings, Set<string>>();\n\t\tfor (const [key, value] of source.entries()) {\n\t\t\tsnapshot.set(key, new Set(value));\n\t\t}\n\t\treturn snapshot;\n\t}\n\n\tprivate persistScopedSettings(\n\t\tscope: SettingsScope,\n\t\tsnapshotSettings: Settings,\n\t\tmodifiedFields: Set<keyof Settings>,\n\t\tmodifiedNestedFields: Map<keyof Settings, Set<string>>,\n\t): void {\n\t\tthis.storage.withLock(scope, (current) => {\n\t\t\tconst currentFileSettings = current\n\t\t\t\t? SettingsManager.migrateSettings(JSON.parse(current) as Record<string, unknown>)\n\t\t\t\t: {};\n\t\t\tconst mergedSettings: Settings = { ...currentFileSettings };\n\t\t\tfor (const field of modifiedFields) {\n\t\t\t\tconst value = snapshotSettings[field];\n\t\t\t\tif (modifiedNestedFields.has(field) && typeof value === \"object\" && value !== null) {\n\t\t\t\t\tconst nestedModified = modifiedNestedFields.get(field)!;\n\t\t\t\t\tconst baseNested = (currentFileSettings[field] as Record<string, unknown>) ?? {};\n\t\t\t\t\tconst inMemoryNested = value as Record<string, unknown>;\n\t\t\t\t\tconst mergedNested = { ...baseNested };\n\t\t\t\t\tfor (const nestedKey of nestedModified) {\n\t\t\t\t\t\tmergedNested[nestedKey] = inMemoryNested[nestedKey];\n\t\t\t\t\t}\n\t\t\t\t\t(mergedSettings as Record<string, unknown>)[field] = mergedNested;\n\t\t\t\t} else {\n\t\t\t\t\t(mergedSettings as Record<string, unknown>)[field] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn JSON.stringify(mergedSettings, null, 2);\n\t\t});\n\t}\n\n\tprivate save(): void {\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\n\t\tif (this.globalSettingsLoadError) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst snapshotGlobalSettings = structuredClone(this.globalSettings);\n\t\tconst modifiedFields = new Set(this.modifiedFields);\n\t\tconst modifiedNestedFields = this.cloneModifiedNestedFields(this.modifiedNestedFields);\n\n\t\tthis.enqueueWrite(\"global\", () => {\n\t\t\tthis.persistScopedSettings(\"global\", snapshotGlobalSettings, modifiedFields, modifiedNestedFields);\n\t\t});\n\t}\n\n\tprivate saveProjectSettings(settings: Settings): void {\n\t\tthis.projectSettings = structuredClone(settings);\n\t\tthis.settings = deepMergeSettings(this.globalSettings, this.projectSettings);\n\n\t\tif (this.projectSettingsLoadError) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst snapshotProjectSettings = structuredClone(this.projectSettings);\n\t\tconst modifiedFields = new Set(this.modifiedProjectFields);\n\t\tconst modifiedNestedFields = this.cloneModifiedNestedFields(this.modifiedProjectNestedFields);\n\t\tthis.enqueueWrite(\"project\", () => {\n\t\t\tthis.persistScopedSettings(\"project\", snapshotProjectSettings, modifiedFields, modifiedNestedFields);\n\t\t});\n\t}\n\n\tasync flush(): Promise<void> {\n\t\tawait this.writeQueue;\n\t}\n\n\tdrainErrors(): SettingsError[] {\n\t\tconst drained = [...this.errors];\n\t\tthis.errors = [];\n\t\treturn drained;\n\t}\n\n\tgetLastChangelogVersion(): string | undefined {\n\t\treturn this.settings.lastChangelogVersion;\n\t}\n\n\tsetLastChangelogVersion(version: string): void {\n\t\tthis.globalSettings.lastChangelogVersion = version;\n\t\tthis.markModified(\"lastChangelogVersion\");\n\t\tthis.save();\n\t}\n\n\tgetSessionDir(): string | undefined {\n\t\treturn this.settings.sessionDir;\n\t}\n\n\tgetDefaultProvider(): string | undefined {\n\t\treturn this.settings.defaultProvider;\n\t}\n\n\tgetDefaultModel(): string | undefined {\n\t\treturn this.settings.defaultModel;\n\t}\n\n\tsetDefaultProvider(provider: string): void {\n\t\tthis.globalSettings.defaultProvider = provider;\n\t\tthis.markModified(\"defaultProvider\");\n\t\tthis.save();\n\t}\n\n\tsetDefaultModel(modelId: string): void {\n\t\tthis.globalSettings.defaultModel = modelId;\n\t\tthis.markModified(\"defaultModel\");\n\t\tthis.save();\n\t}\n\n\tsetDefaultModelAndProvider(provider: string, modelId: string): void {\n\t\tthis.globalSettings.defaultProvider = provider;\n\t\tthis.globalSettings.defaultModel = modelId;\n\t\tthis.markModified(\"defaultProvider\");\n\t\tthis.markModified(\"defaultModel\");\n\t\tthis.save();\n\t}\n\n\tgetSteeringMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn this.settings.steeringMode || \"one-at-a-time\";\n\t}\n\n\tsetSteeringMode(mode: \"all\" | \"one-at-a-time\"): void {\n\t\tthis.globalSettings.steeringMode = mode;\n\t\tthis.markModified(\"steeringMode\");\n\t\tthis.save();\n\t}\n\n\tgetFollowUpMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn this.settings.followUpMode || \"one-at-a-time\";\n\t}\n\n\tsetFollowUpMode(mode: \"all\" | \"one-at-a-time\"): void {\n\t\tthis.globalSettings.followUpMode = mode;\n\t\tthis.markModified(\"followUpMode\");\n\t\tthis.save();\n\t}\n\n\tgetTheme(): string | undefined {\n\t\treturn this.settings.theme;\n\t}\n\n\tsetTheme(theme: string): void {\n\t\tthis.globalSettings.theme = theme;\n\t\tthis.markModified(\"theme\");\n\t\tthis.save();\n\t}\n\n\tgetDefaultThinkingLevel(): \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\" | undefined {\n\t\treturn this.settings.defaultThinkingLevel;\n\t}\n\n\tsetDefaultThinkingLevel(level: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\"): void {\n\t\tthis.globalSettings.defaultThinkingLevel = level;\n\t\tthis.markModified(\"defaultThinkingLevel\");\n\t\tthis.save();\n\t}\n\n\tgetTransport(): TransportSetting {\n\t\treturn this.settings.transport ?? \"sse\";\n\t}\n\n\tsetTransport(transport: TransportSetting): void {\n\t\tthis.globalSettings.transport = transport;\n\t\tthis.markModified(\"transport\");\n\t\tthis.save();\n\t}\n\n\tgetCompactionEnabled(): boolean {\n\t\treturn this.settings.compaction?.enabled ?? true;\n\t}\n\n\tsetCompactionEnabled(enabled: boolean): void {\n\t\tif (!this.globalSettings.compaction) {\n\t\t\tthis.globalSettings.compaction = {};\n\t\t}\n\t\tthis.globalSettings.compaction.enabled = enabled;\n\t\tthis.markModified(\"compaction\", \"enabled\");\n\t\tthis.save();\n\t}\n\n\tgetCompactionReserveTokens(): number {\n\t\treturn this.settings.compaction?.reserveTokens ?? 16384;\n\t}\n\n\tgetCompactionKeepRecentTokens(): number {\n\t\treturn this.settings.compaction?.keepRecentTokens ?? 20000;\n\t}\n\n\tgetCompactionSettings(): { enabled: boolean; reserveTokens: number; keepRecentTokens: number } {\n\t\treturn {\n\t\t\tenabled: this.getCompactionEnabled(),\n\t\t\treserveTokens: this.getCompactionReserveTokens(),\n\t\t\tkeepRecentTokens: this.getCompactionKeepRecentTokens(),\n\t\t};\n\t}\n\n\tgetBranchSummarySettings(): { reserveTokens: number; skipPrompt: boolean } {\n\t\treturn {\n\t\t\treserveTokens: this.settings.branchSummary?.reserveTokens ?? 16384,\n\t\t\tskipPrompt: this.settings.branchSummary?.skipPrompt ?? false,\n\t\t};\n\t}\n\n\tgetBranchSummarySkipPrompt(): boolean {\n\t\treturn this.settings.branchSummary?.skipPrompt ?? false;\n\t}\n\n\tgetRetryEnabled(): boolean {\n\t\treturn this.settings.retry?.enabled ?? true;\n\t}\n\n\tsetRetryEnabled(enabled: boolean): void {\n\t\tif (!this.globalSettings.retry) {\n\t\t\tthis.globalSettings.retry = {};\n\t\t}\n\t\tthis.globalSettings.retry.enabled = enabled;\n\t\tthis.markModified(\"retry\", \"enabled\");\n\t\tthis.save();\n\t}\n\n\tgetRetrySettings(): { enabled: boolean; maxRetries: number; baseDelayMs: number; maxDelayMs: number } {\n\t\treturn {\n\t\t\tenabled: this.getRetryEnabled(),\n\t\t\tmaxRetries: this.settings.retry?.maxRetries ?? 3,\n\t\t\tbaseDelayMs: this.settings.retry?.baseDelayMs ?? 2000,\n\t\t\tmaxDelayMs: this.settings.retry?.maxDelayMs ?? 60000,\n\t\t};\n\t}\n\n\tgetHideThinkingBlock(): boolean {\n\t\treturn this.settings.hideThinkingBlock ?? false;\n\t}\n\n\tsetHideThinkingBlock(hide: boolean): void {\n\t\tthis.globalSettings.hideThinkingBlock = hide;\n\t\tthis.markModified(\"hideThinkingBlock\");\n\t\tthis.save();\n\t}\n\n\tgetShellPath(): string | undefined {\n\t\treturn this.settings.shellPath;\n\t}\n\n\tsetShellPath(path: string | undefined): void {\n\t\tthis.globalSettings.shellPath = path;\n\t\tthis.markModified(\"shellPath\");\n\t\tthis.save();\n\t}\n\n\tgetQuietStartup(): boolean {\n\t\treturn this.settings.quietStartup ?? false;\n\t}\n\n\tsetQuietStartup(quiet: boolean): void {\n\t\tthis.globalSettings.quietStartup = quiet;\n\t\tthis.markModified(\"quietStartup\");\n\t\tthis.save();\n\t}\n\n\tgetShellCommandPrefix(): string | undefined {\n\t\treturn this.settings.shellCommandPrefix;\n\t}\n\n\tsetShellCommandPrefix(prefix: string | undefined): void {\n\t\tthis.globalSettings.shellCommandPrefix = prefix;\n\t\tthis.markModified(\"shellCommandPrefix\");\n\t\tthis.save();\n\t}\n\n\tgetNpmCommand(): string[] | undefined {\n\t\treturn this.settings.npmCommand ? [...this.settings.npmCommand] : undefined;\n\t}\n\n\tsetNpmCommand(command: string[] | undefined): void {\n\t\tthis.globalSettings.npmCommand = command ? [...command] : undefined;\n\t\tthis.markModified(\"npmCommand\");\n\t\tthis.save();\n\t}\n\n\tgetCollapseChangelog(): boolean {\n\t\treturn this.settings.collapseChangelog ?? false;\n\t}\n\n\tsetCollapseChangelog(collapse: boolean): void {\n\t\tthis.globalSettings.collapseChangelog = collapse;\n\t\tthis.markModified(\"collapseChangelog\");\n\t\tthis.save();\n\t}\n\n\tgetPackages(): PackageSource[] {\n\t\treturn [...(this.settings.packages ?? [])];\n\t}\n\n\tsetPackages(packages: PackageSource[]): void {\n\t\tthis.globalSettings.packages = packages;\n\t\tthis.markModified(\"packages\");\n\t\tthis.save();\n\t}\n\n\tsetProjectPackages(packages: PackageSource[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.packages = packages;\n\t\tthis.markProjectModified(\"packages\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetExtensionPaths(): string[] {\n\t\treturn [...(this.settings.extensions ?? [])];\n\t}\n\n\tsetExtensionPaths(paths: string[]): void {\n\t\tthis.globalSettings.extensions = paths;\n\t\tthis.markModified(\"extensions\");\n\t\tthis.save();\n\t}\n\n\tsetProjectExtensionPaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.extensions = paths;\n\t\tthis.markProjectModified(\"extensions\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetSkillPaths(): string[] {\n\t\treturn [...(this.settings.skills ?? [])];\n\t}\n\n\tsetSkillPaths(paths: string[]): void {\n\t\tthis.globalSettings.skills = paths;\n\t\tthis.markModified(\"skills\");\n\t\tthis.save();\n\t}\n\n\tsetProjectSkillPaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.skills = paths;\n\t\tthis.markProjectModified(\"skills\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetPromptTemplatePaths(): string[] {\n\t\treturn [...(this.settings.prompts ?? [])];\n\t}\n\n\tsetPromptTemplatePaths(paths: string[]): void {\n\t\tthis.globalSettings.prompts = paths;\n\t\tthis.markModified(\"prompts\");\n\t\tthis.save();\n\t}\n\n\tsetProjectPromptTemplatePaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.prompts = paths;\n\t\tthis.markProjectModified(\"prompts\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetThemePaths(): string[] {\n\t\treturn [...(this.settings.themes ?? [])];\n\t}\n\n\tsetThemePaths(paths: string[]): void {\n\t\tthis.globalSettings.themes = paths;\n\t\tthis.markModified(\"themes\");\n\t\tthis.save();\n\t}\n\n\tsetProjectThemePaths(paths: string[]): void {\n\t\tconst projectSettings = structuredClone(this.projectSettings);\n\t\tprojectSettings.themes = paths;\n\t\tthis.markProjectModified(\"themes\");\n\t\tthis.saveProjectSettings(projectSettings);\n\t}\n\n\tgetEnableSkillCommands(): boolean {\n\t\treturn this.settings.enableSkillCommands ?? true;\n\t}\n\n\tsetEnableSkillCommands(enabled: boolean): void {\n\t\tthis.globalSettings.enableSkillCommands = enabled;\n\t\tthis.markModified(\"enableSkillCommands\");\n\t\tthis.save();\n\t}\n\n\tgetThinkingBudgets(): ThinkingBudgetsSettings | undefined {\n\t\treturn this.settings.thinkingBudgets;\n\t}\n\n\tgetShowImages(): boolean {\n\t\treturn this.settings.terminal?.showImages ?? true;\n\t}\n\n\tsetShowImages(show: boolean): void {\n\t\tif (!this.globalSettings.terminal) {\n\t\t\tthis.globalSettings.terminal = {};\n\t\t}\n\t\tthis.globalSettings.terminal.showImages = show;\n\t\tthis.markModified(\"terminal\", \"showImages\");\n\t\tthis.save();\n\t}\n\n\tgetImageAutoResize(): boolean {\n\t\treturn this.settings.images?.autoResize ?? true;\n\t}\n\n\tsetImageAutoResize(enabled: boolean): void {\n\t\tif (!this.globalSettings.images) {\n\t\t\tthis.globalSettings.images = {};\n\t\t}\n\t\tthis.globalSettings.images.autoResize = enabled;\n\t\tthis.markModified(\"images\", \"autoResize\");\n\t\tthis.save();\n\t}\n\n\tgetBlockImages(): boolean {\n\t\treturn this.settings.images?.blockImages ?? false;\n\t}\n\n\tsetBlockImages(blocked: boolean): void {\n\t\tif (!this.globalSettings.images) {\n\t\t\tthis.globalSettings.images = {};\n\t\t}\n\t\tthis.globalSettings.images.blockImages = blocked;\n\t\tthis.markModified(\"images\", \"blockImages\");\n\t\tthis.save();\n\t}\n\n\tgetEnabledModels(): string[] | undefined {\n\t\treturn this.settings.enabledModels;\n\t}\n\n\tsetEnabledModels(patterns: string[] | undefined): void {\n\t\tthis.globalSettings.enabledModels = patterns;\n\t\tthis.markModified(\"enabledModels\");\n\t\tthis.save();\n\t}\n\n\tgetDoubleEscapeAction(): \"fork\" | \"tree\" | \"none\" {\n\t\treturn this.settings.doubleEscapeAction ?? \"tree\";\n\t}\n\n\tsetDoubleEscapeAction(action: \"fork\" | \"tree\" | \"none\"): void {\n\t\tthis.globalSettings.doubleEscapeAction = action;\n\t\tthis.markModified(\"doubleEscapeAction\");\n\t\tthis.save();\n\t}\n\n\tgetTreeFilterMode(): \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\" {\n\t\tconst mode = this.settings.treeFilterMode;\n\t\tconst valid = [\"default\", \"no-tools\", \"user-only\", \"labeled-only\", \"all\"];\n\t\treturn mode && valid.includes(mode) ? mode : \"default\";\n\t}\n\n\tsetTreeFilterMode(mode: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\"): void {\n\t\tthis.globalSettings.treeFilterMode = mode;\n\t\tthis.markModified(\"treeFilterMode\");\n\t\tthis.save();\n\t}\n\n\tgetShowHardwareCursor(): boolean {\n\t\treturn this.settings.showHardwareCursor ?? process.env.DREB_HARDWARE_CURSOR === \"1\";\n\t}\n\n\tsetShowHardwareCursor(enabled: boolean): void {\n\t\tthis.globalSettings.showHardwareCursor = enabled;\n\t\tthis.markModified(\"showHardwareCursor\");\n\t\tthis.save();\n\t}\n\n\tgetEditorPaddingX(): number {\n\t\treturn this.settings.editorPaddingX ?? 0;\n\t}\n\n\tsetEditorPaddingX(padding: number): void {\n\t\tthis.globalSettings.editorPaddingX = Math.max(0, Math.min(3, Math.floor(padding)));\n\t\tthis.markModified(\"editorPaddingX\");\n\t\tthis.save();\n\t}\n\n\tgetAutocompleteMaxVisible(): number {\n\t\treturn this.settings.autocompleteMaxVisible ?? 5;\n\t}\n\n\tsetAutocompleteMaxVisible(maxVisible: number): void {\n\t\tthis.globalSettings.autocompleteMaxVisible = Math.max(3, Math.min(20, Math.floor(maxVisible)));\n\t\tthis.markModified(\"autocompleteMaxVisible\");\n\t\tthis.save();\n\t}\n\n\tgetCodeBlockIndent(): string {\n\t\treturn this.settings.markdown?.codeBlockIndent ?? \" \";\n\t}\n\n\tgetForbiddenCommands(): string[] | undefined {\n\t\treturn this.settings.forbiddenCommands;\n\t}\n}\n"]}
|
|
@@ -608,21 +608,6 @@ export class SettingsManager {
|
|
|
608
608
|
this.markModified("terminal", "showImages");
|
|
609
609
|
this.save();
|
|
610
610
|
}
|
|
611
|
-
getClearOnShrink() {
|
|
612
|
-
// Settings takes precedence, then env var, then default false
|
|
613
|
-
if (this.settings.terminal?.clearOnShrink !== undefined) {
|
|
614
|
-
return this.settings.terminal.clearOnShrink;
|
|
615
|
-
}
|
|
616
|
-
return process.env.DREB_CLEAR_ON_SHRINK === "1";
|
|
617
|
-
}
|
|
618
|
-
setClearOnShrink(enabled) {
|
|
619
|
-
if (!this.globalSettings.terminal) {
|
|
620
|
-
this.globalSettings.terminal = {};
|
|
621
|
-
}
|
|
622
|
-
this.globalSettings.terminal.clearOnShrink = enabled;
|
|
623
|
-
this.markModified("terminal", "clearOnShrink");
|
|
624
|
-
this.save();
|
|
625
|
-
}
|
|
626
611
|
getImageAutoResize() {
|
|
627
612
|
return this.settings.images?.autoResize ?? true;
|
|
628
613
|
}
|