jaz-clio 4.35.1 → 4.35.3

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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-api
3
- version: 4.35.1
3
+ version: 4.35.3
4
4
  description: >-
5
5
  Use this skill whenever you call, debug, or review code that touches the Jaz
6
6
  REST API. Covers field names, response shapes, 117 production gotchas, error
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-cli
3
- version: 4.35.1
3
+ version: 4.35.3
4
4
  description: >-
5
5
  Use this skill when running Clio CLI commands, building shell scripts with
6
6
  Clio, debugging auth issues, understanding --json output, paginating results,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-conversion
3
- version: 4.35.1
3
+ version: 4.35.3
4
4
  description: >-
5
5
  Use this skill when migrating accounting data into Jaz — importing from Xero,
6
6
  QuickBooks, Sage, MYOB, or Excel exports. Covers the full conversion pipeline:
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-jobs
3
- version: 4.35.1
3
+ version: 4.35.3
4
4
  description: >-
5
5
  Use this skill for recurring accounting workflows — month/quarter/year-end
6
6
  close, bank reconciliation, GST/VAT filing, payment runs, credit control,
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: jaz-recipes
3
- version: 4.35.1
3
+ version: 4.35.3
4
4
  description: >-
5
5
  Use this skill when modeling complex multi-step accounting transactions —
6
6
  anything that spans multiple periods, involves changing amounts, or requires
@@ -1,13 +1,6 @@
1
- /**
2
- * Interactive command picker for bare `clio` and bare `clio <group>` invocations.
3
- *
4
- * Uses `@clack/prompts` select with numbered choices and inline category tags.
5
- * Activates only in TTY terminals — non-interactive usage (pipes, --json, CI)
6
- * is unaffected.
7
- */
8
- import * as p from '@clack/prompts';
9
- import { muted } from './ui/theme.js';
10
- // ── Category map (keyed by top-level command name) ──────────────────
1
+ import { showPicker, isCancel } from './ui/picker.js';
2
+ import { accent, muted } from './ui/theme.js';
3
+ // ── Category map ────────────────────────────────────────────────────
11
4
  const CATEGORIES = {
12
5
  invoices: 'AR',
13
6
  'customer-credit-notes': 'AR',
@@ -50,9 +43,21 @@ const CATEGORY_ORDER = [
50
43
  'AR', 'AP', 'Acctg', 'Banking', 'Jobs', 'Calc', 'Recipes',
51
44
  'Reports', 'Data', 'AI', 'Setup',
52
45
  ];
53
- /** Commands excluded from the picker. */
46
+ const CATEGORY_HINTS = {
47
+ AR: 'Invoices, credit notes, payments',
48
+ AP: 'Bills, supplier credit notes',
49
+ Acctg: 'Journals, cash in/out/transfer',
50
+ Banking: 'Bank, reconciliation, cashflow',
51
+ Jobs: 'Month-end, recon, tax filing',
52
+ Calc: 'Loan, lease, depreciation, ECL',
53
+ Recipes: 'Capsule transactions (IFRS)',
54
+ Reports: 'TB, P&L, balance sheet, exports',
55
+ Data: 'Contacts, accounts, items, tags',
56
+ AI: 'Magic extraction, help center',
57
+ Setup: 'Auth, org, init, update',
58
+ };
54
59
  const SKIP = new Set(['serve', 'mcp', 'help']);
55
- // ── Collect leaf commands recursively ───────────────────────────────
60
+ // ── Collect leaf commands ───────────────────────────────────────────
56
61
  function collectLeaves(commands, prefix, category) {
57
62
  const leaves = [];
58
63
  for (const cmd of commands) {
@@ -64,11 +69,8 @@ function collectLeaves(commands, prefix, category) {
64
69
  const subs = cmd.commands.filter(c => !SKIP.has(c.name()));
65
70
  const hasOwnAction = cmd._actionHandler != null;
66
71
  if (subs.length > 0) {
67
- // Commands with own action + subcommands (e.g. jobs bank-recon)
68
- // appear as both a leaf AND parent
69
- if (hasOwnAction) {
72
+ if (hasOwnAction)
70
73
  leaves.push({ path, description: cmd.description(), category: cat });
71
- }
72
74
  leaves.push(...collectLeaves(subs, path, cat));
73
75
  }
74
76
  else {
@@ -77,28 +79,9 @@ function collectLeaves(commands, prefix, category) {
77
79
  }
78
80
  return leaves;
79
81
  }
80
- // ── Build select options ───────────────────────────────────────────
81
- function buildOptions(leaves) {
82
- // Sort by category order, then alphabetically within category
83
- const sorted = [...leaves].sort((a, b) => {
84
- const idxA = CATEGORY_ORDER.indexOf(a.category);
85
- const idxB = CATEGORY_ORDER.indexOf(b.category);
86
- const catA = idxA === -1 ? 999 : idxA;
87
- const catB = idxB === -1 ? 999 : idxB;
88
- if (catA !== catB)
89
- return catA - catB;
90
- return a.path.localeCompare(b.path);
91
- });
92
- const maxPath = Math.min(Math.max(...sorted.map(l => l.path.length)), 38);
93
- return sorted.map((leaf, i) => {
94
- const num = String(i + 1).padStart(3);
95
- const cmd = leaf.path.padEnd(maxPath);
96
- return {
97
- label: `${num}. ${cmd}`,
98
- value: leaf.path,
99
- hint: `${leaf.description} [${leaf.category}]`,
100
- };
101
- });
82
+ function truncateDesc(desc, max) {
83
+ const short = desc.split(/[.;—]/).shift()?.trim() ?? desc;
84
+ return short.length <= max ? short : short.slice(0, max - 1) + '…';
102
85
  }
103
86
  // ── TTY guard ───────────────────────────────────────────────────────
104
87
  export function shouldShowPicker() {
@@ -111,52 +94,78 @@ export function shouldShowPicker() {
111
94
  // ── Top-level picker ────────────────────────────────────────────────
112
95
  export async function showCommandPicker(program, authInfo) {
113
96
  const leaves = collectLeaves(program.commands, '', '');
114
- const options = buildOptions(leaves);
115
97
  const version = program.version() ?? '';
116
- // Strip control chars / ANSI sequences from untrusted strings
117
98
  const safe = (s) => s.replace(/[\x00-\x1F\x7F]/g, '').replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
118
- // Merged header: version + org info (if authenticated) + help hints
119
- let header = ` Clio v${version}`;
99
+ // Build header
100
+ let headerText = `Clio v${version}`;
120
101
  if (authInfo) {
121
- header += ` · ${safe(authInfo.label)} · ${safe(authInfo.orgName)} (${safe(authInfo.currency)})`;
122
- if (authInfo.otherOrgCount > 0) {
123
- header += ` + ${authInfo.otherOrgCount} other org${authInfo.otherOrgCount > 1 ? 's' : ''}`;
124
- }
102
+ headerText += ` ${muted('·')} ${safe(authInfo.label)} ${muted('·')} ${safe(authInfo.orgName)} (${safe(authInfo.currency)})`;
103
+ if (authInfo.otherOrgCount > 0)
104
+ headerText += ` ${muted(`+${authInfo.otherOrgCount} more`)}`;
105
+ }
106
+ // ── Level 1: Categories ──
107
+ while (true) {
108
+ const categoryItems = CATEGORY_ORDER.map(cat => {
109
+ const count = leaves.filter(l => l.category === cat).length;
110
+ return {
111
+ label: cat,
112
+ value: cat,
113
+ hint: `${String(count).padStart(3)} commands ${CATEGORY_HINTS[cat] ?? ''}`,
114
+ };
115
+ });
116
+ const category = await showPicker({
117
+ header: headerText,
118
+ items: categoryItems,
119
+ });
120
+ if (isCancel(category))
121
+ process.exit(0);
122
+ // ── Level 2: Commands in category ──
123
+ const catLeaves = leaves
124
+ .filter(l => l.category === category)
125
+ .sort((a, b) => a.path.localeCompare(b.path));
126
+ const descBudget = Math.max(20, (process.stdout.columns || 80) - 44);
127
+ const commandItems = [
128
+ { label: '← Back', value: '__back__', hint: 'Return to categories' },
129
+ ...catLeaves.map(leaf => ({
130
+ label: leaf.path,
131
+ value: leaf.path,
132
+ hint: truncateDesc(leaf.description, descBudget),
133
+ })),
134
+ ];
135
+ const selected = await showPicker({
136
+ header: `${accent(String(category))} ${muted('·')} ${catLeaves.length} commands`,
137
+ items: commandItems,
138
+ filterable: true,
139
+ });
140
+ if (isCancel(selected))
141
+ process.exit(0);
142
+ if (selected === '__back__')
143
+ continue;
144
+ return selected;
125
145
  }
126
- header += ` — ${options.length} commands`;
127
- process.stderr.write(muted(header + '\n'));
128
- process.stderr.write(muted(' Type to search, ↑↓ navigate, Enter select, Esc quit\n\n'));
129
- const result = await p.autocomplete({
130
- message: 'clio',
131
- options,
132
- });
133
- if (p.isCancel(result))
134
- process.exit(0);
135
- return result ?? null;
136
146
  }
137
147
  // ── Subcommand picker ───────────────────────────────────────────────
138
148
  async function showSubcommandPicker(parent, parentPath) {
139
149
  const topLevel = parentPath.split(' ')[0];
140
150
  const category = CATEGORIES[topLevel] ?? 'Other';
141
- const leaves = collectLeaves(parent.commands, parentPath, category);
142
- const options = buildOptions(leaves);
143
- process.stderr.write(muted(` ${options.length} subcommands ↑↓ navigate, Enter select, Esc quit\n\n`));
144
- const result = await p.autocomplete({
145
- message: `clio ${parentPath}`,
146
- options,
151
+ const leaves = collectLeaves(parent.commands, parentPath, category)
152
+ .sort((a, b) => a.path.localeCompare(b.path));
153
+ const descBudget = Math.max(20, (process.stdout.columns || 80) - 44);
154
+ const items = leaves.map(leaf => ({
155
+ label: leaf.path,
156
+ value: leaf.path,
157
+ hint: truncateDesc(leaf.description, descBudget),
158
+ }));
159
+ const result = await showPicker({
160
+ header: `clio ${parentPath}`,
161
+ items,
162
+ filterable: items.length > 10,
147
163
  });
148
- if (p.isCancel(result))
164
+ if (isCancel(result))
149
165
  process.exit(0);
150
166
  return result ?? null;
151
167
  }
152
- // ── Attach subcommand pickers to group commands ─────────────────────
153
- /**
154
- * Recursively attach interactive pickers to group commands that have
155
- * subcommands but no default action of their own.
156
- *
157
- * After this, bare `clio invoices` or `clio invoices draft` show a picker
158
- * instead of the Commander help dump.
159
- */
168
+ // ── Attach subcommand pickers ───────────────────────────────────────
160
169
  export function attachSubcommandPickers(program) {
161
170
  function recurse(parent, parentPath) {
162
171
  for (const cmd of parent.commands) {
@@ -176,39 +185,29 @@ export function attachSubcommandPickers(program) {
176
185
  });
177
186
  }
178
187
  else if (process.stdin.isTTY && process.stdout.isTTY) {
179
- // Parent has both own action AND subcommands (e.g. jobs payment-run).
180
- // Wrap action to show disambiguation when invoked bare from picker.
181
188
  const originalAction = cmd._actionHandler;
182
189
  let inDisambiguation = false;
183
190
  cmd._actionHandler = async (...args) => {
184
- // Re-entrancy guard: if we're already in disambiguation (e.g. parseAsync
185
- // re-invoked this handler), run the original action directly.
186
191
  if (inDisambiguation)
187
192
  return originalAction.call(cmd, ...args);
188
193
  inDisambiguation = true;
189
194
  try {
190
- const disambigOptions = [
191
- {
192
- label: `${name} (default blueprint)`,
193
- value: '__default__',
194
- hint: cmd.description(),
195
- },
195
+ const items = [
196
+ { label: `${name} (default)`, value: '__default__', hint: truncateDesc(cmd.description(), 50) },
196
197
  ...subs.map(sub => ({
197
198
  label: `${name} ${sub.name()}`,
198
199
  value: sub.name(),
199
- hint: sub.description(),
200
+ hint: truncateDesc(sub.description(), 50),
200
201
  })),
201
202
  ];
202
- process.stderr.write(muted(`\n ${name} has subcommands pick one:\n\n`));
203
- const action = await p.select({
204
- message: `clio ${path}`,
205
- options: disambigOptions,
203
+ const action = await showPicker({
204
+ header: `clio ${path}`,
205
+ items,
206
206
  });
207
- if (p.isCancel(action))
207
+ if (isCancel(action))
208
208
  process.exit(0);
209
- if (action === '__default__') {
209
+ if (action === '__default__')
210
210
  return originalAction.call(cmd, ...args);
211
- }
212
211
  await program.parseAsync(['node', 'clio', ...path.split(' '), action]);
213
212
  }
214
213
  finally {
@@ -216,7 +215,6 @@ export function attachSubcommandPickers(program) {
216
215
  }
217
216
  };
218
217
  }
219
- // Recurse deeper (invoices > draft, capsules > types, etc.)
220
218
  recurse(cmd, path);
221
219
  }
222
220
  }
@@ -5,3 +5,4 @@ export { formatApiError, formatAuthError, formatGenericError, formatWarning } fr
5
5
  export { renderOrgBanner } from './banner.js';
6
6
  export { createProgress } from './progress.js';
7
7
  export { formatRecord } from './record.js';
8
+ export { showPicker, isCancel } from './picker.js';
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Custom-built interactive picker — branded for Clio.
3
+ *
4
+ * Built with raw readline for full visual control.
5
+ * Features: type-to-filter, arrow navigation, scrollable viewport,
6
+ * rounded borders, brand colors, compact single-line items.
7
+ */
8
+ import readline from 'node:readline';
9
+ import { accent, muted, highlight, subtle, box, sym, INDENT } from './theme.js';
10
+ const CANCEL = Symbol('cancel');
11
+ export function isCancel(value) {
12
+ return value === CANCEL;
13
+ }
14
+ // ── Helpers ──────────────────────────────────────────────────────
15
+ function stripAnsi(str) {
16
+ // eslint-disable-next-line no-control-regex
17
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
18
+ }
19
+ function fuzzyMatch(query, target) {
20
+ const q = query.toLowerCase();
21
+ const t = target.toLowerCase();
22
+ let qi = 0;
23
+ for (let ti = 0; ti < t.length && qi < q.length; ti++) {
24
+ if (t[ti] === q[qi])
25
+ qi++;
26
+ }
27
+ return qi === q.length;
28
+ }
29
+ function truncate(str, max) {
30
+ const plain = stripAnsi(str);
31
+ if (plain.length <= max)
32
+ return str;
33
+ return str.slice(0, max - 1) + '…';
34
+ }
35
+ // ── Render Frame ─────────────────────────────────────────────────
36
+ function renderFrame(header, items, cursor, filter, filterable, maxVisible) {
37
+ const termWidth = process.stdout.columns || 80;
38
+ const contentWidth = Math.min(termWidth - 4, 72);
39
+ const lines = [];
40
+ // Header box
41
+ const hr = box.horizontal.repeat(contentWidth);
42
+ lines.push(muted(`${INDENT}${box.topLeft}${hr}${box.topRight}`));
43
+ const headerPlain = stripAnsi(header);
44
+ const headerPad = Math.max(0, contentWidth - headerPlain.length - 1);
45
+ lines.push(muted(`${INDENT}${box.vertical}`) + ` ${header}${' '.repeat(headerPad)}` + muted(box.vertical));
46
+ lines.push(muted(`${INDENT}${box.bottomLeft}${hr}${box.bottomRight}`));
47
+ // Filter input
48
+ if (filterable) {
49
+ const display = filter ? highlight(filter) : subtle('type to filter...');
50
+ lines.push(`${INDENT} ${muted(sym.pointerSmall)} ${display}`);
51
+ lines.push('');
52
+ }
53
+ else {
54
+ lines.push('');
55
+ }
56
+ // Items viewport
57
+ if (items.length === 0) {
58
+ lines.push(`${INDENT} ${muted('No matching commands')}`);
59
+ }
60
+ else {
61
+ const total = items.length;
62
+ const viewSize = Math.min(maxVisible, total);
63
+ let start = 0;
64
+ if (total > viewSize) {
65
+ start = Math.max(0, cursor - Math.floor(viewSize / 2));
66
+ start = Math.min(start, total - viewSize);
67
+ }
68
+ const end = start + viewSize;
69
+ const visibleItems = items.slice(start, end);
70
+ const maxLabel = Math.min(Math.max(...visibleItems.map(i => stripAnsi(i.label).length), 8), 36);
71
+ const hintBudget = Math.max(10, contentWidth - maxLabel - 8);
72
+ if (start > 0)
73
+ lines.push(`${INDENT} ${muted('↑')}`);
74
+ for (let i = start; i < end; i++) {
75
+ const item = items[i];
76
+ const isSelected = i === cursor;
77
+ const label = stripAnsi(item.label).padEnd(maxLabel);
78
+ const hint = item.hint ? truncate(item.hint, hintBudget) : '';
79
+ if (isSelected) {
80
+ lines.push(`${INDENT}${accent(sym.pointer)} ${highlight(label)} ${muted(hint)}`);
81
+ }
82
+ else {
83
+ lines.push(`${INDENT} ${label} ${subtle(hint)}`);
84
+ }
85
+ }
86
+ if (end < total)
87
+ lines.push(`${INDENT} ${muted('↓')} ${subtle(`+${total - end} more`)}`);
88
+ }
89
+ // Footer
90
+ lines.push('');
91
+ lines.push(`${INDENT}${muted('↑↓')} ${subtle('navigate')} ${muted('·')} ${muted('enter')} ${subtle('select')} ${muted('·')} ${muted('esc')} ${subtle('quit')}`);
92
+ return lines.join('\n');
93
+ }
94
+ // ── Interactive Picker ───────────────────────────────────────────
95
+ export function showPicker(opts) {
96
+ return new Promise((resolve) => {
97
+ const { header, items, filterable = false } = opts;
98
+ const termRows = process.stdout.rows || 24;
99
+ const maxVisible = opts.maxVisible ?? Math.max(8, termRows - 10);
100
+ let filter = '';
101
+ let cursor = 0;
102
+ let prevLineCount = 0;
103
+ const getFiltered = () => {
104
+ if (!filter)
105
+ return items;
106
+ return items.filter(i => fuzzyMatch(filter, i.label) || (i.hint ? fuzzyMatch(filter, i.hint) : false));
107
+ };
108
+ const draw = () => {
109
+ const filtered = getFiltered();
110
+ if (cursor >= filtered.length)
111
+ cursor = Math.max(0, filtered.length - 1);
112
+ const frame = renderFrame(header, filtered, cursor, filter, filterable, maxVisible);
113
+ const frameLines = frame.split('\n').length;
114
+ // Clear previous frame
115
+ if (prevLineCount > 0) {
116
+ process.stdout.write(`\x1b[${prevLineCount}A`); // Move up
117
+ process.stdout.write('\x1b[J'); // Clear to end of screen
118
+ }
119
+ process.stdout.write(frame + '\n');
120
+ prevLineCount = frameLines;
121
+ };
122
+ // Setup readline in raw mode
123
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
124
+ if (process.stdin.isTTY)
125
+ process.stdin.setRawMode(true);
126
+ readline.emitKeypressEvents(process.stdin, rl);
127
+ // Hide cursor
128
+ process.stdout.write('\x1b[?25l');
129
+ const cleanup = () => {
130
+ process.stdout.write('\x1b[?25h'); // Show cursor
131
+ if (process.stdin.isTTY)
132
+ process.stdin.setRawMode(false);
133
+ process.stdin.removeListener('keypress', onKey);
134
+ rl.close();
135
+ };
136
+ const onKey = (_char, key) => {
137
+ const filtered = getFiltered();
138
+ if (key?.name === 'up') {
139
+ cursor = cursor > 0 ? cursor - 1 : filtered.length - 1;
140
+ draw();
141
+ }
142
+ else if (key?.name === 'down') {
143
+ cursor = cursor < filtered.length - 1 ? cursor + 1 : 0;
144
+ draw();
145
+ }
146
+ else if (key?.name === 'return') {
147
+ if (filtered.length > 0) {
148
+ cleanup();
149
+ // Clear the picker output
150
+ if (prevLineCount > 0) {
151
+ process.stdout.write(`\x1b[${prevLineCount}A\x1b[J`);
152
+ }
153
+ resolve(filtered[cursor].value);
154
+ }
155
+ }
156
+ else if (key?.name === 'escape' || (key?.ctrl && key?.name === 'c')) {
157
+ cleanup();
158
+ if (prevLineCount > 0) {
159
+ process.stdout.write(`\x1b[${prevLineCount}A\x1b[J`);
160
+ }
161
+ resolve(CANCEL);
162
+ }
163
+ else if (key?.name === 'backspace') {
164
+ if (filterable && filter.length > 0) {
165
+ filter = filter.slice(0, -1);
166
+ cursor = 0;
167
+ draw();
168
+ }
169
+ }
170
+ else if (filterable && key?.sequence && key.sequence.length === 1 && !key.ctrl) {
171
+ filter += key.sequence;
172
+ cursor = 0;
173
+ draw();
174
+ }
175
+ };
176
+ process.stdin.on('keypress', onKey);
177
+ // Initial draw
178
+ draw();
179
+ });
180
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jaz-clio",
3
- "version": "4.35.1",
3
+ "version": "4.35.3",
4
4
  "description": "Clio — Command Line Interface Orchestrator for Jaz AI.",
5
5
  "type": "module",
6
6
  "bin": {