miii-cli 0.1.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 (98) hide show
  1. package/.claude/settings.local.json +18 -0
  2. package/Makefile +13 -0
  3. package/README.md +182 -0
  4. package/dist/config.d.ts +2 -0
  5. package/dist/config.js +24 -0
  6. package/dist/config.js.map +1 -0
  7. package/dist/files/ops.d.ts +11 -0
  8. package/dist/files/ops.js +66 -0
  9. package/dist/files/ops.js.map +1 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +12 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/init.d.ts +1 -0
  14. package/dist/init.js +32 -0
  15. package/dist/init.js.map +1 -0
  16. package/dist/llm/ollama.d.ts +9 -0
  17. package/dist/llm/ollama.js +51 -0
  18. package/dist/llm/ollama.js.map +1 -0
  19. package/dist/llm/stream.d.ts +12 -0
  20. package/dist/llm/stream.js +129 -0
  21. package/dist/llm/stream.js.map +1 -0
  22. package/dist/parser/stream-parser.d.ts +17 -0
  23. package/dist/parser/stream-parser.js +54 -0
  24. package/dist/parser/stream-parser.js.map +1 -0
  25. package/dist/sessions.d.ts +9 -0
  26. package/dist/sessions.js +48 -0
  27. package/dist/sessions.js.map +1 -0
  28. package/dist/skills/loader.d.ts +23 -0
  29. package/dist/skills/loader.js +91 -0
  30. package/dist/skills/loader.js.map +1 -0
  31. package/dist/tools/index.d.ts +8 -0
  32. package/dist/tools/index.js +79 -0
  33. package/dist/tools/index.js.map +1 -0
  34. package/dist/tui/App.d.ts +9 -0
  35. package/dist/tui/App.js +259 -0
  36. package/dist/tui/App.js.map +1 -0
  37. package/dist/tui/InputBar.d.ts +10 -0
  38. package/dist/tui/InputBar.js +289 -0
  39. package/dist/tui/InputBar.js.map +1 -0
  40. package/dist/tui/components/AtPicker.d.ts +8 -0
  41. package/dist/tui/components/AtPicker.js +19 -0
  42. package/dist/tui/components/AtPicker.js.map +1 -0
  43. package/dist/tui/components/CommandPalette.d.ts +8 -0
  44. package/dist/tui/components/CommandPalette.js +25 -0
  45. package/dist/tui/components/CommandPalette.js.map +1 -0
  46. package/dist/tui/components/InputArea.d.ts +11 -0
  47. package/dist/tui/components/InputArea.js +268 -0
  48. package/dist/tui/components/InputArea.js.map +1 -0
  49. package/dist/tui/components/MessageList.d.ts +10 -0
  50. package/dist/tui/components/MessageList.js +98 -0
  51. package/dist/tui/components/MessageList.js.map +1 -0
  52. package/dist/tui/components/ModelPicker.d.ts +18 -0
  53. package/dist/tui/components/ModelPicker.js +74 -0
  54. package/dist/tui/components/ModelPicker.js.map +1 -0
  55. package/dist/tui/components/StatusBar.d.ts +12 -0
  56. package/dist/tui/components/StatusBar.js +15 -0
  57. package/dist/tui/components/StatusBar.js.map +1 -0
  58. package/dist/tui/printer.d.ts +7 -0
  59. package/dist/tui/printer.js +106 -0
  60. package/dist/tui/printer.js.map +1 -0
  61. package/dist/types.d.ts +19 -0
  62. package/dist/types.js +4 -0
  63. package/dist/types.js.map +1 -0
  64. package/dist/workers/context.worker.d.ts +1 -0
  65. package/dist/workers/context.worker.js +69 -0
  66. package/dist/workers/context.worker.js.map +1 -0
  67. package/dist/workers/diff.worker.d.ts +1 -0
  68. package/dist/workers/diff.worker.js +12 -0
  69. package/dist/workers/diff.worker.js.map +1 -0
  70. package/dist/workers/spawn.d.ts +1 -0
  71. package/dist/workers/spawn.js +18 -0
  72. package/dist/workers/spawn.js.map +1 -0
  73. package/install.sh +6 -0
  74. package/package.json +29 -0
  75. package/src/config.ts +25 -0
  76. package/src/files/ops.ts +71 -0
  77. package/src/index.ts +11 -0
  78. package/src/init.ts +39 -0
  79. package/src/llm/ollama.ts +58 -0
  80. package/src/llm/stream.ts +118 -0
  81. package/src/parser/stream-parser.ts +54 -0
  82. package/src/sessions.ts +46 -0
  83. package/src/skills/loader.ts +109 -0
  84. package/src/tools/index.ts +83 -0
  85. package/src/tui/App.tsx +308 -0
  86. package/src/tui/InputBar.tsx +347 -0
  87. package/src/tui/components/AtPicker.tsx +49 -0
  88. package/src/tui/components/CommandPalette.tsx +50 -0
  89. package/src/tui/components/InputArea.tsx +285 -0
  90. package/src/tui/components/MessageList.tsx +192 -0
  91. package/src/tui/components/ModelPicker.tsx +134 -0
  92. package/src/tui/components/StatusBar.tsx +36 -0
  93. package/src/tui/printer.ts +121 -0
  94. package/src/types.ts +25 -0
  95. package/src/workers/context.worker.ts +62 -0
  96. package/src/workers/diff.worker.ts +20 -0
  97. package/src/workers/spawn.ts +19 -0
  98. package/tsconfig.json +18 -0
@@ -0,0 +1,106 @@
1
+ // ANSI-formatted stdout output — goes into terminal scrollback
2
+ const R = '\x1b[0m';
3
+ const BOLD = '\x1b[1m';
4
+ const DIM = '\x1b[2m';
5
+ function bold(s) { return `${BOLD}${s}${R}`; }
6
+ function dim(s) { return `${DIM}${s}${R}`; }
7
+ function col(code, s) { return `\x1b[${code}m${s}${R}`; }
8
+ const blue = (s) => col(94, s);
9
+ const green = (s) => col(92, s);
10
+ const cyan = (s) => col(96, s);
11
+ const gray = (s) => col(90, s);
12
+ const yellow = (s) => col(93, s);
13
+ function indent(text, pad = ' ') {
14
+ return text.split('\n').map(l => pad + l).join('\n');
15
+ }
16
+ function formatContent(text) {
17
+ const lines = text.split('\n');
18
+ let inCode = false;
19
+ const out = [];
20
+ for (const line of lines) {
21
+ if (line.startsWith('<tool_call>') || line.startsWith('</tool_call>'))
22
+ continue;
23
+ if (line.startsWith('```')) {
24
+ inCode = !inCode;
25
+ out.push(' ' + dim(gray(line)));
26
+ }
27
+ else if (inCode) {
28
+ out.push(' ' + yellow(line || ' '));
29
+ }
30
+ else {
31
+ out.push(' ' + (line || ''));
32
+ }
33
+ }
34
+ return out.join('\n');
35
+ }
36
+ export function welcome(provider, model, cwd) {
37
+ const cols = Math.min(process.stdout.columns ?? 80, 100);
38
+ const innerW = cols - 2;
39
+ const leftW = Math.floor(innerW * 0.44);
40
+ const rightW = innerW - leftW - 1;
41
+ function vis(s) { return s.replace(/\x1b\[[0-9;]*m/g, ''); }
42
+ function cell(s, w) {
43
+ const v = vis(s);
44
+ if (v.length < w)
45
+ return s + ' '.repeat(w - v.length);
46
+ if (v.length === w)
47
+ return s;
48
+ return v.slice(0, w - 1) + '…';
49
+ }
50
+ function row(l, r) {
51
+ return gray('│') + cell(l, leftW) + gray('│') + cell(r, rightW) + gray('│');
52
+ }
53
+ function blank() {
54
+ return gray('│') + ' '.repeat(leftW) + gray('│') + ' '.repeat(rightW) + gray('│');
55
+ }
56
+ function rcmd(key, desc, keyW = 10) {
57
+ return ' ' + cyan(key) + ' '.repeat(Math.max(1, keyW - key.length)) + gray(desc);
58
+ }
59
+ const titleStr = '─ MIII - CLI ';
60
+ const dashCount = Math.max(0, cols - 2 - titleStr.length);
61
+ const top = gray('╭') + gray('─') + bold(cyan(' MIII - CLI ')) + gray('─'.repeat(dashCount) + '╮');
62
+ const bottom = gray('╰' + '─'.repeat(innerW) + '╯');
63
+ const shortCwd = cwd.replace(process.env.HOME ?? '', '~');
64
+ const lines = [
65
+ top,
66
+ blank(),
67
+ row(` ${bold(cyan('MIII - CLI'))}`, ` ${bold(yellow('Getting started'))}`),
68
+ row(` ${gray('Claude Code-level terminal')}`, rcmd('@filename', 'inject file into context')),
69
+ row(` ${gray('workflows, local models.')}`, rcmd('/skill', 'run a skill or command')),
70
+ row('', rcmd('/models', 'switch or pull models')),
71
+ row('', rcmd('/list', 'list all skills')),
72
+ row('', rcmd('/session', 'manage sessions')),
73
+ blank(),
74
+ row(` ${gray(provider + '/' + model)}`, ` ${bold(yellow('Tips'))}`),
75
+ row(` ${gray(shortCwd)}`, rcmd('ctrl+c', 'stop streaming')),
76
+ row('', rcmd('ctrl+c x2', 'exit')),
77
+ blank(),
78
+ bottom,
79
+ ];
80
+ process.stdout.write(lines.join('\n') + '\n');
81
+ }
82
+ export function userMsg(text) {
83
+ const atHighlighted = text.replace(/(@[\w./\-]+)/g, (m) => cyan(m));
84
+ console.log(`\n${bold(blue('You'))}\n${indent(atHighlighted)}`);
85
+ }
86
+ export function assistantMsg(text) {
87
+ console.log(`\n${bold(green('miii'))}\n${formatContent(text)}`);
88
+ }
89
+ export function toolMsg(name, result) {
90
+ const preview = result.length > 250 ? result.slice(0, 250) + '…' : result;
91
+ const body = preview.trim()
92
+ ? preview.split('\n').map(l => gray(' ' + l)).join('\n')
93
+ : '';
94
+ console.log(` ${green('✓')} ${cyan(name)}${body ? '\n' + body : ''}`);
95
+ }
96
+ export function systemMsg(text) {
97
+ console.log(gray(`─ ${text}`));
98
+ }
99
+ export function errorMsg(text) {
100
+ console.log(gray(`error: ${text}`));
101
+ }
102
+ export function divider() {
103
+ const cols = process.stdout.columns ?? 80;
104
+ process.stdout.write(`${gray('─'.repeat(cols))}\n`);
105
+ }
106
+ //# sourceMappingURL=printer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"printer.js","sourceRoot":"","sources":["../../src/tui/printer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ export type Role = 'user' | 'assistant' | 'system' | 'tool';
2
+ export type Status = 'idle' | 'streaming' | 'tool';
3
+ export interface Message {
4
+ id: string;
5
+ role: Role;
6
+ content: string;
7
+ timestamp: number;
8
+ }
9
+ export interface Config {
10
+ model: string;
11
+ provider: 'ollama' | 'openai-compat';
12
+ baseUrl: string;
13
+ systemPrompt?: string;
14
+ }
15
+ export interface ChatMessage {
16
+ role: 'system' | 'user' | 'assistant';
17
+ content: string;
18
+ }
19
+ export declare function generateId(): string;
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ export function generateId() {
2
+ return Math.random().toString(36).slice(2, 10);
3
+ }
4
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAsBA,MAAM,UAAU,UAAU;IACxB,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;AAChD,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,69 @@
1
+ import { workerData, parentPort } from 'worker_threads';
2
+ import { readFileSync, statSync, readdirSync, existsSync } from 'fs';
3
+ import { join, relative, extname } from 'path';
4
+ const SKIP_DIRS = new Set([
5
+ 'node_modules', 'dist', 'build', '.git', '.next', '.nuxt', '.svelte-kit',
6
+ 'out', '__pycache__', '.cache', 'coverage', '.nyc_output', 'vendor',
7
+ 'target', '.turbo', '.vercel', 'generated', '.gradle', '.expo',
8
+ 'bin', 'obj', 'tmp', 'temp', 'logs',
9
+ ]);
10
+ const SKIP_EXTS = new Set(['.map', '.lock', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.mp4', '.mp3', '.pdf', '.zip', '.tar', '.gz', '.exe', '.dll', '.so', '.dylib', '.wasm', '.class', '.pyc', '.ttf', '.woff', '.woff2']);
11
+ function safe(p) {
12
+ try {
13
+ const s = statSync(p);
14
+ if (s.size > 512 * 1024)
15
+ return null;
16
+ return readFileSync(p, 'utf-8');
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ function walk(dir, out, cwd, depth = 0) {
23
+ if (depth > 4)
24
+ return;
25
+ for (const name of readdirSync(dir)) {
26
+ if (name.startsWith('.'))
27
+ continue;
28
+ if (SKIP_DIRS.has(name))
29
+ continue;
30
+ if (SKIP_EXTS.has(extname(name)))
31
+ continue;
32
+ if (name.endsWith('.d.ts') || name.endsWith('.js.map'))
33
+ continue;
34
+ const full = join(dir, name);
35
+ try {
36
+ const s = statSync(full);
37
+ if (s.isDirectory())
38
+ walk(full, out, cwd, depth + 1);
39
+ else
40
+ out.push(full);
41
+ }
42
+ catch { }
43
+ }
44
+ }
45
+ function build(input) {
46
+ const parts = [];
47
+ for (const p of input.paths) {
48
+ if (!existsSync(p))
49
+ continue;
50
+ const s = statSync(p);
51
+ if (s.isFile()) {
52
+ const content = safe(p);
53
+ if (content !== null)
54
+ parts.push(`<file path="${relative(input.cwd, p)}">\n${content}\n</file>`);
55
+ }
56
+ else if (s.isDirectory()) {
57
+ const files = [];
58
+ walk(p, files, input.cwd);
59
+ for (const f of files.slice(0, 100)) {
60
+ const content = safe(f);
61
+ if (content !== null)
62
+ parts.push(`<file path="${relative(input.cwd, f)}">\n${content}\n</file>`);
63
+ }
64
+ }
65
+ }
66
+ return parts.join('\n\n');
67
+ }
68
+ parentPort?.postMessage({ context: build(workerData) });
69
+ //# sourceMappingURL=context.worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.worker.js","sourceRoot":"","sources":["../../src/workers/context.worker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AACvD,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,IAAI,CAAA;AACpE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AAE9C,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa;IACxE,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ;IACnE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO;IAC9D,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM;CACpC,CAAC,CAAA;AACF,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAA;AAOpO,SAAS,IAAI,CAAC,CAAS;IACrB,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;QACrB,IAAI,CAAC,CAAC,IAAI,GAAG,GAAG,GAAG,IAAI;YAAE,OAAO,IAAI,CAAA;QACpC,OAAO,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC,CAAA;IACjC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,IAAI,CAAA;IAAC,CAAC;AACzB,CAAC;AAED,SAAS,IAAI,CAAC,GAAW,EAAE,GAAa,EAAE,GAAW,EAAE,KAAK,GAAG,CAAC;IAC9D,IAAI,KAAK,GAAG,CAAC;QAAE,OAAM;IACrB,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;QACpC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAQ;QAClC,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAQ;QACjC,IAAI,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAAE,SAAQ;QAC1C,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;YAAE,SAAQ;QAChE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;QAC5B,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAA;YACxB,IAAI,CAAC,CAAC,WAAW,EAAE;gBAAE,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC,CAAC,CAAA;;gBAC/C,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACrB,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,KAAK,CAAC,KAAY;IACzB,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QAC5B,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;YAAE,SAAQ;QAC5B,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAA;QACrB,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;YACvB,IAAI,OAAO,KAAK,IAAI;gBAAE,KAAK,CAAC,IAAI,CAAC,eAAe,QAAQ,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,OAAO,OAAO,WAAW,CAAC,CAAA;QAClG,CAAC;aAAM,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAa,EAAE,CAAA;YAC1B,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAA;YACzB,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;gBACpC,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;gBACvB,IAAI,OAAO,KAAK,IAAI;oBAAE,KAAK,CAAC,IAAI,CAAC,eAAe,QAAQ,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,OAAO,OAAO,WAAW,CAAC,CAAA;YAClG,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;AAC3B,CAAC;AAED,UAAU,EAAE,WAAW,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,UAAmB,CAAC,EAAE,CAAC,CAAA"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { workerData, parentPort } from 'worker_threads';
2
+ import { createPatch, applyPatch } from 'diff';
3
+ const inp = workerData;
4
+ if (inp.action === 'diff') {
5
+ const patch = createPatch(inp.filename ?? 'file', inp.oldContent ?? '', inp.newContent ?? '');
6
+ parentPort?.postMessage({ patch });
7
+ }
8
+ else {
9
+ const result = applyPatch(inp.oldContent ?? '', inp.patch ?? '');
10
+ parentPort?.postMessage({ result: result === false ? null : result });
11
+ }
12
+ //# sourceMappingURL=diff.worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.worker.js","sourceRoot":"","sources":["../../src/workers/diff.worker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,MAAM,CAAA;AAU9C,MAAM,GAAG,GAAG,UAAmB,CAAA;AAE/B,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;IAC1B,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,QAAQ,IAAI,MAAM,EAAE,GAAG,CAAC,UAAU,IAAI,EAAE,EAAE,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,CAAA;IAC7F,UAAU,EAAE,WAAW,CAAC,EAAE,KAAK,EAAE,CAAC,CAAA;AACpC,CAAC;KAAM,CAAC;IACN,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,UAAU,IAAI,EAAE,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAA;IAChE,UAAU,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAA;AACvE,CAAC"}
@@ -0,0 +1 @@
1
+ export declare function spawnWorker<T>(name: string, data: unknown): Promise<T>;
@@ -0,0 +1,18 @@
1
+ import { Worker } from 'worker_threads';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+ const __dir = dirname(fileURLToPath(import.meta.url));
5
+ const isDev = process.argv.some(a => a.includes('tsx')) || import.meta.url.endsWith('.ts');
6
+ const ext = isDev ? '.ts' : '.js';
7
+ export function spawnWorker(name, data) {
8
+ return new Promise((resolve, reject) => {
9
+ const path = join(__dir, `${name}.worker${ext}`);
10
+ const w = new Worker(path, {
11
+ workerData: data,
12
+ execArgv: isDev ? ['--import', 'tsx/esm'] : [],
13
+ });
14
+ w.once('message', (r) => { w.terminate(); resolve(r); });
15
+ w.once('error', (e) => { w.terminate(); reject(e); });
16
+ });
17
+ }
18
+ //# sourceMappingURL=spawn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spawn.js","sourceRoot":"","sources":["../../src/workers/spawn.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAA;AACnC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAEpC,MAAM,KAAK,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;AACrD,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AAC1F,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAA;AAEjC,MAAM,UAAU,WAAW,CAAI,IAAY,EAAE,IAAa;IACxD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,UAAU,GAAG,EAAE,CAAC,CAAA;QAChD,MAAM,CAAC,GAAG,IAAI,MAAM,CAAC,IAAI,EAAE;YACzB,UAAU,EAAE,IAAI;YAChB,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE;SAC/C,CAAC,CAAA;QACF,CAAC,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAI,EAAE,EAAE,GAAG,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA,CAAC,CAAC,CAAC,CAAA;QAC1D,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA,CAAC,CAAC,CAAC,CAAA;IACtD,CAAC,CAAC,CAAA;AACJ,CAAC"}
package/install.sh ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+ npm install
4
+ npm run build
5
+ npm link
6
+ echo "miii installed. Run: miii"
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "miii-cli",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "miii": "dist/index.js"
7
+ },
8
+ "scripts": {
9
+ "dev": "tsx src/index.ts",
10
+ "build": "tsc",
11
+ "start": "node dist/index.js",
12
+ "link": "npm run build && npm link"
13
+ },
14
+ "dependencies": {
15
+ "ink": "^5.2.0",
16
+ "react": "^18.3.1",
17
+ "diff": "^7.0.0",
18
+ "glob": "^11.0.1",
19
+ "minimist": "^1.2.8"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^22.10.0",
23
+ "@types/react": "^18.3.1",
24
+ "@types/diff": "^7.0.0",
25
+ "@types/minimist": "^1.2.5",
26
+ "tsx": "^4.19.1",
27
+ "typescript": "^5.7.3"
28
+ }
29
+ }
package/src/config.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { readFileSync, existsSync } from 'fs'
2
+ import { homedir } from 'os'
3
+ import { join } from 'path'
4
+ import type { Config } from './types.js'
5
+
6
+ const defaults: Config = {
7
+ model: 'llama3.2',
8
+ provider: 'ollama',
9
+ baseUrl: 'http://localhost:11434',
10
+ }
11
+
12
+ export function loadConfig(): Config {
13
+ const candidates = [
14
+ join(process.cwd(), '.miii.json'),
15
+ join(homedir(), '.config', 'miii', 'config.json'),
16
+ ]
17
+ for (const p of candidates) {
18
+ if (existsSync(p)) {
19
+ try {
20
+ return { ...defaults, ...JSON.parse(readFileSync(p, 'utf-8')) }
21
+ } catch {}
22
+ }
23
+ }
24
+ return { ...defaults }
25
+ }
@@ -0,0 +1,71 @@
1
+ import {
2
+ readFileSync, writeFileSync, unlinkSync,
3
+ mkdirSync, readdirSync, statSync, existsSync,
4
+ } from 'fs'
5
+ import { join, dirname, relative, extname } from 'path'
6
+
7
+ const SKIP_DIRS = new Set([
8
+ 'node_modules', 'dist', 'build', '.git', '.next', '.nuxt', '.svelte-kit',
9
+ 'out', '__pycache__', '.cache', 'coverage', '.nyc_output', 'vendor',
10
+ 'target', '.turbo', '.vercel', 'generated', '.gradle', '.expo',
11
+ 'bin', 'obj', '.idea', '.vscode', 'tmp', 'temp', 'logs',
12
+ ])
13
+
14
+ const SKIP_EXTS = new Set([
15
+ '.map', '.lock',
16
+ '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.bmp', '.tiff',
17
+ '.mp4', '.mp3', '.wav', '.ogg', '.pdf',
18
+ '.zip', '.tar', '.gz', '.rar', '.7z',
19
+ '.exe', '.dll', '.so', '.dylib', '.wasm', '.class', '.pyc',
20
+ '.ttf', '.woff', '.woff2', '.eot',
21
+ ])
22
+
23
+ const SKIP_NAMES = new Set([
24
+ 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'Cargo.lock',
25
+ 'poetry.lock', 'Gemfile.lock', 'composer.lock',
26
+ '.DS_Store', 'Thumbs.db', '.env.local', 'LICENSE', 'LICENSE.md',
27
+ ])
28
+
29
+ export function readFile(p: string): string {
30
+ if (!existsSync(p)) return ''
31
+ return readFileSync(p, 'utf-8')
32
+ }
33
+
34
+ export function writeFile(p: string, content: string): void {
35
+ mkdirSync(dirname(p), { recursive: true })
36
+ writeFileSync(p, content, 'utf-8')
37
+ }
38
+
39
+ export function deleteFile(p: string): void {
40
+ unlinkSync(p)
41
+ }
42
+
43
+ export interface FileEntry {
44
+ name: string
45
+ path: string
46
+ rel: string
47
+ type: 'file' | 'dir'
48
+ size?: number
49
+ }
50
+
51
+ export function listFiles(dir: string, recursive = false, cwd = process.cwd()): FileEntry[] {
52
+ if (!existsSync(dir)) return []
53
+ const entries: FileEntry[] = []
54
+ for (const name of readdirSync(dir)) {
55
+ if (name.startsWith('.')) continue
56
+ if (SKIP_NAMES.has(name)) continue
57
+ if (SKIP_DIRS.has(name)) continue
58
+ const ext = extname(name)
59
+ if (SKIP_EXTS.has(ext)) continue
60
+ if (name.endsWith('.d.ts') || name.endsWith('.js.map')) continue
61
+ let stat
62
+ try { stat = statSync(join(dir, name)) } catch { continue }
63
+ const full = join(dir, name)
64
+ const type = stat.isDirectory() ? 'dir' : 'file'
65
+ entries.push({ name, path: full, rel: relative(cwd, full), type, size: stat.isFile() ? stat.size : undefined })
66
+ if (recursive && type === 'dir') {
67
+ entries.push(...listFiles(full, true, cwd))
68
+ }
69
+ }
70
+ return entries
71
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ // Minimal entry — heavy imports lazy-loaded
3
+ async function main() {
4
+ const { lazyInit } = await import('./init.js')
5
+ await lazyInit()
6
+ }
7
+
8
+ main().catch(err => {
9
+ process.stderr.write(`fatal: ${err.message}\n`)
10
+ process.exit(1)
11
+ })
package/src/init.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { render } from 'ink'
2
+ import React from 'react'
3
+ import minimist from 'minimist'
4
+ import { loadConfig } from './config.js'
5
+ import { SkillLoader } from './skills/loader.js'
6
+ import { InputBar } from './tui/InputBar.js'
7
+ import { welcome } from './tui/printer.js'
8
+
9
+ export async function lazyInit(): Promise<void> {
10
+ const argv = minimist(process.argv.slice(2), {
11
+ string: ['model', 'url', 'provider', 'session'],
12
+ alias: { m: 'model', u: 'url', p: 'provider', s: 'session' },
13
+ })
14
+
15
+ const config = loadConfig()
16
+ if (argv.model) config.model = argv.model
17
+ if (argv.url) config.baseUrl = argv.url
18
+ if (argv.provider) config.provider = argv.provider as typeof config.provider
19
+
20
+ const skills = new SkillLoader()
21
+ await skills.loadAll()
22
+
23
+ // Print welcome banner to scrollback BEFORE Ink starts
24
+ welcome(config.provider, config.model, process.cwd())
25
+
26
+ // Move cursor to last terminal row so Ink renders at absolute bottom
27
+ process.stdout.write(`\x1b[${process.stdout.rows ?? 24};1H`)
28
+
29
+ // Ink renders ONLY the input bar (small footprint at bottom)
30
+ // patchConsole: true (default) ensures console.log output appears above Ink
31
+ const sessionName = (argv.session as string) || 'default'
32
+
33
+ const { waitUntilExit } = render(
34
+ React.createElement(InputBar, { config, skills, cwd: process.cwd(), session: sessionName }),
35
+ { exitOnCtrlC: false }
36
+ )
37
+
38
+ await waitUntilExit()
39
+ }
@@ -0,0 +1,58 @@
1
+ export interface OllamaModel {
2
+ name: string
3
+ size: number
4
+ modified_at: string
5
+ digest?: string
6
+ }
7
+
8
+ export async function listModels(baseUrl: string): Promise<OllamaModel[]> {
9
+ const res = await fetch(`${baseUrl}/api/tags`)
10
+ if (!res.ok) throw new Error(`Ollama ${res.status}: ${await res.text()}`)
11
+ const data = (await res.json()) as { models?: OllamaModel[] }
12
+ return data.models ?? []
13
+ }
14
+
15
+ export async function pullModel(
16
+ baseUrl: string,
17
+ name: string,
18
+ onProgress: (status: string, pct: number | undefined) => void,
19
+ signal?: AbortSignal,
20
+ ): Promise<void> {
21
+ const res = await fetch(`${baseUrl}/api/pull`, {
22
+ method: 'POST',
23
+ headers: { 'Content-Type': 'application/json' },
24
+ body: JSON.stringify({ name, stream: true }),
25
+ signal,
26
+ })
27
+ if (!res.ok) throw new Error(`pull failed: ${res.status} ${await res.text()}`)
28
+
29
+ const reader = res.body!.getReader()
30
+ const dec = new TextDecoder()
31
+ let buf = ''
32
+
33
+ try {
34
+ while (true) {
35
+ const { done, value } = await reader.read()
36
+ if (done) break
37
+ buf += dec.decode(value, { stream: true })
38
+ const lines = buf.split('\n')
39
+ buf = lines.pop() ?? ''
40
+ for (const line of lines) {
41
+ if (!line.trim()) continue
42
+ try {
43
+ const obj = JSON.parse(line) as { status?: string; completed?: number; total?: number }
44
+ const pct = obj.total ? Math.round(((obj.completed ?? 0) / obj.total) * 100) : undefined
45
+ onProgress(obj.status ?? '', pct)
46
+ } catch {}
47
+ }
48
+ }
49
+ } finally {
50
+ reader.releaseLock()
51
+ }
52
+ }
53
+
54
+ export function fmtSize(bytes: number): string {
55
+ if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)}GB`
56
+ if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(0)}MB`
57
+ return `${(bytes / 1e3).toFixed(0)}KB`
58
+ }
@@ -0,0 +1,118 @@
1
+ import type { ChatMessage } from '../types.js'
2
+
3
+ export interface StreamConfig {
4
+ provider: 'ollama' | 'openai-compat'
5
+ model: string
6
+ baseUrl: string
7
+ messages: ChatMessage[]
8
+ signal?: AbortSignal
9
+ onToken: (token: string) => void
10
+ onDone: (fullText: string) => void | Promise<void>
11
+ onError: (err: Error) => void
12
+ }
13
+
14
+ export async function stream(cfg: StreamConfig): Promise<void> {
15
+ if (cfg.provider === 'openai-compat') {
16
+ return streamOpenAI(cfg)
17
+ }
18
+ return streamOllama(cfg)
19
+ }
20
+
21
+ async function streamOllama(cfg: StreamConfig): Promise<void> {
22
+ const { model, messages, baseUrl, signal, onToken, onDone, onError } = cfg
23
+ let res: Response
24
+ try {
25
+ res = await fetch(`${baseUrl}/api/chat`, {
26
+ method: 'POST',
27
+ headers: { 'Content-Type': 'application/json' },
28
+ body: JSON.stringify({ model, messages, stream: true }),
29
+ signal,
30
+ })
31
+ } catch (err) {
32
+ onError(toError(err))
33
+ return
34
+ }
35
+ if (!res.ok) {
36
+ onError(new Error(`Ollama ${res.status}: ${await res.text()}`))
37
+ return
38
+ }
39
+ const reader = res.body!.getReader()
40
+ const dec = new TextDecoder()
41
+ let full = ''
42
+ let buf = ''
43
+ try {
44
+ while (true) {
45
+ const { done, value } = await reader.read()
46
+ if (done) break
47
+ buf += dec.decode(value, { stream: true })
48
+ const lines = buf.split('\n')
49
+ buf = lines.pop() ?? ''
50
+ for (const line of lines) {
51
+ if (!line.trim()) continue
52
+ try {
53
+ const obj = JSON.parse(line)
54
+ const tok: string = obj?.message?.content ?? ''
55
+ if (tok) { onToken(tok); full += tok }
56
+ if (obj?.done) { await onDone(full); return }
57
+ } catch {}
58
+ }
59
+ }
60
+ await onDone(full)
61
+ } catch (err) {
62
+ if ((err as Error)?.name !== 'AbortError') onError(toError(err))
63
+ } finally {
64
+ reader.releaseLock()
65
+ }
66
+ }
67
+
68
+ async function streamOpenAI(cfg: StreamConfig): Promise<void> {
69
+ const { model, messages, baseUrl, signal, onToken, onDone, onError } = cfg
70
+ let res: Response
71
+ try {
72
+ res = await fetch(`${baseUrl}/v1/chat/completions`, {
73
+ method: 'POST',
74
+ headers: { 'Content-Type': 'application/json', Authorization: 'Bearer local' },
75
+ body: JSON.stringify({ model, messages, stream: true }),
76
+ signal,
77
+ })
78
+ } catch (err) {
79
+ onError(toError(err))
80
+ return
81
+ }
82
+ if (!res.ok) {
83
+ onError(new Error(`LLM ${res.status}: ${await res.text()}`))
84
+ return
85
+ }
86
+ const reader = res.body!.getReader()
87
+ const dec = new TextDecoder()
88
+ let full = ''
89
+ let buf = ''
90
+ try {
91
+ while (true) {
92
+ const { done, value } = await reader.read()
93
+ if (done) break
94
+ buf += dec.decode(value, { stream: true })
95
+ const lines = buf.split('\n')
96
+ buf = lines.pop() ?? ''
97
+ for (const line of lines) {
98
+ const trimmed = line.replace(/^data:\s*/, '').trim()
99
+ if (!trimmed || trimmed === '[DONE]') continue
100
+ try {
101
+ const obj = JSON.parse(trimmed)
102
+ const tok: string = obj?.choices?.[0]?.delta?.content ?? ''
103
+ if (tok) { onToken(tok); full += tok }
104
+ if (obj?.choices?.[0]?.finish_reason) { await onDone(full); return }
105
+ } catch {}
106
+ }
107
+ }
108
+ await onDone(full)
109
+ } catch (err) {
110
+ if ((err as Error)?.name !== 'AbortError') onError(toError(err))
111
+ } finally {
112
+ reader.releaseLock()
113
+ }
114
+ }
115
+
116
+ function toError(e: unknown): Error {
117
+ return e instanceof Error ? e : new Error(String(e))
118
+ }