ispbills-icli 8.4.12 → 8.4.13
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/bin/icli.js +4 -24
- package/package.json +1 -1
- package/src/banner.js +1 -3
- package/src/client.js +0 -6
- package/src/clipboard.js +1 -35
- package/src/commands.js +0 -3
- package/src/tui/app.js +27 -171
- package/src/tui/components.js +89 -587
- package/src/tui/composer.js +4 -39
- package/src/tui/markdown.js +3 -19
- package/src/tui/run.js +1 -3
- package/src/tui/theme.js +11 -29
- package/src/logger.js +0 -23
- package/src/resume.js +0 -30
- package/src/shell.js +0 -18
- package/src/version-check.js +0 -43
package/src/tui/composer.js
CHANGED
|
@@ -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 }) {
|
|
24
24
|
const [value, setValue] = useState('');
|
|
25
25
|
const [cursor, setCursor] = useState(0);
|
|
26
26
|
const [history, setHistory] = useState([]);
|
|
@@ -40,18 +40,6 @@ export function Composer({ onSubmit, busy, width = 80, prefill = null, onPrefill
|
|
|
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
|
-
|
|
52
|
-
// Ctrl+S stash: save the current prompt to restore later (GitHub Copilot CLI parity).
|
|
53
|
-
const stashRef = useRef('');
|
|
54
|
-
|
|
55
43
|
// Bracketed-paste assembly. A right-click / Cmd+V paste is wrapped by the
|
|
56
44
|
// terminal in \x1b[200~ … \x1b[201~ and can arrive split across several
|
|
57
45
|
// reads, so we buffer until the end marker before inserting.
|
|
@@ -155,18 +143,6 @@ export function Composer({ onSubmit, busy, width = 80, prefill = null, onPrefill
|
|
|
155
143
|
return;
|
|
156
144
|
}
|
|
157
145
|
|
|
158
|
-
// ── Ctrl+S: stash / pop prompt (GitHub Copilot CLI parity) ──
|
|
159
|
-
if (key.ctrl && input === 's') {
|
|
160
|
-
if (value) {
|
|
161
|
-
stashRef.current = value;
|
|
162
|
-
setValue(''); setCursor(0);
|
|
163
|
-
} else if (stashRef.current) {
|
|
164
|
-
const v = stashRef.current; stashRef.current = '';
|
|
165
|
-
setText(v, v.length);
|
|
166
|
-
}
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
146
|
// ── Menu / history navigation ──
|
|
171
147
|
if (key.upArrow) {
|
|
172
148
|
if (menuOpen) { setSel((i) => Math.max(0, i - 1)); return; }
|
|
@@ -239,15 +215,6 @@ export function Composer({ onSubmit, busy, width = 80, prefill = null, onPrefill
|
|
|
239
215
|
const sugTok = menuOpen && matches[sel] ? (slashMode ? matches[sel].cmd : matches[sel].tag) : '';
|
|
240
216
|
const ghost = sugTok && sugTok.startsWith(token) && cursor >= value.length ? sugTok.slice(token.length) : '';
|
|
241
217
|
|
|
242
|
-
// ── Shell mode: prefix '!' triggers yellow $ prompt + orange border ──
|
|
243
|
-
const shellMode = value.startsWith('!') && !menuOpen;
|
|
244
|
-
const borderColor = busy ? C.gray : shellMode ? C.orange : C.accent;
|
|
245
|
-
const promptGlyph = shellMode ? '$ ' : glyphs.prompt + ' ';
|
|
246
|
-
const promptColor = busy ? C.gray : shellMode ? C.orange : C.accent;
|
|
247
|
-
|
|
248
|
-
// Number of lines in the buffer (for the line-count badge).
|
|
249
|
-
const lineCount = value.split('\n').length;
|
|
250
|
-
|
|
251
218
|
return html`
|
|
252
219
|
<${Box} flexDirection="column" width=${width}>
|
|
253
220
|
${menuOpen
|
|
@@ -273,18 +240,16 @@ export function Composer({ onSubmit, busy, width = 80, prefill = null, onPrefill
|
|
|
273
240
|
<//>`
|
|
274
241
|
: null}
|
|
275
242
|
|
|
276
|
-
<${Box} width=${width} borderStyle=${borderStyle()} borderColor=${
|
|
277
|
-
<${Text} color=${
|
|
243
|
+
<${Box} width=${width} borderStyle=${borderStyle()} borderColor=${busy ? C.gray : C.accent} paddingX=${1}>
|
|
244
|
+
<${Text} color=${busy ? C.gray : C.accent}>${glyphs.prompt} <//>
|
|
278
245
|
${empty
|
|
279
246
|
? html`<${Box} flexGrow=${1}>
|
|
280
247
|
<${Text} color=${C.gray} inverse> <//>
|
|
281
|
-
<${Text} color=${C.gray}>${busy ? 'Working…' : '
|
|
248
|
+
<${Text} color=${C.gray}>${busy ? 'Working…' : 'Ask about your network, or type / for commands'}<//>
|
|
282
249
|
<//>`
|
|
283
250
|
: html`<${Box} flexGrow=${1}>
|
|
284
251
|
<${Text}>${before}<//><${Text} inverse>${at}<//><${Text}>${after}<//><${Text} color=${C.gray}>${ghost}<//>
|
|
285
252
|
<//>`}
|
|
286
|
-
${lineCount > 1
|
|
287
|
-
? html`<${Text} dimColor> ${lineCount}L<//>` : null}
|
|
288
253
|
<//>
|
|
289
254
|
<//>`;
|
|
290
255
|
}
|
package/src/tui/markdown.js
CHANGED
|
@@ -90,30 +90,14 @@ function renderTable(header, rows) {
|
|
|
90
90
|
export function Markdown({ content = '' }) {
|
|
91
91
|
const rows = [];
|
|
92
92
|
let inCode = false;
|
|
93
|
-
let codeLines = [];
|
|
94
93
|
const lines = String(content).replace(/\s+$/, '').split('\n');
|
|
95
94
|
|
|
96
95
|
for (let i = 0; i < lines.length; i++) {
|
|
97
96
|
const line = lines[i].replace(/\t/g, ' ');
|
|
98
97
|
|
|
99
|
-
// Fenced code block
|
|
100
|
-
if (line.trim().startsWith('```')) {
|
|
101
|
-
|
|
102
|
-
inCode = true;
|
|
103
|
-
codeLines = [];
|
|
104
|
-
} else {
|
|
105
|
-
inCode = false;
|
|
106
|
-
const captured = codeLines;
|
|
107
|
-
rows.push(html`
|
|
108
|
-
<${Box} key=${k()} flexDirection="column" marginY=${1}
|
|
109
|
-
borderStyle=${asciiSafe() ? 'single' : 'single'} borderColor=${C.gray}
|
|
110
|
-
borderTop=${false} borderRight=${false} borderBottom=${false} paddingLeft=${1}>
|
|
111
|
-
${captured.map((cl, ci) => html`<${Text} key=${k()} color=${C.green}>${cl}<//>` )}
|
|
112
|
-
<//>`);
|
|
113
|
-
}
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
if (inCode) { codeLines.push(line); continue; }
|
|
98
|
+
// Fenced code block.
|
|
99
|
+
if (line.trim().startsWith('```')) { inCode = !inCode; continue; }
|
|
100
|
+
if (inCode) { rows.push(html`<${Text} key=${k()} color=${C.green}> ${line}<//>`); continue; }
|
|
117
101
|
|
|
118
102
|
// GFM table: header row + separator + body rows.
|
|
119
103
|
if (isTableRow(line) && i + 1 < lines.length && isTableSep(lines[i + 1])) {
|
package/src/tui/run.js
CHANGED
|
@@ -8,7 +8,7 @@ import { render } from 'ink';
|
|
|
8
8
|
import { App } from './app.js';
|
|
9
9
|
|
|
10
10
|
export async function runTui(cfg, opts = {}) {
|
|
11
|
-
const { display = {}, onExternal, initialQuestion
|
|
11
|
+
const { display = {}, onExternal, initialQuestion } = opts;
|
|
12
12
|
let externalAction;
|
|
13
13
|
|
|
14
14
|
const instance = render(
|
|
@@ -16,8 +16,6 @@ export async function runTui(cfg, opts = {}) {
|
|
|
16
16
|
cfg=${cfg}
|
|
17
17
|
display=${display}
|
|
18
18
|
initialQuestion=${initialQuestion}
|
|
19
|
-
version=${version}
|
|
20
|
-
resumeSession=${resumeSession}
|
|
21
19
|
onExternal=${(a) => { externalAction = a; onExternal?.(a); }}
|
|
22
20
|
/>`,
|
|
23
21
|
{ exitOnCtrlC: false },
|
package/src/tui/theme.js
CHANGED
|
@@ -5,36 +5,18 @@ import { glyphs, asciiSafe } from '../ui.js';
|
|
|
5
5
|
|
|
6
6
|
export { glyphs, asciiSafe };
|
|
7
7
|
|
|
8
|
-
// Soft-blue brand accent (approx. xterm-256 colour 111)
|
|
8
|
+
// Soft-blue brand accent (approx. xterm-256 colour 111).
|
|
9
9
|
export const ACCENT = '#87afff';
|
|
10
|
-
|
|
11
|
-
// Semantic color palette. Where possible mirror Copilot CLI conventions:
|
|
12
|
-
// - accent/cyan for interactive / in-flight elements
|
|
13
|
-
// - green for success / completed
|
|
14
|
-
// - red for errors / destructive
|
|
15
|
-
// - yellow for warnings / caution / autopilot
|
|
16
|
-
// - magenta for reasoning/thinking
|
|
17
|
-
// - gray / dimColor for secondary / completed-dim text
|
|
18
10
|
export const C = {
|
|
19
|
-
accent:
|
|
20
|
-
gray:
|
|
21
|
-
white:
|
|
22
|
-
cyan:
|
|
23
|
-
green:
|
|
24
|
-
yellow:
|
|
25
|
-
red:
|
|
11
|
+
accent: ACCENT,
|
|
12
|
+
gray: 'gray',
|
|
13
|
+
white: 'white',
|
|
14
|
+
cyan: 'cyan',
|
|
15
|
+
green: 'green',
|
|
16
|
+
yellow: 'yellow',
|
|
17
|
+
red: 'red',
|
|
26
18
|
magenta: 'magenta',
|
|
27
|
-
blue:
|
|
28
|
-
orange: '#ffb86c', // shell mode / caution
|
|
29
|
-
purple: '#d097ff', // sub-agent / agent role
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// Copilot-style task status glyphs (Unicode only; fall back handled per use-site).
|
|
33
|
-
// ● filled = step completed ◐ half = step in-progress ○ empty = pending
|
|
34
|
-
export const SG = {
|
|
35
|
-
filled: '●', // U+25CF completed
|
|
36
|
-
half: '◐', // U+25D0 in-progress (used alongside spinner)
|
|
37
|
-
empty: '○', // U+25CB pending
|
|
19
|
+
blue: 'blue',
|
|
38
20
|
};
|
|
39
21
|
|
|
40
22
|
// ink-spinner uses cli-spinners; 'line' is pure ASCII (|/-\), 'dots' is braille.
|
|
@@ -48,9 +30,9 @@ export function borderStyle() {
|
|
|
48
30
|
return asciiSafe() ? 'single' : 'round';
|
|
49
31
|
}
|
|
50
32
|
|
|
51
|
-
// Middle-dot separator
|
|
33
|
+
// Middle-dot separator, ASCII-safe on classic Windows consoles.
|
|
52
34
|
export function midDot() {
|
|
53
|
-
return asciiSafe() ? ' - ' : '
|
|
35
|
+
return asciiSafe() ? ' - ' : ' · ';
|
|
54
36
|
}
|
|
55
37
|
|
|
56
38
|
// Em-dash, ASCII-safe on classic Windows consoles.
|
package/src/logger.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
// Append-only log of every prompt + reply that passes through the icli stream.
|
|
2
|
-
// Writes to: ~/.config/ispbills-icli/logs/<userKey>/stream.log
|
|
3
|
-
import { appendFileSync, mkdirSync } from 'fs';
|
|
4
|
-
import { join } from 'path';
|
|
5
|
-
import { userDir } from './userdir.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Append a prompt/reply pair to the stream log for this user.
|
|
9
|
-
* Call once per turn: logStream(cfg, prompt, reply).
|
|
10
|
-
* Silently no-ops on any I/O error.
|
|
11
|
-
*/
|
|
12
|
-
export function logStream(cfg, prompt, reply) {
|
|
13
|
-
try {
|
|
14
|
-
const dir = userDir(cfg, 'logs');
|
|
15
|
-
const file = join(dir, 'stream.log');
|
|
16
|
-
const ts = new Date().toISOString();
|
|
17
|
-
const sep = '─'.repeat(60);
|
|
18
|
-
const entry = `\n${sep}\n[${ts}]\nPROMPT: ${String(prompt).trim()}\nREPLY: ${String(reply).trim()}\n`;
|
|
19
|
-
appendFileSync(file, entry, 'utf8');
|
|
20
|
-
} catch {
|
|
21
|
-
// never crash the app over logging
|
|
22
|
-
}
|
|
23
|
-
}
|
package/src/resume.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Temporary session persistence for the /update hot-reload flow.
|
|
3
|
-
*
|
|
4
|
-
* Before updating, icli serialises the live conversation to a JSON file in
|
|
5
|
-
* /tmp so the new binary can resume right where the user left off.
|
|
6
|
-
*/
|
|
7
|
-
import { writeFileSync, readFileSync, existsSync, unlinkSync } from 'fs';
|
|
8
|
-
import { tmpdir } from 'os';
|
|
9
|
-
import { join } from 'path';
|
|
10
|
-
|
|
11
|
-
const PREFIX = 'icli-resume-';
|
|
12
|
-
|
|
13
|
-
/** Write a resume file and return its path. */
|
|
14
|
-
export function saveResume(convo, plain, items = []) {
|
|
15
|
-
const path = join(tmpdir(), `${PREFIX}${Date.now()}.json`);
|
|
16
|
-
writeFileSync(path, JSON.stringify({ convo, plain, items, savedAt: new Date().toISOString() }), 'utf8');
|
|
17
|
-
return path;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Load and delete a resume file. Returns { convo, plain, items } or null. */
|
|
21
|
-
export function loadResume(path) {
|
|
22
|
-
if (!path || !existsSync(path)) return null;
|
|
23
|
-
try {
|
|
24
|
-
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
25
|
-
try { unlinkSync(path); } catch { /* best-effort delete */ }
|
|
26
|
-
return { convo: data.convo || [], plain: data.plain || [], items: data.items || [] };
|
|
27
|
-
} catch {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
}
|
package/src/shell.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { exec } from 'child_process';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Run a shell command and capture combined stdout/stderr.
|
|
5
|
-
* Returns { ok, stdout, stderr, exitCode }.
|
|
6
|
-
*/
|
|
7
|
-
export function runShell(cmd, timeout = 30_000) {
|
|
8
|
-
return new Promise((resolve) => {
|
|
9
|
-
exec(cmd, { timeout, maxBuffer: 512 * 1024, shell: true }, (err, stdout, stderr) => {
|
|
10
|
-
resolve({
|
|
11
|
-
ok: !err || err.code === 0,
|
|
12
|
-
stdout: (stdout || '').trim(),
|
|
13
|
-
stderr: (stderr || '').trim(),
|
|
14
|
-
exitCode: err?.code ?? 0,
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
}
|
package/src/version-check.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Background version check against npm registry.
|
|
3
|
-
* Caches the result in the session so we only hit npm once per run.
|
|
4
|
-
* Resolves to { latest, isNewer } or null on any error (network, etc.).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const PKG_NAME = 'ispbills-icli';
|
|
8
|
-
const REGISTRY = `https://registry.npmjs.org/${PKG_NAME}/latest`;
|
|
9
|
-
|
|
10
|
-
/** Parse a semver string into [major, minor, patch] ints. */
|
|
11
|
-
function parse(v) {
|
|
12
|
-
const parts = String(v || '').replace(/[^0-9.]/g, '').split('.').map(Number);
|
|
13
|
-
return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Returns true when `remote` is strictly newer than `current`. */
|
|
17
|
-
function isNewer(current, remote) {
|
|
18
|
-
const [cMaj, cMin, cPat] = parse(current);
|
|
19
|
-
const [rMaj, rMin, rPat] = parse(remote);
|
|
20
|
-
if (rMaj !== cMaj) return rMaj > cMaj;
|
|
21
|
-
if (rMin !== cMin) return rMin > cMin;
|
|
22
|
-
return rPat > cPat;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Check npm for a newer version of ispbills-icli.
|
|
27
|
-
* Resolves to { latest: string, isNewer: boolean } or null.
|
|
28
|
-
*/
|
|
29
|
-
export async function checkLatestVersion(currentVersion) {
|
|
30
|
-
try {
|
|
31
|
-
const ctrl = new AbortController();
|
|
32
|
-
const timer = setTimeout(() => ctrl.abort(), 5000);
|
|
33
|
-
const res = await fetch(REGISTRY, { signal: ctrl.signal });
|
|
34
|
-
clearTimeout(timer);
|
|
35
|
-
if (!res.ok) return null;
|
|
36
|
-
const data = await res.json();
|
|
37
|
-
const latest = data.version;
|
|
38
|
-
if (!latest) return null;
|
|
39
|
-
return { latest, isNewer: isNewer(currentVersion, latest) };
|
|
40
|
-
} catch {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
}
|