@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.
Files changed (61) hide show
  1. package/.env.example +62 -0
  2. package/LICENSE +21 -0
  3. package/README.md +431 -0
  4. package/bin/personal-ai.js +4 -0
  5. package/config/mcp.json +3 -0
  6. package/config/models.yaml +23 -0
  7. package/config/persona.yaml +24 -0
  8. package/config/profiles.yaml +61 -0
  9. package/config/providers.yaml +22 -0
  10. package/dist/bootstrap.js +41 -0
  11. package/dist/core/assistant.js +170 -0
  12. package/dist/core/context.js +35 -0
  13. package/dist/core/events.js +45 -0
  14. package/dist/core/logger.js +67 -0
  15. package/dist/core/model-manager.js +101 -0
  16. package/dist/index.js +98 -0
  17. package/dist/mcp/client.js +3 -0
  18. package/dist/mcp/loader.js +3 -0
  19. package/dist/memory/embeddings.js +53 -0
  20. package/dist/memory/intent.js +113 -0
  21. package/dist/memory/long-term.js +312 -0
  22. package/dist/memory/short-term.js +63 -0
  23. package/dist/memory/types.js +5 -0
  24. package/dist/memory/vector-store.js +57 -0
  25. package/dist/persona/loader.js +56 -0
  26. package/dist/persona/profiles.js +51 -0
  27. package/dist/persona/system-prompt.js +99 -0
  28. package/dist/persona/types.js +22 -0
  29. package/dist/plugins/interface.js +1 -0
  30. package/dist/plugins/loader.js +3 -0
  31. package/dist/providers/anthropic.js +112 -0
  32. package/dist/providers/factory.js +40 -0
  33. package/dist/providers/gemini.js +86 -0
  34. package/dist/providers/groq.js +14 -0
  35. package/dist/providers/interface.js +2 -0
  36. package/dist/providers/lmstudio.js +13 -0
  37. package/dist/providers/metadata.js +96 -0
  38. package/dist/providers/mistral.js +133 -0
  39. package/dist/providers/ollama.js +265 -0
  40. package/dist/providers/openai-compatible.js +110 -0
  41. package/dist/providers/openai.js +14 -0
  42. package/dist/providers/together.js +14 -0
  43. package/dist/providers/utils.js +57 -0
  44. package/dist/tools/calculator.js +44 -0
  45. package/dist/tools/file-reader.js +101 -0
  46. package/dist/tools/memory-tool.js +58 -0
  47. package/dist/tools/notes.js +121 -0
  48. package/dist/tools/parser.js +119 -0
  49. package/dist/tools/registry.js +88 -0
  50. package/dist/tools/tasks.js +134 -0
  51. package/dist/tools/types.js +3 -0
  52. package/dist/tools/web-search.js +108 -0
  53. package/dist/ui/cli-helpers.js +153 -0
  54. package/dist/ui/cli.js +647 -0
  55. package/dist/ui/setup.js +196 -0
  56. package/dist/ui/web/client/index.html +2081 -0
  57. package/dist/ui/web/server.js +310 -0
  58. package/dist/voice/stt.js +3 -0
  59. package/dist/voice/tts.js +3 -0
  60. package/dist/web.js +63 -0
  61. package/package.json +68 -0
@@ -0,0 +1,153 @@
1
+ // MIT License — personal-ai
2
+ // Pure helpers for the CLI — no readline, no provider, no engine dependencies.
3
+ import fs from 'node:fs';
4
+ import chalk from 'chalk';
5
+ // Tool names whose XML blocks should be stripped from display if output as raw text
6
+ const TOOL_XML_NAMES = ['memory', 'web_search', 'notes', 'tasks', 'calculator', 'file_reader', 'tool'];
7
+ const TOOL_OPEN_RE = new RegExp(`<(${TOOL_XML_NAMES.join('|')})>`, 'i');
8
+ /** Streaming filter: buffers and strips XML tool-call blocks from displayed text. */
9
+ export function makeToolXmlStripper() {
10
+ let inTag = null;
11
+ let buf = '';
12
+ function feed(text) {
13
+ buf += text;
14
+ let out = '';
15
+ while (true) {
16
+ if (!inTag) {
17
+ const match = TOOL_OPEN_RE.exec(buf);
18
+ if (!match) {
19
+ // Guard last 20 chars against partial opening tag
20
+ const safe = buf.length > 20 ? buf.length - 20 : 0;
21
+ out += buf.slice(0, safe);
22
+ buf = buf.slice(safe);
23
+ break;
24
+ }
25
+ out += buf.slice(0, match.index);
26
+ inTag = (match[1] ?? '').toLowerCase();
27
+ buf = buf.slice(match.index + match[0].length);
28
+ }
29
+ else {
30
+ const closeOwn = `</${inTag}>`;
31
+ const closeArgs = '</args>';
32
+ const iOwn = buf.indexOf(closeOwn);
33
+ const iArgs = buf.indexOf(closeArgs);
34
+ // Pick whichever closing tag comes first
35
+ let end = -1;
36
+ let len = 0;
37
+ if (iOwn !== -1 && (iArgs === -1 || iOwn <= iArgs)) {
38
+ end = iOwn;
39
+ len = closeOwn.length;
40
+ }
41
+ else if (iArgs !== -1) {
42
+ end = iArgs;
43
+ len = closeArgs.length;
44
+ }
45
+ if (end === -1)
46
+ break; // still accumulating
47
+ buf = buf.slice(end + len);
48
+ inTag = null;
49
+ }
50
+ }
51
+ return out;
52
+ }
53
+ function flush() {
54
+ const result = inTag ? '' : buf;
55
+ buf = '';
56
+ inTag = null;
57
+ return result;
58
+ }
59
+ return { feed, flush };
60
+ }
61
+ /**
62
+ * Stream renderer with line-state tracking.
63
+ *
64
+ * Invariants:
65
+ * - Text chunks render continuously — nothing is ever inserted mid-word.
66
+ * - The XML stripper's held-back tail is flushed BEFORE any status output
67
+ * (tool pills, model switches, token usage). Printing status before the
68
+ * flush was the bug that split words across the usage line.
69
+ * - Status lines start on a fresh line, never gluing onto streamed text.
70
+ */
71
+ export function createStreamRenderer(write) {
72
+ const filter = makeToolXmlStripper();
73
+ let midLine = false;
74
+ const emit = (s) => {
75
+ if (!s)
76
+ return;
77
+ write(s);
78
+ midLine = !s.endsWith('\n');
79
+ };
80
+ const flushFilter = () => { emit(filter.flush()); };
81
+ const freshLine = () => {
82
+ flushFilter();
83
+ if (midLine) {
84
+ write('\n');
85
+ midLine = false;
86
+ }
87
+ };
88
+ return {
89
+ text(delta) { emit(filter.feed(delta)); },
90
+ toolCall(name) {
91
+ freshLine();
92
+ write(chalk.cyan(` ⟳ ${name}…`));
93
+ midLine = true;
94
+ },
95
+ toolResult() {
96
+ write(chalk.green(' ✓\n'));
97
+ midLine = false;
98
+ },
99
+ modelSwitch(from, to) {
100
+ freshLine();
101
+ write(chalk.dim(` ⟳ switching model: ${from} → ${to}\n`));
102
+ midLine = false;
103
+ },
104
+ error(msg) {
105
+ freshLine();
106
+ write(chalk.red(`Error: ${msg}\n`));
107
+ midLine = false;
108
+ },
109
+ usage(input, output) {
110
+ freshLine();
111
+ write(chalk.dim(` [${input}in / ${output}out tokens]\n`));
112
+ midLine = false;
113
+ },
114
+ finish() { flushFilter(); },
115
+ };
116
+ }
117
+ /** Update or append KEY=value lines in a .env file. */
118
+ export function patchEnvFile(envPath, changes) {
119
+ let content = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : '';
120
+ for (const [key, val] of Object.entries(changes)) {
121
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
122
+ const re = new RegExp(`^${escaped}=.*$`, 'm');
123
+ if (re.test(content)) {
124
+ // Function replacer — a literal `$` in val must not trigger
125
+ // replacement-pattern expansion ($&, $', …) and corrupt the .env
126
+ content = content.replace(re, () => `${key}=${val}`);
127
+ }
128
+ else {
129
+ content += `\n${key}=${val}`;
130
+ }
131
+ }
132
+ fs.writeFileSync(envPath, content);
133
+ }
134
+ /** Maps raw provider error messages to actionable user-facing strings. */
135
+ export function friendlyError(msg, providerName) {
136
+ if (/401|unauthorized|invalid.*key|api.?key/i.test(msg)) {
137
+ const key = providerName ? `${providerName.toUpperCase()}_API_KEY` : 'PROVIDER_API_KEY';
138
+ return `Invalid API key. Check ${key} in .env`;
139
+ }
140
+ if (/ECONNREFUSED|ENOTFOUND|connect.*ollama/i.test(msg))
141
+ return 'Ollama not running. Run: ollama serve';
142
+ if (/model.*not.?found|pull.*model|does not exist|is not found/i.test(msg)) {
143
+ const isOllama = providerName === 'ollama' || /\bollama\b/i.test(msg);
144
+ const m = msg.match(/["']([^"']+)["']/) ?? msg.match(/model[s]?[\s/]+(\S+:\S+)/i);
145
+ const name = m?.[1];
146
+ if (isOllama)
147
+ return `Model ${name ?? 'unknown'} not installed. Run: ollama pull ${name ?? '<model>'}`;
148
+ return `Model not available on ${providerName ?? 'this provider'}. To use local models run: /switch ollama`;
149
+ }
150
+ if (/429|rate.?limit|too many requests/i.test(msg))
151
+ return 'Rate limit hit. Wait 60s or run /switch for provider-switch instructions';
152
+ return msg;
153
+ }