@nandansai08/personal-ai 0.8.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/.env.example +62 -0
- package/LICENSE +21 -0
- package/README.md +431 -0
- package/bin/personal-ai.js +4 -0
- package/config/mcp.json +3 -0
- package/config/models.yaml +23 -0
- package/config/persona.yaml +24 -0
- package/config/profiles.yaml +61 -0
- package/config/providers.yaml +22 -0
- package/dist/bootstrap.js +41 -0
- package/dist/core/assistant.js +170 -0
- package/dist/core/context.js +35 -0
- package/dist/core/events.js +45 -0
- package/dist/core/logger.js +67 -0
- package/dist/core/model-manager.js +101 -0
- package/dist/index.js +98 -0
- package/dist/mcp/client.js +3 -0
- package/dist/mcp/loader.js +3 -0
- package/dist/memory/embeddings.js +53 -0
- package/dist/memory/intent.js +113 -0
- package/dist/memory/long-term.js +312 -0
- package/dist/memory/short-term.js +63 -0
- package/dist/memory/types.js +5 -0
- package/dist/memory/vector-store.js +57 -0
- package/dist/persona/loader.js +56 -0
- package/dist/persona/profiles.js +51 -0
- package/dist/persona/system-prompt.js +99 -0
- package/dist/persona/types.js +22 -0
- package/dist/plugins/interface.js +1 -0
- package/dist/plugins/loader.js +3 -0
- package/dist/providers/anthropic.js +112 -0
- package/dist/providers/factory.js +40 -0
- package/dist/providers/gemini.js +86 -0
- package/dist/providers/groq.js +14 -0
- package/dist/providers/interface.js +2 -0
- package/dist/providers/lmstudio.js +13 -0
- package/dist/providers/metadata.js +96 -0
- package/dist/providers/mistral.js +133 -0
- package/dist/providers/ollama.js +265 -0
- package/dist/providers/openai-compatible.js +110 -0
- package/dist/providers/openai.js +14 -0
- package/dist/providers/together.js +14 -0
- package/dist/providers/utils.js +57 -0
- package/dist/tools/calculator.js +44 -0
- package/dist/tools/file-reader.js +101 -0
- package/dist/tools/memory-tool.js +58 -0
- package/dist/tools/notes.js +121 -0
- package/dist/tools/parser.js +119 -0
- package/dist/tools/registry.js +88 -0
- package/dist/tools/tasks.js +134 -0
- package/dist/tools/types.js +3 -0
- package/dist/tools/web-search.js +108 -0
- package/dist/ui/cli-helpers.js +153 -0
- package/dist/ui/cli.js +647 -0
- package/dist/ui/setup.js +196 -0
- package/dist/ui/web/client/index.html +2081 -0
- package/dist/ui/web/server.js +310 -0
- package/dist/voice/stt.js +3 -0
- package/dist/voice/tts.js +3 -0
- package/dist/web.js +63 -0
- package/package.json +68 -0
package/dist/ui/setup.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// MIT License — personal-ai
|
|
2
|
+
import readline from 'node:readline';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const ROOT = path.join(__dirname, '..', '..');
|
|
9
|
+
import { PROVIDER_META } from '../providers/metadata.js';
|
|
10
|
+
const PROVIDERS = Object.values(PROVIDER_META);
|
|
11
|
+
function ask(rl, q) {
|
|
12
|
+
return new Promise(resolve => rl.question(q, resolve));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Ask for a secret without echoing it — each typed character renders as `*`.
|
|
16
|
+
* Works by muting readline's output while the question is pending.
|
|
17
|
+
*/
|
|
18
|
+
function askHidden(rl, q) {
|
|
19
|
+
// readline.Interface exposes _writeToOutput internally; mask while pending
|
|
20
|
+
const iface = rl;
|
|
21
|
+
const original = iface._writeToOutput;
|
|
22
|
+
process.stdout.write(q);
|
|
23
|
+
let muted = true;
|
|
24
|
+
iface._writeToOutput = (s) => {
|
|
25
|
+
if (!muted) {
|
|
26
|
+
original?.call(iface, s);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// Echo newlines normally, mask everything else
|
|
30
|
+
if (s.includes('\n') || s.includes('\r')) {
|
|
31
|
+
process.stdout.write('\n');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
process.stdout.write('*');
|
|
35
|
+
};
|
|
36
|
+
return new Promise(resolve => {
|
|
37
|
+
rl.question('', answer => {
|
|
38
|
+
muted = false;
|
|
39
|
+
if (original)
|
|
40
|
+
iface._writeToOutput = original;
|
|
41
|
+
resolve(answer);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
async function checkOllama(baseUrl) {
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(`${baseUrl}/api/tags`, { signal: AbortSignal.timeout(3000) });
|
|
48
|
+
if (!res.ok)
|
|
49
|
+
return [];
|
|
50
|
+
const data = await res.json();
|
|
51
|
+
return (data.models ?? []).map(m => m.name);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// fallow-ignore-next-line complexity
|
|
58
|
+
async function testKey(p, key) {
|
|
59
|
+
if (!p.testUrl)
|
|
60
|
+
return true;
|
|
61
|
+
try {
|
|
62
|
+
const headers = p.testAuthHeader ? p.testAuthHeader(key) : {};
|
|
63
|
+
const res = await fetch(p.testUrl, { headers, signal: AbortSignal.timeout(5000) });
|
|
64
|
+
return res.ok;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Returns true if setup wizard should run (no .env or PROVIDER unset). */
|
|
71
|
+
export function needsSetup(envPath) {
|
|
72
|
+
if (!fs.existsSync(envPath))
|
|
73
|
+
return true;
|
|
74
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
75
|
+
return !content.match(/^\s*PROVIDER\s*=/m);
|
|
76
|
+
}
|
|
77
|
+
async function step1PickProvider(rl) {
|
|
78
|
+
console.log(chalk.bold(' Step 1 of 3 — Choose a provider\n'));
|
|
79
|
+
// fallow-ignore-next-line complexity
|
|
80
|
+
PROVIDERS.forEach((p, i) => {
|
|
81
|
+
const tags = [];
|
|
82
|
+
if (p.free)
|
|
83
|
+
tags.push(chalk.green('FREE'));
|
|
84
|
+
if (p.local)
|
|
85
|
+
tags.push(chalk.cyan('LOCAL'));
|
|
86
|
+
const tagStr = tags.length ? ' ' + tags.join(' ') : '';
|
|
87
|
+
console.log(` ${chalk.bold(`${i + 1}.`)} ${p.label}${tagStr}`);
|
|
88
|
+
console.log(` ${chalk.dim(p.hint)}`);
|
|
89
|
+
if (p.signupUrl)
|
|
90
|
+
console.log(` ${chalk.dim(p.signupUrl)}`);
|
|
91
|
+
console.log();
|
|
92
|
+
});
|
|
93
|
+
let prov;
|
|
94
|
+
while (!prov) {
|
|
95
|
+
const raw = (await ask(rl, chalk.cyan(' Enter number (1-8): '))).trim();
|
|
96
|
+
const idx = parseInt(raw, 10) - 1;
|
|
97
|
+
if (idx >= 0 && idx < PROVIDERS.length)
|
|
98
|
+
prov = PROVIDERS[idx];
|
|
99
|
+
else
|
|
100
|
+
console.log(chalk.yellow(' Please enter 1-8\n'));
|
|
101
|
+
}
|
|
102
|
+
console.log(chalk.green(`\n ✓ ${prov.label}\n`));
|
|
103
|
+
return prov;
|
|
104
|
+
}
|
|
105
|
+
// fallow-ignore-next-line complexity
|
|
106
|
+
async function configOllama(rl) {
|
|
107
|
+
console.log(chalk.bold(' Step 2 of 3 — Ollama config\n'));
|
|
108
|
+
const baseUrl = (await ask(rl, chalk.cyan(' Ollama URL [http://localhost:11434]: '))).trim() || 'http://localhost:11434';
|
|
109
|
+
const lines = [`OLLAMA_BASE_URL=${baseUrl}`];
|
|
110
|
+
process.stdout.write(chalk.dim(' Checking Ollama... '));
|
|
111
|
+
const models = await checkOllama(baseUrl);
|
|
112
|
+
if (!models.length) {
|
|
113
|
+
console.log(chalk.yellow('not running or no models'));
|
|
114
|
+
console.log(chalk.dim(' Start: ollama serve'));
|
|
115
|
+
console.log(chalk.dim(' Pull: ollama pull qwen2.5:14b && ollama pull gemma3:12b\n'));
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
console.log(chalk.green(`${models.length} model(s) found`));
|
|
119
|
+
console.log(chalk.dim(` ${models.slice(0, 6).join(' ')}${models.length > 6 ? ` …+${models.length - 6}` : ''}\n`));
|
|
120
|
+
}
|
|
121
|
+
const defMain = models.find(m => m.startsWith('qwen2.5')) || models[0] || 'qwen2.5:14b';
|
|
122
|
+
const defChat = models.find(m => m.startsWith('gemma3')) || models[1] || 'gemma3:12b';
|
|
123
|
+
const mainModel = (await ask(rl, chalk.cyan(` Primary model [${defMain}]: `))).trim() || defMain;
|
|
124
|
+
const chatModel = (await ask(rl, chalk.cyan(` Chat model [${defChat}]: `))).trim() || defChat;
|
|
125
|
+
lines.push(`OLLAMA_MODEL=${mainModel}`, `OLLAMA_CHAT_MODEL=${chatModel}`, 'OLLAMA_NUM_CTX=8192', 'OLLAMA_NUM_PREDICT=512', 'OLLAMA_TEMPERATURE=0.7');
|
|
126
|
+
return lines;
|
|
127
|
+
}
|
|
128
|
+
async function configLMStudio(rl) {
|
|
129
|
+
console.log(chalk.bold(' Step 2 of 3 — LM Studio config\n'));
|
|
130
|
+
const baseUrl = (await ask(rl, chalk.cyan(' LM Studio URL [http://localhost:1234/v1]: '))).trim() || 'http://localhost:1234/v1';
|
|
131
|
+
const model = (await ask(rl, chalk.cyan(' Model name [local-model]: '))).trim() || 'local-model';
|
|
132
|
+
return [`LMSTUDIO_BASE_URL=${baseUrl}`, `LMSTUDIO_MODEL=${model}`];
|
|
133
|
+
}
|
|
134
|
+
// fallow-ignore-next-line complexity
|
|
135
|
+
async function configApiKey(rl, prov) {
|
|
136
|
+
console.log(chalk.bold(` Step 2 of 3 — ${prov.label} config\n`));
|
|
137
|
+
let apiKey = '';
|
|
138
|
+
while (!apiKey) {
|
|
139
|
+
apiKey = (await askHidden(rl, chalk.cyan(` ${prov.envKey} (input hidden): `))).trim();
|
|
140
|
+
if (!apiKey)
|
|
141
|
+
console.log(chalk.yellow(' Key required\n'));
|
|
142
|
+
}
|
|
143
|
+
process.stdout.write(chalk.dim(' Testing key... '));
|
|
144
|
+
const ok = await testKey(prov, apiKey);
|
|
145
|
+
console.log(ok ? chalk.green('valid ✓') : chalk.yellow('could not verify (may still work)'));
|
|
146
|
+
console.log();
|
|
147
|
+
const model = (await ask(rl, chalk.cyan(` Model [${prov.defaultModel}]: `))).trim() || prov.defaultModel;
|
|
148
|
+
return [`${prov.envKey}=${apiKey}`, `${prov.modelEnvKey}=${model}`];
|
|
149
|
+
}
|
|
150
|
+
async function step2ProviderConfig(rl, prov) {
|
|
151
|
+
const base = [`PROVIDER=${prov.key}`];
|
|
152
|
+
if (prov.key === 'ollama')
|
|
153
|
+
return [...base, ...await configOllama(rl)];
|
|
154
|
+
if (prov.key === 'lmstudio')
|
|
155
|
+
return [...base, ...await configLMStudio(rl)];
|
|
156
|
+
if (prov.envKey)
|
|
157
|
+
return [...base, ...await configApiKey(rl, prov)];
|
|
158
|
+
return base;
|
|
159
|
+
}
|
|
160
|
+
async function step3Persona(rl) {
|
|
161
|
+
console.log(chalk.bold('\n Step 3 of 3 — Persona\n'));
|
|
162
|
+
const userName = (await ask(rl, chalk.cyan(' Your name [User]: '))).trim() || 'User';
|
|
163
|
+
const aiName = (await ask(rl, chalk.cyan(' Assistant name [Aria]: '))).trim() || 'Aria';
|
|
164
|
+
const personaPath = path.join(ROOT, 'config', 'persona.yaml');
|
|
165
|
+
if (fs.existsSync(personaPath)) {
|
|
166
|
+
let yaml = fs.readFileSync(personaPath, 'utf8');
|
|
167
|
+
yaml = yaml.replace(/^name:\s*.*$/m, `name: "${aiName}"`);
|
|
168
|
+
yaml = yaml.replace(/^user_name:\s*.*$/m, `user_name: "${userName}"`);
|
|
169
|
+
fs.writeFileSync(personaPath, yaml);
|
|
170
|
+
console.log(chalk.dim(' Updated config/persona.yaml'));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
export async function runSetupWizard(envPath) {
|
|
174
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
175
|
+
console.clear();
|
|
176
|
+
console.log(chalk.cyan([
|
|
177
|
+
'', ' ╔══════════════════════════════════════════╗',
|
|
178
|
+
` ║ ${chalk.bold('PersonalAI')} — First-Run Setup ║`,
|
|
179
|
+
` ║ ${chalk.dim('Local-first. Any model. Any provider.')} ║`,
|
|
180
|
+
' ╚══════════════════════════════════════════╝', '',
|
|
181
|
+
].join('\n')));
|
|
182
|
+
const prov = await step1PickProvider(rl);
|
|
183
|
+
const envLines = await step2ProviderConfig(rl, prov);
|
|
184
|
+
await step3Persona(rl);
|
|
185
|
+
fs.writeFileSync(envPath, ['# PersonalAI — generated by setup wizard', ...envLines, '', '# LOG_LEVEL=info'].join('\n') + '\n');
|
|
186
|
+
console.log(chalk.green(`\n ✓ Wrote .env`));
|
|
187
|
+
console.log(chalk.cyan([
|
|
188
|
+
'', ' ╔══════════════════════════════════════════╗',
|
|
189
|
+
` ║ ${chalk.bold.green('Setup complete!')} ║`,
|
|
190
|
+
' ║ ║',
|
|
191
|
+
` ║ CLI: ${chalk.bold('npm start')} ║`,
|
|
192
|
+
` ║ Web: ${chalk.bold('npm run web')} ║`,
|
|
193
|
+
' ╚══════════════════════════════════════════╝', '',
|
|
194
|
+
].join('\n')));
|
|
195
|
+
rl.close();
|
|
196
|
+
}
|