pelulu-cli 1.0.0 → 1.0.1

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 (49) hide show
  1. package/DailyNotesApp/app/build.gradle.kts +57 -0
  2. package/DailyNotesApp/app/src/main/java/com/dailynotes/app/MainActivity.kt +48 -0
  3. package/DailyNotesApp/app/src/main/java/com/dailynotes/app/Note.kt +13 -0
  4. package/DailyNotesApp/app/src/main/java/com/dailynotes/app/NoteDatabase.kt +9 -0
  5. package/DailyNotesApp/build.gradle.kts +5 -0
  6. package/DailyNotesApp/settings.gradle.kts +17 -0
  7. package/README.md +69 -82
  8. package/config.example.json +2 -1
  9. package/package.json +6 -3
  10. package/preview.jpg +0 -0
  11. package/src/core/auto-format.js +61 -12
  12. package/src/core/config.js +1 -1
  13. package/src/core/confirm.js +2 -2
  14. package/src/core/diff.js +4 -4
  15. package/src/core/doctor.js +2 -2
  16. package/src/core/error-handler.js +1 -1
  17. package/src/core/file-tracker.js +4 -4
  18. package/src/core/formatter.js +19 -19
  19. package/src/core/logger.js +19 -3
  20. package/src/core/spinner.js +2 -2
  21. package/src/core/stats.js +2 -2
  22. package/src/core/thinking.js +8 -8
  23. package/src/core/tool-help.js +2 -2
  24. package/src/core/tool-registry.js +6 -10
  25. package/src/core/update-checker.js +125 -0
  26. package/src/core/wizard.js +5 -5
  27. package/src/core/workspace.js +4 -4
  28. package/src/index.js +82 -37
  29. package/src/mcp/activation.js +3 -3
  30. package/src/mcp/message-sender.js +1 -1
  31. package/src/mcp/mqtt-client.js +4 -4
  32. package/src/repl-commands.js +2 -1
  33. package/src/repl.js +26 -6
  34. package/src/tools/config.js +1 -1
  35. package/src/tools/diff.js +6 -0
  36. package/src/tools/file.js +3 -3
  37. package/src/tools/git.js +1 -1
  38. package/src/tools/network.js +1 -1
  39. package/src/tools/project.js +2 -2
  40. package/src/tools/search.js +3 -3
  41. package/src/tools/shell.js +2 -2
  42. package/src/tools/template.js +1 -1
  43. package/src/tools/watch.js +1 -1
  44. package/src/tui/completable-input.js +127 -0
  45. package/src/tui/ink-app.js +324 -0
  46. package/src/tui/ink-components.js +157 -0
  47. package/src/tui/ink-entry.js +86 -0
  48. package/src/tui/renderer.js +137 -19
  49. package/src/tui/status-bar.js +7 -7
@@ -111,9 +111,9 @@ export class MqttClient {
111
111
  for (const r of responses) {
112
112
  if (r.type === 'mcp') { this._send(r); }
113
113
  else if (r.type === 'tool_call') {
114
- log('tool', `🔧 ${r.name}`);
114
+ log('tool', r.name);
115
115
  this.mcp.executeTool(r.name, r.args)
116
- .then(result => { log('tool', result.isError ? '' : ''); this._send({ type: 'mcp', payload: { jsonrpc: '2.0', id: r.id, result } }); })
116
+ .then(result => { log('tool', result.isError ? 'failed' : 'done'); this._send({ type: 'mcp', payload: { jsonrpc: '2.0', id: r.id, result } }); })
117
117
  .catch(err => { this._send({ type: 'mcp', payload: { jsonrpc: '2.0', id: r.id, result: { content: [{ type: 'text', text: err.message }], isError: true } } }); });
118
118
  }
119
119
  }
@@ -124,7 +124,7 @@ export class MqttClient {
124
124
  }
125
125
 
126
126
  _onOther(msg) {
127
- if (msg.type === 'stt') { log('user', `🎤 "${msg.text}"`); bus.emit('stt', msg.text); }
127
+ if (msg.type === 'stt') { log('user', `"${msg.text}"`); bus.emit('stt', msg.text); }
128
128
  if (msg.type === 'llm' && msg.text) { bus.emit('llm:text', msg.text); }
129
129
  if (msg.type === 'tts' && msg.state === 'sentence_start' && msg.text) { bus.emit('tts:sentence', msg.text); }
130
130
  if (msg.type === 'goodbye') { this.sessionId = null; this.mcp.reset(); this._helloQueue = []; }
@@ -138,7 +138,7 @@ export class MqttClient {
138
138
 
139
139
  sendText(text) {
140
140
  this._send({ type: 'listen', state: 'detect', text });
141
- log('user', `💬 "${text}"`);
141
+ log('user', `"${text}"`);
142
142
  bus.emit('user:text', text);
143
143
  }
144
144
 
@@ -76,7 +76,7 @@ export async function handleCommand(cmd, arg, ctx) {
76
76
  case '/history': {
77
77
  const recent = history.slice(-10);
78
78
  if (!recent.length) { log('info', 'No history'); break; }
79
- console.log(`\n${COLORS.bold}📜 Recent:${COLORS.reset}\n`);
79
+ console.log(`\n${COLORS.bold}[LOG] Recent:${COLORS.reset}\n`);
80
80
  for (const h of recent) console.log(` ${COLORS.dim}${new Date(h.ts).toLocaleTimeString()}${COLORS.reset} ${h.text.slice(0, 80)}`);
81
81
  console.log();
82
82
  break;
@@ -84,6 +84,7 @@ export async function handleCommand(cmd, arg, ctx) {
84
84
 
85
85
  case '/doctor': await runDoctor(); break;
86
86
  case '/model': console.log(`\n${formatModelInfo()}\n`); break;
87
+ case '/keys': return 'keys';
87
88
  case '/clear': console.clear(); break;
88
89
  default: log('info', `Unknown: ${cmd}. /help`);
89
90
  }
package/src/repl.js CHANGED
@@ -9,6 +9,8 @@ import { formatToolResult } from './core/formatter.js';
9
9
  import { parseIntent } from './core/intent.js';
10
10
  import { FileTracker } from './core/file-tracker.js';
11
11
  import { handleCommand } from './repl-commands.js';
12
+ import { getCompletions } from './core/completion.js';
13
+ import { formatKeybindings } from './core/keybindings.js';
12
14
  import {
13
15
  renderBanner, renderStatus, renderTools, renderHelp,
14
16
  renderToolCall, renderToolResult, renderAiResponse,
@@ -16,19 +18,21 @@ import {
16
18
  } from './tui/renderer.js';
17
19
 
18
20
  export class REPL {
19
- constructor(registry, mqtt, stats, session) {
21
+ constructor(registry, mqtt, stats, session, extras = {}) {
20
22
  this.registry = registry;
21
23
  this.mqtt = mqtt;
22
24
  this.stats = stats;
23
25
  this.session = session;
24
26
  this.fileTracker = new FileTracker();
27
+ this.thinking = extras.thinking || null;
28
+ this.sender = extras.sender || null;
29
+ this.autoFormat = extras.autoFormat || null;
25
30
  this.rl = null;
26
31
  this.history = [];
27
32
  }
28
33
 
29
34
  start() {
30
35
  const config = getConfig();
31
- renderBanner(config, this.registry.all(), this.mqtt.connected);
32
36
  this._events();
33
37
 
34
38
  if (!process.stdin.isTTY) {
@@ -41,6 +45,10 @@ export class REPL {
41
45
  this.rl = createInterface({
42
46
  input: process.stdin, output: process.stdout,
43
47
  prompt: createPrompt(dirName), historySize: 200,
48
+ completer: (line) => {
49
+ const hits = getCompletions(line);
50
+ return [hits.length ? hits : [], line];
51
+ },
44
52
  });
45
53
  this.rl.prompt();
46
54
  this.rl.on('line', (input) => this._line(input.trim()));
@@ -49,12 +57,15 @@ export class REPL {
49
57
 
50
58
  _events() {
51
59
  bus.on('llm:text', (text) => {
60
+ if (this.thinking) this.thinking.set('idle');
61
+ // Clear any thinking indicator line
62
+ process.stdout.write('\r' + ' '.repeat(60) + '\r');
52
63
  renderAiResponse(text);
53
64
  if (this.rl) this.rl.prompt();
54
65
  });
55
66
 
56
67
  bus.on('tts:sentence', (text) => {
57
- console.log(chalk.dim(` 🔊 ${text}`));
68
+ // TTS is audio output, no need to display text
58
69
  });
59
70
 
60
71
  bus.on('tool:called', ({ name, result, args }) => {
@@ -63,7 +74,7 @@ export class REPL {
63
74
  });
64
75
 
65
76
  bus.on('ready', () => {
66
- console.log(chalk.green(' ✅ XiaoZhi session ready\n'));
77
+ if (this.rl) this.rl.prompt();
67
78
  });
68
79
  }
69
80
 
@@ -75,7 +86,11 @@ export class REPL {
75
86
  const intent = parseIntent(input);
76
87
  if (intent.matched) {
77
88
  renderUserInput(input);
89
+ if (this.thinking) this.thinking.set('tool_call');
78
90
  const result = await this.registry.call(intent.tool, intent.params);
91
+ if (this.thinking) this.thinking.set('idle');
92
+ // Clear thinking indicator
93
+ process.stdout.write('\r' + ' '.repeat(60) + '\r');
79
94
  const formatted = formatToolResult(intent.tool, intent.action, result);
80
95
  console.log(`\n${formatted}\n`);
81
96
  this.history.push({ role: 'user', text: input, ts: Date.now() });
@@ -86,6 +101,7 @@ export class REPL {
86
101
  // Send to XiaoZhi
87
102
  renderUserInput(input);
88
103
  this.history.push({ role: 'user', text: input, ts: Date.now() });
104
+ if (this.thinking) this.thinking.set('thinking');
89
105
  this.mqtt.sendText(input);
90
106
  this.rl?.prompt();
91
107
  }
@@ -93,15 +109,19 @@ export class REPL {
93
109
  async _cmd(input) {
94
110
  const [cmd, ...rest] = input.split(' ');
95
111
  const arg = rest.join(' ');
96
- const ctx = { registry: this.registry, mqtt: this.mqtt, stats: this.stats, session: this.session, fileTracker: this.fileTracker, history: this.history };
112
+ const ctx = {
113
+ registry: this.registry, mqtt: this.mqtt, stats: this.stats,
114
+ session: this.session, fileTracker: this.fileTracker, history: this.history,
115
+ };
97
116
  const special = await handleCommand(cmd, arg, ctx);
98
117
 
99
118
  if (special === 'help') renderHelp();
100
119
  if (special === 'tools') renderTools(this.registry.list());
120
+ if (special === 'keys') console.log(formatKeybindings());
101
121
  if (special === 'status') {
102
122
  const s = this.session.getStats();
103
123
  renderStatus({
104
- 'MQTT': this.mqtt.connected ? ' Connected' : ' Disconnected',
124
+ 'MQTT': this.mqtt.connected ? '[OK] Connected' : '[ERR] Disconnected',
105
125
  'Session': this.mqtt.sessionId || 'none',
106
126
  'Tools': `${this.registry.all().length} (${this.registry.all().reduce((s, t) => s + (t.actions?.length || 0), 0)} actions)`,
107
127
  'Turns': s.turns,
@@ -36,7 +36,7 @@ const ACTIONS = {
36
36
  const config = getConfig();
37
37
  setNested(config, key, value);
38
38
  await saveConfig(config._root, config);
39
- log('config', `⚙️ Set ${key} = ${value}`);
39
+ log('config', `[CFG] Set ${key} = ${value}`);
40
40
  return { key, value, saved: true };
41
41
  },
42
42
  },
package/src/tools/diff.js CHANGED
@@ -6,6 +6,7 @@ import { readFile } from 'fs/promises';
6
6
  import { resolve } from 'path';
7
7
  import { homedir } from 'os';
8
8
  import { log } from '../core/logger.js';
9
+ import { showDiff } from '../core/diff.js';
9
10
 
10
11
  const HOME = homedir();
11
12
 
@@ -50,6 +51,11 @@ const ACTIONS = {
50
51
 
51
52
  return { file1: abs1, file2: abs2, totalChanges: changes.length, hunks: hunks.slice(0, 20) };
52
53
  },
54
+ format: async ({ file1, file2 }) => {
55
+ const c1 = await readFile(file1, 'utf-8').catch(() => '');
56
+ const c2 = await readFile(file2, 'utf-8').catch(() => '');
57
+ if (c1 && c2) showDiff(c1, c2, file1);
58
+ },
53
59
  },
54
60
 
55
61
  stats: {
package/src/tools/file.js CHANGED
@@ -40,7 +40,7 @@ const ACTIONS = {
40
40
  const abs = safe(path);
41
41
  await mkdir(dirname(abs), { recursive: true });
42
42
  await writeFile(abs, content, 'utf-8');
43
- log('file', `📝 Written: ${abs} (${content.length} chars)`);
43
+ log('file', `[EDIT] Written: ${abs} (${content.length} chars)`);
44
44
  return { path: abs, written: content.length };
45
45
  },
46
46
  },
@@ -54,7 +54,7 @@ const ACTIONS = {
54
54
  const count = content.split(old_text).length - 1;
55
55
  content = content.replace(old_text, new_text);
56
56
  await writeFile(abs, content, 'utf-8');
57
- log('file', `✏️ Edited: ${abs} (${count} occurrence(s))`);
57
+ log('file', `[EDIT] Edited: ${abs} (${count} occurrence(s))`);
58
58
  return { path: abs, edited: true, occurrences: count };
59
59
  },
60
60
  },
@@ -80,7 +80,7 @@ const ACTIONS = {
80
80
  const abs = safe(path);
81
81
  if (!existsSync(abs)) throw new Error(`Not found: ${abs}`);
82
82
  await unlink(abs);
83
- log('file', `🗑️ Deleted: ${abs}`);
83
+ log('file', `[DEL] Deleted: ${abs}`);
84
84
  return { path: abs, deleted: true };
85
85
  },
86
86
  },
package/src/tools/git.js CHANGED
@@ -85,7 +85,7 @@ const ACTIONS = {
85
85
  const msg = params.message.replace(/"/g, '\\"');
86
86
  const r = await git(`commit -m "${msg}"`, dir);
87
87
  if (r.code !== 0 && r.stderr.includes('nothing to commit')) return { committed: false, reason: 'nothing to commit' };
88
- log('git', `📝 Committed: ${params.message}`);
88
+ log('git', `[EDIT] Committed: ${params.message}`);
89
89
  return { committed: true, message: params.message, output: r.stdout.slice(-300) };
90
90
  },
91
91
  },
@@ -25,7 +25,7 @@ function httpGet(url, maxChars = 5000) {
25
25
  const ACTIONS = {
26
26
  async fetch({ url, method, body, headers }) {
27
27
  if (!url) throw new Error('url required');
28
- log('network', `🌐 ${method || 'GET'} ${url}`);
28
+ log('network', `[WEB] ${method || 'GET'} ${url}`);
29
29
  const result = await httpGet(url);
30
30
  return { url, status: result.status, body: result.body.slice(0, 5000) };
31
31
  },
@@ -57,7 +57,7 @@ const ACTIONS = {
57
57
  const type = await detectProject(dir);
58
58
  const cmd = BUILD_CMD[type];
59
59
  if (!cmd) throw new Error(`No build for ${type}`);
60
- log('project', `🔨 Building (${type})...`);
60
+ log('project', `[BUILD] Building (${type})...`);
61
61
  const r = await run(cmd, dir);
62
62
  return { success: r.code === 0, type, exitCode: r.code, output: r.stdout.slice(-500) || r.stderr.slice(-500) };
63
63
  },
@@ -70,7 +70,7 @@ const ACTIONS = {
70
70
  const type = await detectProject(dir);
71
71
  const cmd = TEST_CMD[type];
72
72
  if (!cmd) throw new Error(`No test for ${type}`);
73
- log('project', `🧪 Testing (${type})...`);
73
+ log('project', `[TEST] Testing (${type})...`);
74
74
  const r = await run(cmd, dir);
75
75
  return { passed: r.code === 0, type, exitCode: r.code, output: r.stdout.slice(-1000) || r.stderr.slice(-1000) };
76
76
  },
@@ -34,7 +34,7 @@ const ACTIONS = {
34
34
  required: ['pattern'],
35
35
  handler: async ({ pattern, path, ignoreCase, include, context }) => {
36
36
  if (!pattern) throw new Error('pattern required');
37
- log('search', `🔍 grep: ${pattern}`);
37
+ log('search', `[FIND] grep: ${pattern}`);
38
38
  const flags = ignoreCase ? '-rn' : '-rn';
39
39
  const ic = ignoreCase ? 'i' : '';
40
40
  const inc = include ? `--include="*.${include}"` : '';
@@ -49,7 +49,7 @@ const ACTIONS = {
49
49
  find: {
50
50
  required: ['name'],
51
51
  handler: async ({ name, path, type }) => {
52
- log('search', `🔍 find: ${name}`);
52
+ log('search', `[FIND] find: ${name}`);
53
53
  const t = type ? `-type ${type}` : '';
54
54
  const cmd = `find "${path || '.'}" ${t} -name "${name}" 2>/dev/null | head -50`;
55
55
  const r = await runCmd(cmd);
@@ -60,7 +60,7 @@ const ACTIONS = {
60
60
  web: {
61
61
  required: ['url'],
62
62
  handler: async ({ url, maxChars }) => {
63
- log('search', `🌐 fetch: ${url}`);
63
+ log('search', `[WEB] fetch: ${url}`);
64
64
  const r = await httpGet(url, maxChars || 8000);
65
65
  return { url, status: r.status, body: r.body.slice(0, maxChars || 8000), length: r.body.length };
66
66
  },
@@ -26,7 +26,7 @@ const ACTIONS = {
26
26
  exec: {
27
27
  required: ['command'],
28
28
  handler: async ({ command, timeout }) => {
29
- log('shell', `⚙️ $ ${command}`);
29
+ log('shell', `[CFG] $ ${command}`);
30
30
  const result = await run(command, timeout);
31
31
  const maxLen = getConfig().tools?.max_output || 10000;
32
32
  return { stdout: result.stdout.slice(0, maxLen), stderr: result.stderr.slice(0, 2000), exitCode: result.code };
@@ -36,7 +36,7 @@ const ACTIONS = {
36
36
  bg: {
37
37
  required: ['command'],
38
38
  handler: async ({ command }) => {
39
- log('shell', `⚙️ [bg] $ ${command}`);
39
+ log('shell', `[CFG] [bg] $ ${command}`);
40
40
  const child = spawn('bash', ['-c', command], { detached: true, stdio: 'ignore' });
41
41
  child.unref();
42
42
  return { pid: child.pid, background: true, command };
@@ -71,7 +71,7 @@ const ACTIONS = {
71
71
  for (const [file, content] of Object.entries(files)) {
72
72
  await writeFile(join(dir, file), content);
73
73
  }
74
- log('template', `📁 Created ${template}: ${dir}`);
74
+ log('template', `[DIR] Created ${template}: ${dir}`);
75
75
  return { created: true, template, path: dir, files: Object.keys(files) };
76
76
  },
77
77
  },
@@ -26,7 +26,7 @@ const ACTIONS = {
26
26
  const watcher = fsWatch(abs, { recursive: recursive !== false }, (eventType, filename) => {
27
27
  if (filter && !filename.includes(filter)) return;
28
28
  const event = { type: eventType, file: filename, path: abs, ts: Date.now() };
29
- log('watch', `📁 ${eventType}: ${filename}`);
29
+ log('watch', `[DIR] ${eventType}: ${filename}`);
30
30
  bus.emit('file:change', event);
31
31
  });
32
32
 
@@ -0,0 +1,127 @@
1
+ /**
2
+ * CompletableInput — TextInput with Tab auto-completion
3
+ * Wraps ink-text-input and adds completion support
4
+ */
5
+ import React, { useState, useCallback } from 'react';
6
+ import { Box, Text, useInput } from 'ink';
7
+ import TextInput from 'ink-text-input';
8
+ import { getCompletions } from '../core/completion.js';
9
+
10
+ export function CompletableInput({ onSubmit, placeholder }) {
11
+ const [value, setValue] = useState('');
12
+ const [completions, setCompletions] = useState([]);
13
+ const [completionIndex, setCompletionIndex] = useState(0);
14
+ const [showCompletions, setShowCompletions] = useState(false);
15
+
16
+ // Handle Tab for completion
17
+ useInput((input, key) => {
18
+ if (key.tab) {
19
+ if (!showCompletions) {
20
+ // First Tab: show completions
21
+ const hits = getCompletions(value);
22
+ if (hits.length > 0) {
23
+ setCompletions(hits);
24
+ setCompletionIndex(0);
25
+ setShowCompletions(true);
26
+ // Auto-fill first match
27
+ if (hits.length === 1) {
28
+ setValue(hits[0] + ' ');
29
+ setShowCompletions(false);
30
+ } else {
31
+ setValue(hits[0]);
32
+ }
33
+ }
34
+ } else {
35
+ // Subsequent Tab: cycle through completions
36
+ const next = (completionIndex + 1) % completions.length;
37
+ setCompletionIndex(next);
38
+ setValue(completions[next]);
39
+ }
40
+ } else if (key.escape) {
41
+ // Escape: hide completions
42
+ setShowCompletions(false);
43
+ setCompletions([]);
44
+ setCompletionIndex(0);
45
+ } else if (key.return) {
46
+ // Enter: handled by TextInput onSubmit, do nothing here
47
+ setShowCompletions(false);
48
+ setCompletions([]);
49
+ } else {
50
+ // Any other key: reset completions
51
+ if (showCompletions) {
52
+ setShowCompletions(false);
53
+ setCompletions([]);
54
+ setCompletionIndex(0);
55
+ }
56
+ }
57
+ });
58
+
59
+ // Update value from TextInput (but not on Tab/Enter which are handled above)
60
+ const handleChange = useCallback((val) => {
61
+ setValue(val);
62
+ // Live completion hints
63
+ if (val.length > 0) {
64
+ const hits = getCompletions(val);
65
+ if (hits.length > 0 && hits[0] !== val) {
66
+ setCompletions(hits);
67
+ setShowCompletions(true);
68
+ setCompletionIndex(0);
69
+ } else {
70
+ setShowCompletions(false);
71
+ }
72
+ } else {
73
+ setShowCompletions(false);
74
+ }
75
+ }, []);
76
+
77
+ // Handle TextInput submit (Enter key)
78
+ const handleSubmit = useCallback((val) => {
79
+ const trimmed = val.trim();
80
+ if (trimmed) {
81
+ onSubmit(trimmed);
82
+ setValue('');
83
+ }
84
+ setShowCompletions(false);
85
+ setCompletions([]);
86
+ }, [onSubmit]);
87
+
88
+ return React.createElement(Box, { flexDirection: 'column', width: '100%' },
89
+ // Completion suggestions
90
+ showCompletions && completions.length > 1
91
+ ? React.createElement(Box, {
92
+ paddingLeft: 2, paddingBottom: 0,
93
+ flexDirection: 'row', flexWrap: 'wrap', gap: 1,
94
+ },
95
+ completions.slice(0, 8).map((c, i) =>
96
+ React.createElement(Text, {
97
+ key: c,
98
+ color: i === completionIndex ? 'cyan' : 'dim',
99
+ bold: i === completionIndex,
100
+ dimColor: i !== completionIndex,
101
+ },
102
+ i === completionIndex ? `[${c}]` : ` ${c} `
103
+ )
104
+ ),
105
+ )
106
+ : null,
107
+
108
+ // Input line
109
+ React.createElement(Box, { paddingX: 1 },
110
+ React.createElement(Text, { color: 'cyan' }, '> '),
111
+ React.createElement(TextInput, {
112
+ value,
113
+ onChange: handleChange,
114
+ onSubmit: handleSubmit,
115
+ placeholder: placeholder || 'type a message...',
116
+ // Don't show cursor when we have a completion suggestion
117
+ showCursor: !showCompletions || completions.length <= 1,
118
+ }),
119
+ // Show ghost completion (the remaining part of the suggested completion)
120
+ showCompletions && completions.length > 0 && value.length > 0
121
+ ? React.createElement(Text, { dimColor: true },
122
+ completions[completionIndex]?.slice(value.length) || ''
123
+ )
124
+ : null,
125
+ ),
126
+ );
127
+ }