Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,32 @@ Minara: Bitcoin is currently trading at $95,432...
exit Quit the chat
```

### Local Models (DMind / Hugging Face)

| Command | Description |
| ------------------------ | ---------------------------------------------------------- |
| `minara private` | Open interactive local-model menu |
| `minara private install` | Download a DMind model from Hugging Face |
| `minara private models` | List available/installed local models |
| `minara private load` | Load selected model into vLLM server |
| `minara private chat` | Chat with local model |
| `minara private check` | Check whether installed local models have newer HF revision |
| `minara private update` | Explicitly update an installed local model |
| `minara private unload` | Stop local vLLM server |
| `minara private status` | Show local model server status |

```bash
minara private # Interactive menu
minara private install # Install local model from Hugging Face
minara private check # Check model revisions (local vs remote)
minara private update # Explicitly update selected installed model
minara private load # Start local model server
minara private chat # Chat locally
minara private unload # Stop local model server
```

> **Update behavior:** `load` / `chat` only show an update notice when a newer model revision exists. Models are updated only when you explicitly run `minara private update`.

### Market Discovery

| Command | Description |
Expand Down Expand Up @@ -361,4 +387,4 @@ npm run test:coverage # With coverage report

## License

[MIT](LICENSE)
[MIT](LICENSE)
9 changes: 9 additions & 0 deletions src/commands/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ async function showSpotAssets(token: string): Promise<void> {
let totalUnrealizedPnl = 0;
let hasUnrealizedPnl = false;

const STABLECOINS = new Set(['USDC', 'USDT']);
let stablecoinBalance = 0;

for (const t of all) {
const bal = Number(t.balance ?? 0);
const price = Number(t.marketPrice ?? 0);
Expand All @@ -53,6 +56,11 @@ async function showSpotAssets(token: string): Promise<void> {
hasUnrealizedPnl = true;
}

const sym = String(t.tokenSymbol ?? '').toUpperCase();
if (STABLECOINS.has(sym)) {
stablecoinBalance += bal;
}

if (bal > 0 && value >= MIN_DISPLAY_VALUE) {
holdings.push({ ...t, _value: value });
}
Expand All @@ -67,6 +75,7 @@ async function showSpotAssets(token: string): Promise<void> {

console.log('');
console.log(chalk.bold('Spot Wallet:'));
console.log(` Balance (USDC+USDT) : ${fmt(stablecoinBalance)}`);
console.log(` Portfolio Value : ${fmt(totalValue)}`);
console.log(` Unrealized PnL : ${pnlFmt(totalUnrealizedPnl)}`);
console.log(` Realized PnL : ${pnlFmt(totalRealizedPnl)}`);
Expand Down
10 changes: 10 additions & 0 deletions src/commands/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,13 @@ export const chatCommand = new Command('chat')
console.log('');

const rl = createInterface({ input: process.stdin, output: process.stdout });
let exiting = false;

const handleSigint = () => {
if (exiting) return;
exiting = true;
rl.close();
};

async function sendAndPrintWithPause(msg: string) {
rl.pause();
Expand All @@ -205,7 +212,10 @@ export const chatCommand = new Command('chat')
const ask = (): Promise<string> =>
new Promise((resolve) => rl.question(chalk.blue.bold('>>> '), resolve));

rl.on('SIGINT', handleSigint);
process.on('SIGINT', handleSigint);
rl.on('close', () => {
process.off('SIGINT', handleSigint);
console.log(chalk.dim('\nGoodbye!'));
process.exit(0);
});
Expand Down
166 changes: 166 additions & 0 deletions src/commands/install.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { select, confirm } from '@inquirer/prompts';
import chalk from 'chalk';
import {
AVAILABLE_MODELS, getInstalledIds, getModelDef,
markInstalled, markUninstalled,
findPython, hasVllm, hasHfHub,
pipInstall, downloadModel, clearModelCache,
isAppleSilicon, getArchLabel, fixNativeDeps,
} from '../local-models.js';
import { error, info, success, warn, spinner } from '../utils.js';

// ─── List ───────────────────────────────────────────────────────────────────

export function listModels(): void {
const installed = getInstalledIds();
console.log('');
console.log(chalk.bold(' DMind Models'));
console.log(chalk.dim(' ─'.repeat(24)));
console.log('');
for (const m of AVAILABLE_MODELS) {
const status = installed.includes(m.id)
? chalk.green(' [installed]')
: '';
const rec = m.recommended ? chalk.yellow(' ★ recommended') : '';
console.log(` ${chalk.bold(m.name)} ${chalk.dim(`(${m.params})`)}${rec}${status}`);
console.log(chalk.dim(` https://huggingface.co/${m.hfRepo}`));
console.log('');
}
}

// ─── Install ────────────────────────────────────────────────────────────────

export async function installFlow(): Promise<void> {
const py = ensurePython();
if (!py) return;
if (!(await ensureDeps(py))) return;

const installed = getInstalledIds();
const candidates = AVAILABLE_MODELS.filter((m) => !installed.includes(m.id));
if (candidates.length === 0) {
info('All models are already installed.');
return;
}

const defaultModel = candidates.find((m) => m.recommended) ?? candidates[0];
const model = await select({
message: 'Select a model to install:',
choices: candidates.map((m) => ({
name: `${m.name} ${chalk.dim(`(${m.params})`)}${m.recommended ? chalk.yellow(' ★ recommended') : ''}`,
value: m,
})),
default: defaultModel,
});

console.log('');
info(`Downloading ${chalk.bold(model.name)} from Hugging Face…`);
console.log(chalk.dim(` https://huggingface.co/${model.hfRepo}`));
console.log('');

if (!downloadModel(py, model.hfRepo)) {
error('Download failed. Check your network connection and try again.');
return;
}

markInstalled(model.id);
console.log('');
success(`${chalk.bold(model.name)} installed successfully!`);
info(`Start a private chat: ${chalk.cyan('minara private chat')}`);
}

// ─── Uninstall ──────────────────────────────────────────────────────────────

export async function uninstallFlow(): Promise<void> {
const installed = getInstalledIds();
if (installed.length === 0) {
info('No models installed.');
return;
}

const choices = installed.map((id) => {
const def = getModelDef(id);
return { name: def ? `${def.name} (${def.params})` : id, value: id };
});

const modelId = await select({ message: 'Select model to uninstall:', choices });
const def = getModelDef(modelId);
const ok = await confirm({
message: `Uninstall ${def?.name ?? modelId}?`,
default: false,
});
if (!ok) return;

markUninstalled(modelId);

const py = findPython();
if (py && def) {
const spin = spinner('Removing cached model files…');
const cleared = clearModelCache(py, def.hfRepo);
spin.stop();
if (cleared) {
success(`${def.name} uninstalled and cache cleared.`);
} else {
success(`${def.name} uninstalled.`);
warn('Could not clear HuggingFace cache automatically.');
console.log(chalk.dim(' Run `huggingface-cli delete-cache` to free disk space.'));
}
} else {
success(`${def?.name ?? modelId} uninstalled.`);
}
}

// ─── Prerequisite helpers ───────────────────────────────────────────────────

function ensurePython(): string | null {
const py = findPython();
if (!py) {
error('Python 3 is required. Please install Python 3.8+ first.');
console.log(chalk.dim(' https://www.python.org/downloads/'));
return null;
}
info(`Python found · ${chalk.dim(getArchLabel())}`);
return py;
}

async function ensureDeps(py: string): Promise<boolean> {
if (!hasVllm(py)) {
warn('vLLM is not installed.');
const ok = await confirm({ message: 'Install vLLM now? (pip install vllm)', default: true });
if (!ok) {
info('Skipped. Install manually: pip install vllm');
return false;
}
if (!pipInstall(py, 'vllm')) {
error('Failed to install vLLM. Try manually: pip install vllm');
return false;
}
success('vLLM installed');
}

if (!hasHfHub(py)) {
warn('huggingface_hub is not installed.');
const ok = await confirm({ message: 'Install huggingface_hub now?', default: true });
if (!ok) {
info('Skipped. Install manually: pip install huggingface_hub');
return false;
}
if (!pipInstall(py, 'huggingface_hub')) {
error('Failed to install. Try: pip install huggingface_hub');
return false;
}
success('huggingface_hub installed');
}

// On Apple Silicon, scan all native extensions and fix x86_64 mismatches
if (isAppleSilicon()) {
info('Scanning native extensions for arm64 compatibility…');
const fixed = fixNativeDeps(py);
if (fixed.length > 0) {
success(`Fixed ${fixed.length} package(s) for arm64: ${chalk.dim(fixed.join(', '))}`);
} else {
success('All native extensions are arm64 compatible');
}
}

return true;
}
Loading