vigthoria-cli 1.10.1 → 1.10.37

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.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Brain Hub client — sync workspace index + fetch account brain context
3
+ * via coder.vigthoria.io (JWT-authenticated).
4
+ */
5
+ export type BrainSyncPayload = {
6
+ workspaceName: string;
7
+ workspacePath: string;
8
+ fileCount: number;
9
+ chunkCount: number;
10
+ topFiles: string[];
11
+ summary: string;
12
+ indexHash: string;
13
+ };
14
+ export type BrainContextResponse = {
15
+ ok: boolean;
16
+ formattedText?: string;
17
+ memories?: Array<Record<string, unknown>>;
18
+ error?: string;
19
+ skipped?: boolean;
20
+ reason?: string;
21
+ };
22
+ export declare class BrainHubClient {
23
+ private apiBase;
24
+ private getAuthToken;
25
+ constructor(options: {
26
+ apiBase?: string;
27
+ getAuthToken: () => string | null | Promise<string | null>;
28
+ });
29
+ private headers;
30
+ syncWorkspaceIndex(payload: BrainSyncPayload): Promise<Record<string, unknown>>;
31
+ fetchAccountContext(limit?: number): Promise<BrainContextResponse>;
32
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Brain Hub client — sync workspace index + fetch account brain context
3
+ * via coder.vigthoria.io (JWT-authenticated).
4
+ */
5
+ export class BrainHubClient {
6
+ apiBase;
7
+ getAuthToken;
8
+ constructor(options) {
9
+ this.apiBase = (options.apiBase || 'https://coder.vigthoria.io').replace(/\/$/, '');
10
+ this.getAuthToken = options.getAuthToken;
11
+ }
12
+ async headers() {
13
+ const headers = { 'Content-Type': 'application/json' };
14
+ const token = await this.getAuthToken();
15
+ if (token) {
16
+ headers.Authorization = `Bearer ${token}`;
17
+ }
18
+ return headers;
19
+ }
20
+ async syncWorkspaceIndex(payload) {
21
+ const headers = await this.headers();
22
+ if (!headers.Authorization) {
23
+ return { ok: false, skipped: true, reason: 'not_authenticated' };
24
+ }
25
+ const resp = await fetch(`${this.apiBase}/api/brain/sync-index`, {
26
+ method: 'POST',
27
+ headers,
28
+ body: JSON.stringify(payload),
29
+ });
30
+ const data = await resp.json().catch(() => ({}));
31
+ if (!resp.ok) {
32
+ return { ok: false, error: data.error || `HTTP ${resp.status}` };
33
+ }
34
+ return data;
35
+ }
36
+ async fetchAccountContext(limit = 25) {
37
+ const headers = await this.headers();
38
+ if (!headers.Authorization) {
39
+ return { ok: false, formattedText: '', memories: [] };
40
+ }
41
+ const resp = await fetch(`${this.apiBase}/api/brain/context?limit=${limit}`, { headers });
42
+ const data = await resp.json().catch(() => ({}));
43
+ if (!resp.ok) {
44
+ return { ok: false, formattedText: '', error: data.error || `HTTP ${resp.status}` };
45
+ }
46
+ return data;
47
+ }
48
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Local TF-IDF codebase indexer for CLI (mirrors Vigthoria Code extension indexer).
3
+ */
4
+ export type CodebaseSearchResult = {
5
+ filePath: string;
6
+ relativePath: string;
7
+ startLine: number;
8
+ endLine: number;
9
+ content: string;
10
+ language: string;
11
+ score: number;
12
+ symbol: string | null;
13
+ };
14
+ export type IndexMeta = {
15
+ indexedFileCount: number;
16
+ totalChunks: number;
17
+ indexHash: string;
18
+ topFiles: string[];
19
+ indexedAt: string;
20
+ };
21
+ export declare class CodebaseIndexer {
22
+ private workspaceRoot;
23
+ private index;
24
+ private invertedIndex;
25
+ private chunkStore;
26
+ private fileHashes;
27
+ private indexedFileCount;
28
+ private totalChunks;
29
+ private isIndexing;
30
+ private readonly maxFileSize;
31
+ private readonly chunkSize;
32
+ private readonly chunkOverlap;
33
+ private readonly maxResults;
34
+ private readonly ignorePatterns;
35
+ private readonly supportedExtensions;
36
+ constructor(workspaceRoot: string);
37
+ getStatus(): {
38
+ indexedFileCount: number;
39
+ totalChunks: number;
40
+ isIndexing: boolean;
41
+ };
42
+ static metaPath(workspaceRoot: string): string;
43
+ static loadMeta(workspaceRoot: string): IndexMeta | null;
44
+ private saveMeta;
45
+ hasLocalIndex(): boolean;
46
+ countCandidateFiles(): number;
47
+ countIndexableFiles(): number;
48
+ indexWorkspace(): Promise<IndexMeta>;
49
+ search(query: string, maxResults?: number): CodebaseSearchResult[];
50
+ getContextForQuery(query: string, maxTokens?: number): string;
51
+ formatSearchResults(query: string, maxResults?: number): string;
52
+ private findFiles;
53
+ private indexFile;
54
+ private chunkFile;
55
+ private tokenize;
56
+ private isStopWord;
57
+ private getLanguage;
58
+ private shouldIgnore;
59
+ }
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Local TF-IDF codebase indexer for CLI (mirrors Vigthoria Code extension indexer).
3
+ */
4
+ import * as crypto from 'crypto';
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ export class CodebaseIndexer {
8
+ workspaceRoot;
9
+ index = new Map();
10
+ invertedIndex = new Map();
11
+ chunkStore = new Map();
12
+ fileHashes = new Map();
13
+ indexedFileCount = 0;
14
+ totalChunks = 0;
15
+ isIndexing = false;
16
+ maxFileSize = 1024 * 1024;
17
+ chunkSize = 50;
18
+ chunkOverlap = 5;
19
+ maxResults = 10;
20
+ ignorePatterns = [
21
+ 'node_modules', '.git', 'dist', 'build', 'out', '.next',
22
+ '__pycache__', '.pytest_cache', '.mypy_cache', 'venv', 'env',
23
+ '.DS_Store', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
24
+ '.vsix', '.map', '.min.js', '.min.css', 'coverage',
25
+ '.cache', '.tmp', 'tmp', 'temp', '.idea', '.vscode',
26
+ ];
27
+ supportedExtensions = new Set([
28
+ '.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.go', '.rs',
29
+ '.c', '.cpp', '.h', '.hpp', '.cs', '.rb', '.php', '.swift',
30
+ '.kt', '.scala', '.vue', '.svelte', '.astro',
31
+ '.html', '.css', '.scss', '.less', '.sass',
32
+ '.json', '.yaml', '.yml', '.toml', '.xml',
33
+ '.md', '.txt', '.rst', '.sh', '.bash', '.zsh',
34
+ '.sql', '.graphql', '.prisma', '.env',
35
+ '.dockerfile', '.tf', '.hcl',
36
+ ]);
37
+ constructor(workspaceRoot) {
38
+ this.workspaceRoot = path.resolve(workspaceRoot);
39
+ }
40
+ getStatus() {
41
+ return {
42
+ indexedFileCount: this.indexedFileCount,
43
+ totalChunks: this.totalChunks,
44
+ isIndexing: this.isIndexing,
45
+ };
46
+ }
47
+ static metaPath(workspaceRoot) {
48
+ return path.join(workspaceRoot, '.vigthoria', 'index', 'meta.json');
49
+ }
50
+ static loadMeta(workspaceRoot) {
51
+ try {
52
+ const metaPath = CodebaseIndexer.metaPath(workspaceRoot);
53
+ if (fs.existsSync(metaPath)) {
54
+ return JSON.parse(fs.readFileSync(metaPath, 'utf8'));
55
+ }
56
+ }
57
+ catch {
58
+ // fresh workspace
59
+ }
60
+ return null;
61
+ }
62
+ saveMeta(meta) {
63
+ const dir = path.join(this.workspaceRoot, '.vigthoria', 'index');
64
+ fs.mkdirSync(dir, { recursive: true });
65
+ fs.writeFileSync(CodebaseIndexer.metaPath(this.workspaceRoot), `${JSON.stringify(meta, null, 2)}\n`);
66
+ }
67
+ hasLocalIndex() {
68
+ const meta = CodebaseIndexer.loadMeta(this.workspaceRoot);
69
+ return !!(meta && meta.indexedFileCount > 0) || this.totalChunks > 0;
70
+ }
71
+ countCandidateFiles() {
72
+ return this.findFiles(this.workspaceRoot).length;
73
+ }
74
+ countIndexableFiles() {
75
+ return this.findFiles(this.workspaceRoot).length;
76
+ }
77
+ async indexWorkspace() {
78
+ if (this.isIndexing) {
79
+ return {
80
+ indexedFileCount: this.indexedFileCount,
81
+ totalChunks: this.totalChunks,
82
+ indexHash: '',
83
+ topFiles: [],
84
+ indexedAt: new Date().toISOString(),
85
+ };
86
+ }
87
+ this.isIndexing = true;
88
+ this.index.clear();
89
+ this.invertedIndex.clear();
90
+ this.chunkStore.clear();
91
+ this.fileHashes.clear();
92
+ this.indexedFileCount = 0;
93
+ this.totalChunks = 0;
94
+ try {
95
+ const files = this.findFiles(this.workspaceRoot);
96
+ for (const filePath of files) {
97
+ this.indexFile(filePath);
98
+ }
99
+ const indexHash = crypto.createHash('sha256')
100
+ .update(`${this.indexedFileCount}:${this.totalChunks}:${files.length}`)
101
+ .digest('hex')
102
+ .slice(0, 16);
103
+ const topFiles = files.slice(0, 12).map((filePath) => path.relative(this.workspaceRoot, filePath));
104
+ const meta = {
105
+ indexedFileCount: this.indexedFileCount,
106
+ totalChunks: this.totalChunks,
107
+ indexHash,
108
+ topFiles,
109
+ indexedAt: new Date().toISOString(),
110
+ };
111
+ this.saveMeta(meta);
112
+ return meta;
113
+ }
114
+ finally {
115
+ this.isIndexing = false;
116
+ }
117
+ }
118
+ search(query, maxResults = this.maxResults) {
119
+ if (this.totalChunks === 0) {
120
+ return [];
121
+ }
122
+ const queryTerms = this.tokenize(query.toLowerCase());
123
+ const scores = new Map();
124
+ for (const term of queryTerms) {
125
+ const matchingChunks = this.invertedIndex.get(term);
126
+ if (!matchingChunks)
127
+ continue;
128
+ const idf = Math.log(this.totalChunks / matchingChunks.size);
129
+ for (const chunkId of matchingChunks) {
130
+ const chunk = this.chunkStore.get(chunkId);
131
+ if (!chunk)
132
+ continue;
133
+ const tf = (chunk.terms.get(term) || 0) / chunk.totalTerms;
134
+ scores.set(chunkId, (scores.get(chunkId) || 0) + tf * idf);
135
+ }
136
+ }
137
+ for (const [chunkId, score] of scores) {
138
+ const chunk = this.chunkStore.get(chunkId);
139
+ if (!chunk)
140
+ continue;
141
+ const fileName = path.basename(chunk.filePath).toLowerCase();
142
+ for (const term of queryTerms) {
143
+ if (fileName.includes(term)) {
144
+ scores.set(chunkId, score * 1.5);
145
+ }
146
+ }
147
+ }
148
+ return Array.from(scores.entries())
149
+ .sort((a, b) => b[1] - a[1])
150
+ .slice(0, maxResults)
151
+ .map(([chunkId, score]) => {
152
+ const chunk = this.chunkStore.get(chunkId);
153
+ return {
154
+ filePath: chunk.filePath,
155
+ relativePath: chunk.relativePath,
156
+ startLine: chunk.startLine,
157
+ endLine: chunk.endLine,
158
+ content: chunk.content,
159
+ language: chunk.language,
160
+ score: Math.round(score * 1000) / 1000,
161
+ symbol: chunk.symbol,
162
+ };
163
+ });
164
+ }
165
+ getContextForQuery(query, maxTokens = 4000) {
166
+ const results = this.search(query, 15);
167
+ if (results.length === 0) {
168
+ return '';
169
+ }
170
+ let context = '### Relevant code from workspace:\n\n';
171
+ let currentTokens = 0;
172
+ const avgCharsPerToken = 4;
173
+ for (const result of results) {
174
+ const entry = `**${result.relativePath}** (lines ${result.startLine}-${result.endLine}):\n\`\`\`${result.language}\n${result.content}\n\`\`\`\n\n`;
175
+ const entryTokens = Math.ceil(entry.length / avgCharsPerToken);
176
+ if (currentTokens + entryTokens > maxTokens)
177
+ break;
178
+ context += entry;
179
+ currentTokens += entryTokens;
180
+ }
181
+ return context;
182
+ }
183
+ formatSearchResults(query, maxResults = 30) {
184
+ const results = this.search(query, Math.min(maxResults, 20));
185
+ if (results.length === 0) {
186
+ return 'No indexed codebase matches found. Run /index or wait for workspace indexing to finish.';
187
+ }
188
+ return results.map((result) => (`[indexed:${result.score}] ${result.relativePath}:${result.startLine}-${result.endLine}`
189
+ + (result.symbol ? ` (${result.symbol})` : '')
190
+ + `\n${result.content.split('\n').slice(0, 8).join('\n')}`)).join('\n\n');
191
+ }
192
+ findFiles(rootPath) {
193
+ const files = [];
194
+ const walk = (currentDir, depth) => {
195
+ if (depth > 14 || files.length >= 10000)
196
+ return;
197
+ let entries;
198
+ try {
199
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
200
+ }
201
+ catch {
202
+ return;
203
+ }
204
+ for (const entry of entries) {
205
+ if (this.shouldIgnore(entry.name, path.join(currentDir, entry.name)))
206
+ continue;
207
+ const fullPath = path.join(currentDir, entry.name);
208
+ if (entry.isDirectory()) {
209
+ walk(fullPath, depth + 1);
210
+ }
211
+ else if (entry.isFile()) {
212
+ const ext = path.extname(entry.name).toLowerCase();
213
+ if (this.supportedExtensions.has(ext)) {
214
+ files.push(fullPath);
215
+ }
216
+ }
217
+ }
218
+ };
219
+ walk(rootPath, 0);
220
+ return files;
221
+ }
222
+ indexFile(filePath) {
223
+ try {
224
+ const stat = fs.statSync(filePath);
225
+ if (!stat.isFile() || stat.size > this.maxFileSize || stat.size === 0)
226
+ return;
227
+ const content = fs.readFileSync(filePath, 'utf8');
228
+ const hash = crypto.createHash('md5').update(content).digest('hex');
229
+ if (this.fileHashes.get(filePath) === hash)
230
+ return;
231
+ this.fileHashes.set(filePath, hash);
232
+ const relativePath = path.relative(this.workspaceRoot, filePath);
233
+ const language = this.getLanguage(filePath);
234
+ const lines = content.split('\n');
235
+ const chunks = this.chunkFile(lines, filePath, relativePath, language);
236
+ const chunkIds = [];
237
+ for (const chunk of chunks) {
238
+ const chunkId = `${relativePath}:${chunk.startLine}-${chunk.endLine}`;
239
+ const terms = new Map();
240
+ const tokens = this.tokenize(chunk.content.toLowerCase());
241
+ let totalTerms = 0;
242
+ for (const token of tokens) {
243
+ terms.set(token, (terms.get(token) || 0) + 1);
244
+ totalTerms += 1;
245
+ if (!this.invertedIndex.has(token)) {
246
+ this.invertedIndex.set(token, new Set());
247
+ }
248
+ this.invertedIndex.get(token).add(chunkId);
249
+ }
250
+ this.chunkStore.set(chunkId, { ...chunk, terms, totalTerms });
251
+ chunkIds.push(chunkId);
252
+ this.totalChunks += 1;
253
+ }
254
+ this.index.set(filePath, chunkIds);
255
+ this.indexedFileCount += 1;
256
+ }
257
+ catch {
258
+ // skip unreadable files
259
+ }
260
+ }
261
+ chunkFile(lines, filePath, relativePath, language) {
262
+ const chunks = [];
263
+ for (let i = 0; i < lines.length; i += this.chunkSize - this.chunkOverlap) {
264
+ const endLine = Math.min(i + this.chunkSize, lines.length);
265
+ const chunkLines = lines.slice(i, endLine);
266
+ if (chunkLines.join('').trim().length === 0)
267
+ continue;
268
+ chunks.push({
269
+ filePath,
270
+ relativePath,
271
+ startLine: i + 1,
272
+ endLine,
273
+ content: chunkLines.join('\n'),
274
+ language,
275
+ symbol: null,
276
+ });
277
+ }
278
+ return chunks;
279
+ }
280
+ tokenize(text) {
281
+ return text
282
+ .replace(/[^a-zA-Z0-9_$]/g, ' ')
283
+ .split(/\s+/)
284
+ .filter((token) => token.length > 2)
285
+ .filter((token) => !this.isStopWord(token));
286
+ }
287
+ isStopWord(word) {
288
+ const stopWords = new Set([
289
+ 'the', 'and', 'for', 'that', 'this', 'with', 'from', 'are',
290
+ 'was', 'were', 'been', 'have', 'has', 'had', 'not', 'but',
291
+ 'its', 'can', 'will', 'would', 'could', 'should', 'may',
292
+ 'var', 'let', 'const', 'function', 'return', 'true', 'false',
293
+ 'null', 'undefined', 'new', 'class', 'import', 'export',
294
+ 'require', 'module', 'exports', 'default', 'async', 'await',
295
+ ]);
296
+ return stopWords.has(word);
297
+ }
298
+ getLanguage(filePath) {
299
+ const ext = path.extname(filePath).toLowerCase();
300
+ const map = {
301
+ '.js': 'javascript', '.jsx': 'javascript',
302
+ '.ts': 'typescript', '.tsx': 'typescript',
303
+ '.py': 'python', '.html': 'html', '.css': 'css', '.json': 'json',
304
+ '.md': 'markdown', '.sh': 'shell', '.go': 'go', '.rs': 'rust',
305
+ };
306
+ return map[ext] || 'text';
307
+ }
308
+ shouldIgnore(name, fullPath) {
309
+ if (name.startsWith('.') && name !== '.env')
310
+ return true;
311
+ const lowerPath = fullPath.toLowerCase();
312
+ return this.ignorePatterns.some((pattern) => lowerPath.includes(pattern));
313
+ }
314
+ }
@@ -10,6 +10,7 @@ export interface VigthoriaCLIConfig {
10
10
  refreshToken: string | null;
11
11
  userId: string | null;
12
12
  email: string | null;
13
+ v3ServiceKey: string | null;
13
14
  subscription: {
14
15
  plan: string | null;
15
16
  status: string | null;
@@ -27,6 +28,14 @@ export interface VigthoriaCLIConfig {
27
28
  rootPath: string | null;
28
29
  ignorePatterns: string[];
29
30
  };
31
+ persona: 'default' | 'wiener_grant';
32
+ hubModelPrefs?: {
33
+ enabledCloudModels: string[];
34
+ defaultCloudModel?: string;
35
+ defaultLocalModel?: string;
36
+ balance?: number;
37
+ fetchedAt?: string;
38
+ };
30
39
  }
31
40
  export interface AvailableModelDescriptor {
32
41
  id: string;
@@ -68,7 +77,9 @@ export declare class Config {
68
77
  expiresAt?: string;
69
78
  }): void;
70
79
  getAvailableModels(): AvailableModelDescriptor[];
80
+ refreshHubModelPreferences(): Promise<void>;
71
81
  isCloudModel(modelId: string): boolean;
82
+ filterModelsByHubPreferences(models: AvailableModelDescriptor[], enabledCloudModels?: string[]): AvailableModelDescriptor[];
72
83
  isComplexTask(prompt: string): boolean;
73
84
  shouldUseCloudForHeavyTask(prompt: string): boolean;
74
85
  }
@@ -12,19 +12,21 @@ const defaultConfig = {
12
12
  refreshToken: null,
13
13
  userId: null,
14
14
  email: null,
15
+ v3ServiceKey: null,
15
16
  subscription: {
16
17
  plan: null,
17
18
  status: null,
18
19
  expiresAt: null,
19
20
  },
20
21
  preferences: {
21
- defaultModel: 'agent',
22
+ defaultModel: 'code',
22
23
  theme: 'dark',
23
24
  autoApplyFixes: false,
24
25
  showDiffs: true,
25
26
  contextLines: 3,
26
27
  maxTokens: 4096,
27
28
  },
29
+ persona: 'default',
28
30
  project: {
29
31
  rootPath: null,
30
32
  ignorePatterns: [
@@ -63,6 +65,7 @@ export class Config {
63
65
  refreshToken: { type: ['string', 'null'] },
64
66
  userId: { type: ['string', 'null'] },
65
67
  email: { type: ['string', 'null'] },
68
+ v3ServiceKey: { type: ['string', 'null'] },
66
69
  subscription: {
67
70
  type: 'object',
68
71
  properties: {
@@ -82,6 +85,7 @@ export class Config {
82
85
  maxTokens: { type: 'number' },
83
86
  },
84
87
  },
88
+ persona: { type: 'string', enum: ['default', 'wiener_grant'] },
85
89
  project: {
86
90
  type: 'object',
87
91
  properties: {
@@ -182,30 +186,64 @@ export class Config {
182
186
  const sub = this.store.get('subscription');
183
187
  const plan = (sub.plan || '').toLowerCase();
184
188
  // ═══════════════════════════════════════════════════════════════
185
- // VIGTHORIA LOCAL - Self-hosted operational models
189
+ // Vigthoria server infrastructure operational models
186
190
  // ═══════════════════════════════════════════════════════════════
187
191
  const models = [
188
- { id: 'agent', name: 'Vigthoria V3 Code Agent', description: 'Blackwell autonomous agent workflow', tier: 'local', backendModel: 'vigthoria-v3-code-35b' },
189
- { id: 'code', name: 'Vigthoria v3 Code 35B', description: 'Native 35B coding model on Blackwell', tier: 'local', backendModel: 'vigthoria-v3-code-35b' },
192
+ { id: 'code', name: 'Vigthoria v3 Code 35B', description: 'Native 35B coding model on Blackwell (V3 pipeline)', tier: 'local', backendModel: 'vigthoria-v3-code-35b' },
190
193
  { id: 'code-35b', name: 'Vigthoria v3 Code 35B', description: 'Same flagship model as code', tier: 'local', backendModel: 'vigthoria-v3-code-35b' },
191
194
  { id: 'code-9b', name: 'Vigthoria v3 Code 9B', description: 'Efficient coding specialist for quick tasks', tier: 'local', backendModel: 'vigthoria-v3-code-9b' },
192
195
  { id: 'balanced', name: 'Vigthoria Balanced 4B', description: 'Balanced general-purpose local model', tier: 'local', backendModel: 'vigthoria-v3-balanced-4b' },
193
196
  { id: 'balanced-4b', name: 'Vigthoria Balanced 4B', description: 'Efficient 4B general-purpose model (Qwen3.5-4B based)', tier: 'local', backendModel: 'vigthoria-v3-balanced-4b' },
194
197
  ];
195
- // ═══════════════════════════════════════════════════════════════
196
- // VIGTHORIA CLOUD - Premium cloud models (Pro subscription)
197
- // For complex multi-file tasks, large refactoring, architecture
198
- // ═══════════════════════════════════════════════════════════════
199
198
  if (this.hasCloudAccess()) {
200
- models.push({ id: 'cloud', name: 'Vigthoria Cloud Pro', description: 'High-capability cloud model for complex tasks', tier: 'cloud', backendModel: 'vigthoria-cloud-pro' }, { id: 'cloud-reason', name: 'Vigthoria Cloud K2', description: 'Reasoning-focused cloud model', tier: 'cloud', backendModel: 'vigthoria-cloud-k2' }, { id: 'ultra', name: 'Vigthoria Cloud Ultra', description: 'Maximum capability cloud routing', tier: 'cloud', backendModel: 'vigthoria-cloud-ultra' });
199
+ models.push({ id: 'cloud-fast', name: 'Vigthoria Cloud Fast', description: 'Fast cloud responses for lighter work', tier: 'cloud', backendModel: 'vigthoria-cloud-fast' }, { id: 'cloud-balanced', name: 'Vigthoria Cloud Balanced', description: 'Default quality/cost balance for chat and coding', tier: 'cloud', backendModel: 'vigthoria-cloud-balanced' }, { id: 'cloud-code', name: 'Vigthoria Cloud Code', description: 'Economical cloud coding and completion', tier: 'cloud', backendModel: 'vigthoria-cloud-code' }, { id: 'cloud-power', name: 'Vigthoria Cloud Power', description: 'Premium general intelligence for demanding work', tier: 'cloud', backendModel: 'vigthoria-cloud-power' }, { id: 'cloud-maximum', name: 'Vigthoria Cloud Maximum', description: 'Maximum power for complex architecture and reviews', tier: 'cloud', backendModel: 'vigthoria-cloud-maximum' },
200
+ // Legacy aliases
201
+ { id: 'cloud', name: 'Vigthoria Cloud Balanced', description: 'Legacy alias for cloud-balanced', tier: 'cloud', backendModel: 'vigthoria-cloud-balanced' }, { id: 'cloud-reason', name: 'Vigthoria Cloud Balanced', description: 'Legacy alias for cloud-balanced', tier: 'cloud', backendModel: 'vigthoria-cloud-balanced' }, { id: 'ultra', name: 'Vigthoria Cloud Maximum', description: 'Legacy alias for cloud-maximum', tier: 'cloud', backendModel: 'vigthoria-cloud-maximum' });
202
+ }
203
+ const hubPrefs = this.store.get('hubModelPrefs');
204
+ if (hubPrefs?.enabledCloudModels?.length) {
205
+ return this.filterModelsByHubPreferences(models, hubPrefs.enabledCloudModels);
201
206
  }
202
207
  return models;
203
208
  }
209
+ async refreshHubModelPreferences() {
210
+ const token = this.store.get('authToken');
211
+ const apiUrl = String(this.store.get('apiUrl') || '').replace(/\/$/, '');
212
+ if (!token || !apiUrl)
213
+ return;
214
+ try {
215
+ const response = await fetch(`${apiUrl}/api/user/credits/model-preferences`, {
216
+ headers: { Authorization: `Bearer ${token}` },
217
+ signal: AbortSignal.timeout(8000),
218
+ });
219
+ if (!response.ok)
220
+ return;
221
+ const prefs = await response.json();
222
+ if (prefs?.success === false)
223
+ return;
224
+ this.store.set('hubModelPrefs', {
225
+ enabledCloudModels: Array.isArray(prefs.enabledCloudModels) ? prefs.enabledCloudModels : [],
226
+ defaultCloudModel: typeof prefs.defaultCloudModel === 'string' ? prefs.defaultCloudModel : undefined,
227
+ defaultLocalModel: typeof prefs.defaultLocalModel === 'string' ? prefs.defaultLocalModel : undefined,
228
+ balance: typeof prefs.balance === 'number' ? prefs.balance : undefined,
229
+ fetchedAt: new Date().toISOString(),
230
+ });
231
+ }
232
+ catch {
233
+ // Non-fatal: CLI can still run with default model catalog.
234
+ }
235
+ }
204
236
  // Check if a model is a "Cloud" tier model
205
237
  isCloudModel(modelId) {
206
- const cloudModels = ['cloud', 'cloud-reason', 'ultra'];
238
+ const cloudModels = ['cloud-fast', 'cloud-balanced', 'cloud-code', 'cloud-power', 'cloud-maximum', 'cloud', 'cloud-reason', 'ultra'];
207
239
  return cloudModels.includes(modelId) || modelId.includes('cloud');
208
240
  }
241
+ filterModelsByHubPreferences(models, enabledCloudModels) {
242
+ if (!enabledCloudModels || !enabledCloudModels.length)
243
+ return models;
244
+ const enabled = new Set(enabledCloudModels);
245
+ return models.filter((model) => model.tier !== 'cloud' || enabled.has(model.backendModel));
246
+ }
209
247
  // Check if task is complex enough to suggest Cloud upgrade
210
248
  isComplexTask(prompt) {
211
249
  const complexIndicators = [
@@ -221,6 +259,9 @@ export class Config {
221
259
  return complexIndicators.some(pattern => pattern.test(prompt));
222
260
  }
223
261
  shouldUseCloudForHeavyTask(prompt) {
224
- return this.hasOperatorAccess() && this.hasCloudAccess() && this.isComplexTask(prompt);
262
+ // DISABLED: Cloud routing is now EXPLICIT ONLY (--model cloud or /cloud command)
263
+ // Do NOT auto-route based on keywords. User must opt-in.
264
+ // If user wants cloud: they'll use --model cloud or request it explicitly.
265
+ return false;
225
266
  }
226
267
  }
@@ -0,0 +1,4 @@
1
+ export type PersonaMode = 'default' | 'wiener_grant';
2
+ export declare function normalizePersonaMode(value: unknown): PersonaMode | null;
3
+ export declare function isToneDownPrompt(prompt: string): boolean;
4
+ export declare function buildPersonaOverlay(mode: PersonaMode, prompt?: string): string;
@@ -0,0 +1,34 @@
1
+ const WIENER_GRANT_PROMPT = [
2
+ 'Optional persona overlay: Wiener Grantler mode.',
3
+ 'You remain Vigthoria: technically precise, helpful, safe, and comprehensive.',
4
+ 'Persona: theatrical Viennese grumpiness with dry humor, mild sarcasm, impatient charm, and light Austrian dialect flavor.',
5
+ 'Rules:',
6
+ '- Never reduce technical quality, completeness, or safety.',
7
+ '- Never use slurs, hate, racist or sexist stereotypes, protected-class insults, or demeaning nationality/ethnicity jokes.',
8
+ '- Do not seriously demean the user. Keep the bite playful and aimed at messy code, vague prompts, broken configs, or tooling chaos.',
9
+ '- Use dialect flavor sparingly enough that instructions, commands, and code remain fully readable.',
10
+ '- Always provide the actual fix, commands, code, or next steps.',
11
+ ].join('\n');
12
+ const TONE_DOWN_PROMPT = [
13
+ 'Tone-down rule active: this request appears safety-sensitive, destructive, auth/billing-related, security-related, legal/medical/financial, or production-critical.',
14
+ 'Use only a very light flavor line if appropriate, then prioritize plain clarity, caution, and exact steps.',
15
+ ].join('\n');
16
+ export function normalizePersonaMode(value) {
17
+ const normalized = String(value || '').trim().toLowerCase().replace(/_/g, '-');
18
+ if (!normalized || normalized === 'default' || normalized === 'off' || normalized === 'none')
19
+ return 'default';
20
+ if (normalized === 'wiener-grant' || normalized === 'wiener-grantler' || normalized === 'grant' || normalized === 'grantler')
21
+ return 'wiener_grant';
22
+ return null;
23
+ }
24
+ export function isToneDownPrompt(prompt) {
25
+ return /\b(delete|drop|destroy|wipe|rm\s+-rf|format|credential|secret|token|auth|login|billing|payment|wallet|security|vulnerability|exploit|incident|production|prod|deploy|legal|medical|financial|bank|tax)\b/i.test(prompt);
26
+ }
27
+ export function buildPersonaOverlay(mode, prompt = '') {
28
+ if (mode !== 'wiener_grant')
29
+ return '';
30
+ const parts = [WIENER_GRANT_PROMPT];
31
+ if (isToneDownPrompt(prompt))
32
+ parts.push(TONE_DOWN_PROMPT);
33
+ return parts.join('\n\n');
34
+ }