chore: update compiled output, docs, and CLAUDE.md model defaults

This commit is contained in:
svemagie
2026-03-27 21:41:13 +01:00
parent 4208557800
commit b37e88e1e1
4 changed files with 327 additions and 16 deletions
+2 -2
View File
@@ -74,7 +74,7 @@ Entry: `src/main.ts` → bundled to `main.js` via esbuild (CJS, ES2018 target).
| Field | Default | Description | | Field | Default | Description |
|---|---|---| |---|---|---|
| `apiKey` | `""` | Anthropic API key | | `apiKey` | `""` | Anthropic API key |
| `model` | `claude-opus-4-5-20251101` | Claude model ID | | `model` | `claude-opus-4-6` | Claude model ID |
| `maxTokens` | `8192` | Max output tokens (102416000) | | `maxTokens` | `8192` | Max output tokens (102416000) |
| `maxContextNotes` | `6` | TF-IDF/embedding context notes per query | | `maxContextNotes` | `6` | TF-IDF/embedding context notes per query |
| `maxCharsPerNote` | `2500` | Characters per context note | | `maxCharsPerNote` | `2500` | Characters per context note |
@@ -118,4 +118,4 @@ Copy `main.js`, `manifest.json`, `styles.css` into `.obsidian/plugins/memex-chat
## Models (SettingsTab.ts) ## Models (SettingsTab.ts)
Default: `claude-opus-4-5-20251101`. Update `MODELS` array and `DEFAULT_SETTINGS.model` when adding new model IDs. Default: `claude-opus-4-6`. Update `MODELS` array and `DEFAULT_SETTINGS.model` when adding new model IDs.
@@ -0,0 +1,173 @@
# Fetch Models Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add an "Aktualisieren" button next to the model dropdown in settings that fetches the 3 newest Claude models from the Anthropic API and repopulates the dropdown.
**Architecture:** `ClaudeClient` gains a `fetchModels()` method (reusing existing `requestUrl`/`headers` patterns). `SettingsTab` captures the `DropdownComponent` and `ButtonComponent` references, wires the button to call `fetchModels`, and rebuilds the dropdown on success.
**Tech Stack:** TypeScript, Obsidian plugin API (`requestUrl`, `Setting`, `DropdownComponent`, `ButtonComponent`, `Notice`), Anthropic Models API (`GET /v1/models`)
---
## File Map
| File | Change |
|---|---|
| `src/ClaudeClient.ts` | Add `fetchModels(apiKey)` method |
| `src/SettingsTab.ts` | Update imports; refactor "Modell" `Setting` to capture dropdown + add button |
---
### Task 1: Add `fetchModels` to `ClaudeClient`
**Files:**
- Modify: `src/ClaudeClient.ts`
- [ ] **Step 1: Add the method after the `chat()` method**
In `src/ClaudeClient.ts`, add after line 86 (after `chat()`'s closing brace):
```typescript
/** Fetch the 3 newest Claude models from the Anthropic Models API. */
async fetchModels(apiKey: string): Promise<{ id: string; name: string }[]> {
const response = await requestUrl({
url: "https://api.anthropic.com/v1/models",
method: "GET",
headers: this.headers(apiKey),
throw: false,
});
if (response.status >= 400) {
throw new Error(`API Error ${response.status}: ${response.text}`);
}
const data: { id: string; created: number }[] = response.json.data ?? [];
if (data.length === 0) {
throw new Error("No models returned");
}
return data
.sort((a, b) => b.created - a.created)
.slice(0, 3)
.map((m) => ({ id: m.id, name: m.id }));
}
```
- [ ] **Step 2: Verify build passes**
```bash
npm run build
```
Expected: no TypeScript errors, `main.js` written successfully.
- [ ] **Step 3: Commit**
```bash
git add src/ClaudeClient.ts
git commit -m "feat: add fetchModels to ClaudeClient"
```
---
### Task 2: Update SettingsTab — imports and model setting
**Files:**
- Modify: `src/SettingsTab.ts:1` (import line)
- Modify: `src/SettingsTab.ts:151-160` (the "Modell" Setting block)
- [ ] **Step 1: Extend the import from `"obsidian"`**
Replace the current import line 1:
```typescript
import { App, PluginSettingTab, Setting } from "obsidian";
```
With:
```typescript
import { App, ButtonComponent, DropdownComponent, Notice, PluginSettingTab, Setting } from "obsidian";
```
- [ ] **Step 2: Replace the "Modell" Setting block**
Replace lines 151160:
```typescript
new Setting(containerEl)
.setName("Modell")
.setDesc("Welches Claude-Modell verwenden?")
.addDropdown((drop) => {
for (const m of MODELS) drop.addOption(m.id, m.name);
drop.setValue(this.plugin.settings.model).onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
});
});
```
With:
```typescript
let modelDrop: DropdownComponent;
let refreshBtn: ButtonComponent;
new Setting(containerEl)
.setName("Modell")
.setDesc("Welches Claude-Modell verwenden?")
.addDropdown((drop) => {
modelDrop = drop;
for (const m of MODELS) drop.addOption(m.id, m.name);
drop.setValue(this.plugin.settings.model).onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
});
})
.addButton((btn) => {
refreshBtn = btn;
btn.setButtonText("Aktualisieren").onClick(async () => {
const prev = modelDrop.getValue();
refreshBtn.setDisabled(true);
refreshBtn.setButtonText("...");
try {
const models = await this.plugin.claude.fetchModels(this.plugin.settings.apiKey);
modelDrop.selectEl.empty();
for (const m of models) modelDrop.addOption(m.id, m.name);
modelDrop.setValue(prev);
this.plugin.settings.model = modelDrop.getValue();
await this.plugin.saveSettings();
} catch (err) {
new Notice("Modelle konnten nicht geladen werden: " + (err as Error).message);
} finally {
refreshBtn.setDisabled(false);
refreshBtn.setButtonText("Aktualisieren");
}
});
});
```
- [ ] **Step 3: Verify build passes**
```bash
npm run build
```
Expected: no TypeScript errors, `main.js` written successfully.
- [ ] **Step 4: Manual smoke test in Obsidian**
1. Copy `main.js`, `manifest.json`, `styles.css` to `.obsidian/plugins/memex-chat/` in your vault
2. Reload the plugin (or restart Obsidian)
3. Open Settings → Memex Chat
4. Confirm the "Modell" row has a dropdown and an "Aktualisieren" button
5. With a valid API key set, click "Aktualisieren" — button should show "...", then restore; dropdown should show 3 model IDs
6. With no API key, click "Aktualisieren" — a Notice should appear with an error message; dropdown should be unchanged
- [ ] **Step 5: Commit**
```bash
git add src/SettingsTab.ts
git commit -m "feat: add Aktualisieren button to fetch models from API"
```
@@ -0,0 +1,89 @@
# Fetch Models Design
**Date:** 2026-03-27
**Status:** Approved
## Summary
Add an "Aktualisieren" button to the Model setting in the settings tab. When clicked, it fetches the 3 newest Claude models from the Anthropic Models API and updates the dropdown. Falls back to the hardcoded `MODELS` list if the request fails or returns no models.
## ClaudeClient changes
Add a new method `fetchModels(apiKey: string): Promise<{id: string, name: string}[]>` to `ClaudeClient`.
- URL: `"https://api.anthropic.com/v1/models"` — inline string or a separate private constant; **do not use or modify `baseUrl`** (which points to `/v1/messages`)
- Call `requestUrl` with `throw: false` (same pattern as `streamChat`/`chat`) and `this.headers(apiKey)`
- Response shape: `{ data: [{ id: string, created: number, display_name: string, ... }] }`
- Throw on `response.status >= 400` with the response text
- If `data` is empty, throw an error ("No models returned") — do not return an empty array
- Sort `data` descending by `created`, take top 3
- Return `{ id, name: id }` for each — use `id` as the display name (not `display_name`). Note: fetched entries will show raw IDs (e.g. `claude-opus-4-6`) while hardcoded `MODELS` show human-friendly names (e.g. `"Claude Opus 4.6 (Stärkste)"`). This is intentional — keeps the implementation simple and avoids relying on API-provided display strings.
## SettingsTab changes
**Import addition:** Add `Notice, ButtonComponent, DropdownComponent` to the `import { ... } from "obsidian"` line.
Convert the existing "Modell" `Setting` to capture both the `DropdownComponent` and `ButtonComponent` references by chaining `addDropdown()` and `addButton()` on the same `Setting` instance:
```typescript
let modelDrop: DropdownComponent;
let refreshBtn: ButtonComponent;
new Setting(containerEl)
.setName("Modell")
.setDesc("Welches Claude-Modell verwenden?")
.addDropdown((drop) => {
modelDrop = drop;
for (const m of MODELS) drop.addOption(m.id, m.name);
drop.setValue(this.plugin.settings.model).onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
});
})
.addButton((btn) => {
refreshBtn = btn;
btn.setButtonText("Aktualisieren").onClick(async () => { /* see click flow */ });
});
```
**Click flow:**
1. Capture current value: `const prev = modelDrop.getValue()`
2. `refreshBtn.setDisabled(true)` and `refreshBtn.setButtonText("...")`
3. In a try/catch/finally:
- **try:** Call `this.plugin.claude.fetchModels(this.plugin.settings.apiKey)`
- On success: clear dropdown with `modelDrop.selectEl.empty()`, repopulate via `modelDrop.addOption(id, name)` for each fetched model, then set value to `prev` if it exists among the fetched ids, otherwise the first fetched id; save via `this.plugin.settings.model = modelDrop.getValue(); await this.plugin.saveSettings()`
- **catch:** `new Notice("Modelle konnten nicht geladen werden: " + err.message)` — dropdown is **not** modified on error (hardcoded options remain)
- **finally:** `refreshBtn.setDisabled(false)` and `refreshBtn.setButtonText("Aktualisieren")`
**Fallback:** The hardcoded `MODELS` array in `SettingsTab.ts` is unchanged and remains the initial population of the dropdown on every settings open.
## Data flow
```
[Aktualisieren button click]
→ capture prev = modelDrop.getValue()
→ disable button, show "..."
→ this.plugin.claude.fetchModels(apiKey) [throw: false, separate URL]
→ throw if status >= 400 or data empty
→ sort by created desc, take 3
→ return [{id, name: id}]
→ clear selectEl, repopulate, restore selection
→ save model to settings
→ finally: restore button
```
## Error handling
| Scenario | Behaviour |
|---|---|
| No API key (401) | Notice shown; dropdown unchanged |
| Network failure | Notice shown; dropdown unchanged |
| Empty `data` array | Treated as error; Notice shown; dropdown unchanged |
| Fewer than 3 models returned | Take all returned (no error) |
## Out of scope
- Persisting fetched models across restarts
- Auto-fetching on settings open or plugin startup
- Configurable count of models to show
- Updating `DEFAULT_SETTINGS.model` after a fetch
+63 -14
View File
@@ -32725,13 +32725,37 @@ var ClaudeClient = class {
} }
return response.json.content?.[0]?.text ?? ""; return response.json.content?.[0]?.text ?? "";
} }
/**
* Fetch Claude models from the Anthropic Models API.
* Returns the 2 newest versions of each family (opus, sonnet, haiku), in that order.
*/
async fetchModels(apiKey) {
const response = await (0, import_obsidian2.requestUrl)({
url: "https://api.anthropic.com/v1/models",
method: "GET",
headers: this.headers(apiKey),
throw: false
});
if (response.status >= 400) {
throw new Error(`API Error ${response.status}: ${response.text}`);
}
const data = response.json.data ?? [];
if (data.length === 0) {
throw new Error("No models returned");
}
const sorted = data.sort((a, b) => b.created - a.created);
const families = ["opus", "sonnet", "haiku"];
return families.flatMap(
(family) => sorted.filter((m) => m.id.includes(family)).slice(0, 2).map((m) => ({ id: m.id, name: m.id }))
);
}
}; };
// src/SettingsTab.ts // src/SettingsTab.ts
var import_obsidian3 = require("obsidian"); var import_obsidian3 = require("obsidian");
var DEFAULT_SETTINGS = { var DEFAULT_SETTINGS = {
apiKey: "", apiKey: "",
model: "claude-opus-4-5-20251101", model: "claude-opus-4-6",
maxTokens: 8192, maxTokens: 8192,
maxContextNotes: 6, maxContextNotes: 6,
maxCharsPerNote: 2500, maxCharsPerNote: 2500,
@@ -32768,8 +32792,8 @@ Wenn du Fragen beantwortest:
] ]
}; };
var MODELS = [ var MODELS = [
{ id: "claude-opus-4-5-20251101", name: "Claude Opus 4.5 (St\xE4rkste)" }, { id: "claude-opus-4-6", name: "Claude Opus 4.6 (St\xE4rkste)" },
{ id: "claude-sonnet-4-5-20250929", name: "Claude Sonnet 4.5 (Empfohlen)" }, { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6 (Empfohlen)" },
{ id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5 (Schnell)" } { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5 (Schnell)" }
]; ];
var MemexChatSettingsTab = class extends import_obsidian3.PluginSettingTab { var MemexChatSettingsTab = class extends import_obsidian3.PluginSettingTab {
@@ -32827,13 +32851,37 @@ var MemexChatSettingsTab = class extends import_obsidian3.PluginSettingTab {
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) })
); );
new import_obsidian3.Setting(containerEl).setName("Modell").setDesc("Welches Claude-Modell verwenden?").addDropdown((drop) => { let modelDrop;
let refreshBtn;
new import_obsidian3.Setting(containerEl).setName("Modell").setDesc("Welches Claude-Modell verwenden? (Aktualisieren zeigt Roh-IDs)").addDropdown((drop) => {
modelDrop = drop;
for (const m of MODELS) for (const m of MODELS)
drop.addOption(m.id, m.name); drop.addOption(m.id, m.name);
drop.setValue(this.plugin.settings.model).onChange(async (value) => { drop.setValue(this.plugin.settings.model).onChange(async (value) => {
this.plugin.settings.model = value; this.plugin.settings.model = value;
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}); });
}).addButton((btn) => {
refreshBtn = btn;
btn.setButtonText("Aktualisieren").onClick(async () => {
const prev = modelDrop.getValue();
refreshBtn.setDisabled(true);
refreshBtn.setButtonText("...");
try {
const models = await this.plugin.claude.fetchModels(this.plugin.settings.apiKey);
modelDrop.selectEl.empty();
for (const m of models)
modelDrop.addOption(m.id, m.name);
modelDrop.setValue(prev);
this.plugin.settings.model = modelDrop.getValue();
await this.plugin.saveSettings();
} catch (err) {
new import_obsidian3.Notice("Modelle konnten nicht geladen werden: " + err.message);
} finally {
refreshBtn.setDisabled(false);
refreshBtn.setButtonText("Aktualisieren");
}
});
}); });
new import_obsidian3.Setting(containerEl).setName("Max. Antwort-Tokens").setDesc("Maximale L\xE4nge der Claude-Antwort. F\xFCr lange Analysen (z.B. Monthly Check) h\xF6her einstellen. (1024\u201316000)").addSlider( new import_obsidian3.Setting(containerEl).setName("Max. Antwort-Tokens").setDesc("Maximale L\xE4nge der Claude-Antwort. F\xFCr lange Analysen (z.B. Monthly Check) h\xF6her einstellen. (1024\u201316000)").addSlider(
(slider) => slider.setLimits(1024, 16e3, 512).setValue(this.plugin.settings.maxTokens).setDynamicTooltip().onChange(async (value) => { (slider) => slider.setLimits(1024, 16e3, 512).setValue(this.plugin.settings.maxTokens).setDynamicTooltip().onChange(async (value) => {
@@ -33314,6 +33362,12 @@ var MemexChatPlugin = class extends import_obsidian5.Plugin {
} }
}); });
this.addSettingTab(new MemexChatSettingsTab(this.app, this)); this.addSettingTab(new MemexChatSettingsTab(this.app, this));
this.registerEvent(
this.app.vault.on("modify", (file) => {
if (this.embedSearch && file instanceof import_obsidian5.TFile && file.extension === "md")
this.embedSearch.reembedFile(file);
})
);
this.app.workspace.onLayoutReady(() => { this.app.workspace.onLayoutReady(() => {
if (!this.search.isIndexed()) { if (!this.search.isIndexed()) {
this.search.priorityProperties = this.settings.contextProperties; this.search.priorityProperties = this.settings.contextProperties;
@@ -33366,24 +33420,19 @@ var MemexChatPlugin = class extends import_obsidian5.Plugin {
this.embedSearch = new EmbedSearch(this.app, this.settings.embeddingModel); this.embedSearch = new EmbedSearch(this.app, this.settings.embeddingModel);
this.embedSearch.excludeFolders = this.settings.embedExcludeFolders ?? []; this.embedSearch.excludeFolders = this.settings.embedExcludeFolders ?? [];
this.embedSearch.contextProperties = this.settings.contextProperties ?? []; this.embedSearch.contextProperties = this.settings.contextProperties ?? [];
this.registerEvent( const modelShort = this.settings.embeddingModel.split("/").pop() ?? this.settings.embeddingModel;
this.app.vault.on("modify", (file) => { const notice = new import_obsidian5.Notice(`Memex [${modelShort}]: Embedding wird vorbereitet\u2026`, 0);
if (this.embedSearch && file instanceof import_obsidian5.TFile && file.extension === "md")
this.embedSearch.reembedFile(file);
})
);
const notice = new import_obsidian5.Notice("Memex: Embedding wird vorbereitet\u2026", 0);
this.embedSearch.onModelStatus = (status) => { this.embedSearch.onModelStatus = (status) => {
notice.setMessage(`Memex: ${status}`); notice.setMessage(`Memex [${modelShort}]: ${status}`);
}; };
this.embedSearch.onProgress = (done, total, speed) => { this.embedSearch.onProgress = (done, total, speed) => {
const speedStr = speed > 0 ? ` \u2022 ${speed.toFixed(1)} N/s` : ""; const speedStr = speed > 0 ? ` \u2022 ${speed.toFixed(1)} N/s` : "";
const remaining = speed > 0 && done < total ? (total - done) / speed : 0; const remaining = speed > 0 && done < total ? (total - done) / speed : 0;
const eta = remaining > 0 ? ` \u2022 ~${remaining < 60 ? Math.ceil(remaining) + "s" : Math.ceil(remaining / 60) + "min"}` : ""; const eta = remaining > 0 ? ` \u2022 ~${remaining < 60 ? Math.ceil(remaining) + "s" : Math.ceil(remaining / 60) + "min"}` : "";
notice.setMessage(`Memex Embedding: ${done}/${total}${speedStr}${eta}`); notice.setMessage(`Memex [${modelShort}]: ${done}/${total}${speedStr}${eta}`);
}; };
this.waitForSyncIdle(notice).then(() => this.embedSearch?.buildIndex()).then(() => { this.waitForSyncIdle(notice).then(() => this.embedSearch?.buildIndex()).then(() => {
notice.setMessage(`\u2713 Memex: ${this.app.vault.getMarkdownFiles().length} Notizen eingebettet`); notice.setMessage(`\u2713 Memex [${modelShort}]: ${this.app.vault.getMarkdownFiles().length} Notizen eingebettet`);
setTimeout(() => notice.hide(), 4e3); setTimeout(() => notice.hide(), 4e3);
this.notifyRelatedView(); this.notifyRelatedView();
}).catch((e) => { }).catch((e) => {