ispbills-icli 8.4.5 → 8.4.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ispbills-icli",
3
- "version": "8.4.5",
3
+ "version": "8.4.7",
4
4
  "description": "iCli — IspBills AI network engineer in your terminal",
5
5
  "keywords": [
6
6
  "ispbills",
package/src/clipboard.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Cross-platform "copy to system clipboard" helper. Best-effort: resolves to
2
2
  // false (rather than throwing) when no clipboard tool is available so the TUI
3
3
  // can show a friendly notice.
4
- import { spawn } from 'child_process';
4
+ import { spawn, execFile } from 'child_process';
5
5
 
6
6
  function candidates() {
7
7
  if (process.platform === 'darwin') return [['pbcopy', []]];
@@ -40,3 +40,37 @@ export async function copyToClipboard(text) {
40
40
  }
41
41
  return null;
42
42
  }
43
+
44
+ // ── Read from clipboard ─────────────────────────────────────────────────────
45
+
46
+ function readCandidates() {
47
+ if (process.platform === 'darwin') return [['pbpaste', []]];
48
+ if (process.platform === 'win32') return [['powershell', ['-noprofile', '-command', 'Get-Clipboard']]];
49
+ return [
50
+ ['wl-paste', ['--no-newline']],
51
+ ['xclip', ['-selection', 'clipboard', '-o']],
52
+ ['xsel', ['--clipboard', '--output']],
53
+ ];
54
+ }
55
+
56
+ /**
57
+ * Read text from the system clipboard.
58
+ * Returns the clipboard string on success, or null when no tool is available
59
+ * or the clipboard is empty.
60
+ */
61
+ export function readFromClipboard() {
62
+ return new Promise((resolve) => {
63
+ const cands = readCandidates();
64
+ let i = 0;
65
+ function tryNext() {
66
+ if (i >= cands.length) { resolve(null); return; }
67
+ const [cmd, args] = cands[i++];
68
+ execFile(cmd, args, { timeout: 3000, maxBuffer: 1024 * 512 }, (err, stdout) => {
69
+ if (err) { tryNext(); return; }
70
+ const text = stdout.trimEnd();
71
+ resolve(text || null);
72
+ });
73
+ }
74
+ tryNext();
75
+ });
76
+ }
package/src/commands.js CHANGED
@@ -56,6 +56,8 @@ export const SLASH_COMMANDS = [
56
56
  { cmd: '/autopilot', hint: '', desc: 'Toggle autopilot (auto-apply fixes)', group: 'Session', local: true },
57
57
  { cmd: '/model', hint: '[name]', desc: 'Show or set the AI model', group: 'Session', local: true },
58
58
  { cmd: '/reasoning', hint: '', desc: 'Toggle reasoning display (Ctrl+T)', group: 'Session', local: true },
59
+ { cmd: '/paste', hint: '', desc: 'Paste multiline text from clipboard into input', group: 'Session', local: true },
60
+ { cmd: '/about', hint: '', desc: 'About iCopilot — version, server, user', group: 'Session', local: true },
59
61
  { cmd: '/new', hint: '', desc: 'Start a fresh conversation', group: 'Session', local: true },
60
62
  { cmd: '/history', hint: '', desc: 'Show conversation history', group: 'Session', local: true },
61
63
  { cmd: '/clear', hint: '', desc: 'Clear the screen', group: 'Session', local: true },
package/src/tui/app.js CHANGED
@@ -16,7 +16,7 @@ import { slashToQuestion, SLASH_COMMANDS } from '../commands.js';
16
16
  import { saveConfig } from '../config.js';
17
17
  import { shortModel } from '../ui.js';
18
18
  import { loadNotes, addNote, forgetNote, memoryPreamble } from '../memory.js';
19
- import { copyToClipboard } from '../clipboard.js';
19
+ import { copyToClipboard, readFromClipboard } from '../clipboard.js';
20
20
  import { createGist } from '../gist.js';
21
21
  import { git as gitCmd, commitBackup } from '../gitrepo.js';
22
22
  import { runShell } from '../shell.js';
@@ -66,6 +66,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, ve
66
66
  const [startedAt, setStartedAt] = useState(0);
67
67
  const [choice, setChoice] = useState(null); // { title, note, options, allowText, sel, text, focusText, onPick }
68
68
  const [clearEpoch, setClearEpoch] = useState(0); // bump to remount <Static> on /clear /new
69
+ const [prefillText, setPrefillText] = useState(null); // clipboard paste → pre-fills Composer
69
70
 
70
71
  const convo = useRef([]); // [{role, content}] for the backend
71
72
  const plain = useRef([]); // plain-text transcript for exit reprint
@@ -357,8 +358,39 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, ve
357
358
  }
358
359
 
359
360
  // ── Clipboard ──
360
- if (q === '/copy') {
361
- const ans = lastAnswerRef.current;
361
+ // ── /about ─────────────────────────────────────────────────────────────
362
+ if (q === '/about') {
363
+ const lines = [
364
+ `iCopilot CLI v${version || '?'}`,
365
+ `Server ${cfg.url || '(unknown)'}`,
366
+ `User ${cfg.user?.name || '—'} (${cfg.user?.role || '—'})`,
367
+ `Model ${shortModel(model || cfg._model || '—')}`,
368
+ '',
369
+ 'Type /help for all commands · Ctrl+C to exit',
370
+ 'npm: ispbills-icli · github: lupael/IspBills',
371
+ ];
372
+ push({ type: 'notice', text: lines.join('\n'), color: C.accent });
373
+ return;
374
+ }
375
+
376
+ // ── /paste ─────────────────────────────────────────────────────────────
377
+ if (q === '/paste') {
378
+ push({ type: 'notice', text: `${glyphs.arrow} Reading clipboard…`, color: C.gray });
379
+ readFromClipboard().then((text) => {
380
+ if (!text) {
381
+ push({ type: 'notice', text: `${glyphs.cross} Clipboard is empty or no clipboard tool found.\nInstall xclip / wl-clipboard (Linux), or use macOS/Windows.`, color: C.red });
382
+ return;
383
+ }
384
+ const lines = text.split('\n').length;
385
+ push({ type: 'notice',
386
+ text: `${glyphs.check} Pasted ${lines} line${lines !== 1 ? 's' : ''} (${text.length} chars) into the input box — review and press Enter to send.`,
387
+ color: C.green });
388
+ setPrefillText(text);
389
+ });
390
+ return;
391
+ }
392
+
393
+ if (q === '/copy') { const ans = lastAnswerRef.current;
362
394
  if (!ans) { push({ type: 'notice', text: 'Nothing to copy yet — ask something first.', color: C.gray }); return; }
363
395
  copyToClipboard(ans).then((tool) => push({ type: 'notice',
364
396
  text: tool ? `${glyphs.check} Copied last answer (${ans.length} chars) to the clipboard.`
@@ -545,7 +577,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, ve
545
577
  <${Box} flexDirection="column" marginTop=${1}>
546
578
  ${choice
547
579
  ? html`<${Choice} ...${choice} />`
548
- : html`<${Composer} onSubmit=${onSubmit} busy=${busy} width=${cols} />`}
580
+ : html`<${Composer} onSubmit=${onSubmit} busy=${busy} width=${cols} prefill=${prefillText} onPrefillConsumed=${() => setPrefillText(null)} />`}
549
581
  <${Box} paddingX=${1} width=${cols}>
550
582
  <${Box} flexGrow=${1}>
551
583
  <${Text} dimColor wrap="truncate-end">${ctx}<//>
@@ -20,7 +20,7 @@ const MENTIONS = [
20
20
  { tag: '@mac', desc: 'A MAC address' },
21
21
  ];
22
22
 
23
- export function Composer({ onSubmit, busy, width = 80 }) {
23
+ export function Composer({ onSubmit, busy, width = 80, prefill = null, onPrefillConsumed = null }) {
24
24
  const [value, setValue] = useState('');
25
25
  const [cursor, setCursor] = useState(0);
26
26
  const [history, setHistory] = useState([]);
@@ -40,6 +40,15 @@ export function Composer({ onSubmit, busy, width = 80 }) {
40
40
  lastMut.current = kind;
41
41
  };
42
42
 
43
+ // When app.js injects a /paste prefill, load it into the input box once.
44
+ useEffect(() => {
45
+ if (!prefill) return;
46
+ setValue(prefill);
47
+ setCursor(prefill.length);
48
+ undoRef.current = []; redoRef.current = []; lastMut.current = '';
49
+ onPrefillConsumed?.();
50
+ }, [prefill]); // eslint-disable-line react-hooks/exhaustive-deps
51
+
43
52
  // Ctrl+S stash: save the current prompt to restore later (GitHub Copilot CLI parity).
44
53
  const stashRef = useRef('');
45
54