summon-cli 0.1.0 → 0.3.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.
package/README.md CHANGED
@@ -6,9 +6,9 @@
6
6
 
7
7
  Terminal launcher for local AI CLIs. Run `summon`, pick a tool, launch it.
8
8
 
9
- Supported: Codex CLI, Claude Code, Antigravity CLI, Cursor CLI, GitHub Copilot CLI, opencode CLI. Missing tools show dimmed.
9
+ Built-in: Codex CLI, Claude Code, Antigravity CLI, Cursor CLI, GitHub Copilot CLI, opencode CLI. Missing tools show dimmed. Add your own with `--add`.
10
10
 
11
- > Pre-release (0.1.0).
11
+ > Pre-release (0.2.0).
12
12
 
13
13
  ## Install
14
14
 
@@ -21,14 +21,22 @@ Move with arrows or `j/k` or `1-9`. `Enter` launches, `Esc` quits.
21
21
 
22
22
  ## Commands
23
23
 
24
- - `summon` open the picker (or your default)
25
- - `summon menu` always open the picker
24
+ - `summon` open the picker
26
25
  - `summon reorder` set the order
27
- - `summon default <tool>` launch one directly (`off` clears, no arg = pick)
28
- - `summon alias <name>` add another command name (e.g. `summon alias cli`)
26
+ - `summon default <tool>` start the cursor on it (`off` clears, no arg = pick)
27
+ - `summon alias <name>` install a second shell command name for the launcher
29
28
  - `summon help`
30
29
 
31
- Flag: `--no-logo`. Args after `--` go to the launched tool.
30
+ ## Flags
31
+
32
+ - `--add <cmd> [label]` add a custom CLI to the menu. Example:
33
+ ```sh
34
+ summon --add grok "Grok Build"
35
+ ```
36
+ Adds an entry that runs `grok` and shows as `Grok Build` with hint `Custom provider`.
37
+ - `--remove <id>` remove a custom CLI from the menu.
38
+ - `--no-logo` / `--logo` toggle the side logo (remembered).
39
+ - Args after `--` go to the launched tool.
32
40
 
33
41
  ## Config
34
42
 
package/bin/cli.mjs CHANGED
@@ -23,11 +23,18 @@ const args = rawArgs.filter(arg => !arg.startsWith('-'));
23
23
  const command = args[0];
24
24
 
25
25
  const config = loadConfig();
26
- const logo = flags.has('--no-logo') ? false : (flags.has('--logo') || config.logo);
27
- const items = orderTools(config.order);
26
+ let logo = config.logo;
27
+ if (flags.has('--no-logo')) {
28
+ logo = false;
29
+ saveConfig({logo: false});
30
+ } else if (flags.has('--logo')) {
31
+ logo = true;
32
+ saveConfig({logo: true});
33
+ }
34
+ const items = orderTools(config.order, config.customTools);
28
35
 
29
36
  if (process.env.CLI_LEVEL_SNAPSHOT === '1') {
30
- process.stdout.write(renderSnapshot(Number(process.env.CLI_LEVEL_ACTIVE || 0)));
37
+ process.stdout.write(renderSnapshot(Number(process.env.CLI_LEVEL_ACTIVE || 0), items));
31
38
  process.exit(0);
32
39
  }
33
40
 
@@ -36,6 +43,16 @@ if (flags.has('--help') || flags.has('-h') || command === 'help') {
36
43
  process.exit(0);
37
44
  }
38
45
 
46
+ if (flags.has('--add')) {
47
+ runAdd(args[0], args[1]);
48
+ process.exit(0);
49
+ }
50
+
51
+ if (flags.has('--remove') || flags.has('--rm')) {
52
+ runRemove(args[0]);
53
+ process.exit(0);
54
+ }
55
+
39
56
  const canRenderTui = Boolean(process.stdin.isTTY && process.stdout.isTTY && process.env.TERM !== 'dumb');
40
57
 
41
58
  switch (command) {
@@ -48,29 +65,17 @@ switch (command) {
48
65
  case 'alias':
49
66
  runAlias(args[1]);
50
67
  break;
51
- case 'menu':
52
- await runMenu({forceMenu: true});
53
- break;
54
68
  case undefined:
55
- await runMenu({forceMenu: false});
69
+ await runMenu();
56
70
  break;
57
71
  default:
58
72
  process.stderr.write(`${PROG}: unknown command '${command}'. Try '${PROG} --help'.\n`);
59
73
  process.exit(2);
60
74
  }
61
75
 
62
- async function runMenu({forceMenu}) {
63
- if (!forceMenu && config.default) {
64
- const tool = tools.find(item => item.id === config.default);
65
- if (tool && commandExists(tool.command)) {
66
- process.exitCode = await runCommand(tool.command, forwardedArgs);
67
- return;
68
- }
69
- process.stderr.write(`${PROG}: default '${config.default}' unavailable, opening menu.\n`);
70
- }
71
-
76
+ async function runMenu() {
72
77
  if (!canRenderTui) {
73
- process.stdout.write(renderSnapshot(0));
78
+ process.stdout.write(renderSnapshot(0, items));
74
79
  process.stderr.write(`${PROG}: interactive terminal required for selection.\n`);
75
80
  process.exit(2);
76
81
  }
@@ -91,6 +96,7 @@ async function runReorder() {
91
96
  const order = await new Promise(resolve => {
92
97
  let instance;
93
98
  instance = render(React.createElement(ReorderApp, {
99
+ items,
94
100
  onCancel: () => {
95
101
  instance.unmount();
96
102
  resolve(null);
@@ -114,7 +120,7 @@ async function runReorder() {
114
120
  async function runDefault(target) {
115
121
  if (target === 'off' || target === 'none') {
116
122
  saveConfig({default: null});
117
- process.stdout.write(`Default cleared. '${PROG}' now opens the menu.\n`);
123
+ process.stdout.write(`Default cleared. The cursor starts on the first tool.\n`);
118
124
  return;
119
125
  }
120
126
 
@@ -125,7 +131,7 @@ async function runDefault(target) {
125
131
  process.exit(2);
126
132
  }
127
133
  saveConfig({default: tool.id});
128
- process.stdout.write(`Default set to ${tool.label}. '${PROG}' now launches it directly (use '${PROG} menu' for the picker).\n`);
134
+ process.stdout.write(`Default set to ${tool.label}. The menu now opens with the cursor on it.\n`);
129
135
  return;
130
136
  }
131
137
 
@@ -137,7 +143,49 @@ async function runDefault(target) {
137
143
  return;
138
144
  }
139
145
  saveConfig({default: selected.id});
140
- process.stdout.write(`Default set to ${selected.label}. '${PROG}' now launches it directly (use '${PROG} menu' for the picker).\n`);
146
+ process.stdout.write(`Default set to ${selected.label}. The menu now opens with the cursor on it.\n`);
147
+ }
148
+
149
+ function runAdd(name, label) {
150
+ if (!name || !/^[a-zA-Z0-9._-]+$/.test(name)) {
151
+ process.stderr.write(`${PROG}: usage: ${PROG} --add <command> [label]\n`);
152
+ process.exit(2);
153
+ }
154
+
155
+ const id = name.toLowerCase();
156
+ if (tools.some(t => t.id === id)) {
157
+ process.stderr.write(`${PROG}: '${id}' is a built-in tool already.\n`);
158
+ process.exit(2);
159
+ }
160
+
161
+ const current = loadConfig().customTools || [];
162
+ if (current.some(t => t.id === id)) {
163
+ process.stderr.write(`${PROG}: '${id}' is already in your list.\n`);
164
+ process.exit(2);
165
+ }
166
+
167
+ const labelText = label || (name.charAt(0).toUpperCase() + name.slice(1));
168
+ const next = [...current, {id, label: labelText, command: name, hint: 'Custom provider'}];
169
+ saveConfig({customTools: next});
170
+ process.stdout.write(`Added '${labelText}' (command: ${name}) to the menu.\n`);
171
+ }
172
+
173
+ function runRemove(name) {
174
+ if (!name) {
175
+ process.stderr.write(`${PROG}: usage: ${PROG} --remove <id>\n`);
176
+ process.exit(2);
177
+ }
178
+
179
+ const id = name.toLowerCase();
180
+ const current = loadConfig().customTools || [];
181
+ const next = current.filter(t => t.id !== id);
182
+ if (next.length === current.length) {
183
+ process.stderr.write(`${PROG}: no custom tool '${id}'. List: ${current.map(t => t.id).join(', ') || '(none)'}.\n`);
184
+ process.exit(2);
185
+ }
186
+
187
+ saveConfig({customTools: next});
188
+ process.stdout.write(`Removed '${id}'.\n`);
141
189
  }
142
190
 
143
191
  function runAlias(name) {
@@ -165,6 +213,7 @@ function chooseTool() {
165
213
  instance = render(React.createElement(App, {
166
214
  items,
167
215
  logo,
216
+ initialId: config.default,
168
217
  onCancel: () => {
169
218
  instance.unmount();
170
219
  resolve(null);
@@ -239,16 +288,17 @@ function printHelp() {
239
288
  Summon your AI CLI. A terminal launcher.
240
289
 
241
290
  Commands:
242
- (none) Open the picker, or launch your default if one is set
243
- menu Always open the picker (ignore the default)
291
+ (none) Open the picker
244
292
  reorder Set the order tools appear in
245
- default [tool] Launch <tool> directly on '${PROG}'; no tool = pick one;
246
- 'off' clears it
247
- alias <name> Install a second command name for this launcher
293
+ default [tool] Start the cursor on <tool>; no tool = pick one; 'off' clears
294
+ alias <name> Install a second shell command name for this launcher
248
295
  help Show this help
249
296
 
250
297
  Options:
251
- --no-logo Hide the side logo (shown by default)
298
+ --add <cmd> [lbl] Add a custom CLI to the menu (e.g. --add grok "Grok Build")
299
+ --remove <id> Remove a custom CLI from the menu
300
+ --no-logo Hide the side logo and remember it
301
+ --logo Show the side logo and remember it
252
302
 
253
303
  Anything after -- is passed to the launched tool, e.g. ${PROG} -- --version
254
304
  Config: ${configLocation()}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "summon-cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Summon your AI CLI. Terminal launcher for Codex CLI, Claude Code, Antigravity CLI, Cursor CLI, GitHub Copilot CLI, and opencode CLI.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/app.mjs CHANGED
@@ -149,10 +149,13 @@ function LogoPanel({tool}) {
149
149
  );
150
150
  }
151
151
 
152
- export function App({items = tools, logo = false, onSelect, onCancel}) {
152
+ export function App({items = tools, logo = false, initialId = null, onSelect, onCancel}) {
153
153
  const {exit} = useApp();
154
154
  const {stdout} = useStdout();
155
- const [active, setActive] = useState(0);
155
+ const [active, setActive] = useState(() => {
156
+ const idx = items.findIndex(tool => tool.id === initialId);
157
+ return idx >= 0 ? idx : 0;
158
+ });
156
159
  const [tick, setTick] = useState(0);
157
160
  const [spin, setSpin] = useState(0);
158
161
  const [flash, setFlash] = useState(0);
@@ -246,15 +249,15 @@ export function App({items = tools, logo = false, onSelect, onCancel}) {
246
249
  );
247
250
  }
248
251
 
249
- export function ReorderApp({onDone, onCancel}) {
252
+ export function ReorderApp({items = tools, onDone, onCancel}) {
250
253
  const {exit} = useApp();
251
254
  const [order, setOrder] = useState([]);
252
255
  const [active, setActive] = useState(0);
253
256
 
254
257
  const seekUnpicked = (from, dir, taken) => {
255
- for (let step = 0; step <= tools.length; step += 1) {
256
- const index = (from + dir * step + tools.length * (step + 1)) % tools.length;
257
- if (!taken.has(tools[index].id)) {
258
+ for (let step = 0; step <= items.length; step += 1) {
259
+ const index = (from + dir * step + items.length * (step + 1)) % items.length;
260
+ if (!taken.has(items[index].id)) {
258
261
  return index;
259
262
  }
260
263
  }
@@ -287,13 +290,13 @@ export function ReorderApp({onDone, onCancel}) {
287
290
  }
288
291
 
289
292
  if (key.return) {
290
- const id = tools[active].id;
293
+ const id = items[active].id;
291
294
  if (taken.has(id)) {
292
295
  return;
293
296
  }
294
297
 
295
298
  const next = [...order, id];
296
- if (next.length === tools.length) {
299
+ if (next.length === items.length) {
297
300
  exit();
298
301
  onDone(next);
299
302
  return;
@@ -311,7 +314,7 @@ export function ReorderApp({onDone, onCancel}) {
311
314
  React.createElement(Text, {color: dimGray}, ' pick first to last')
312
315
  ),
313
316
  React.createElement(Box, {flexDirection: 'column', gap: 0},
314
- tools.map((tool, index) => {
317
+ items.map((tool, index) => {
315
318
  const pos = order.indexOf(tool.id);
316
319
  const picked = pos >= 0;
317
320
  const isActive = index === active && !picked;
@@ -506,9 +509,10 @@ function renderPlainLine(tool, index, activeIndex) {
506
509
  return `${arrow} ${label.padEnd(16)} ${tool.hint}`;
507
510
  }
508
511
 
509
- export function renderSnapshot(activeIndex = 0) {
510
- const bounded = Math.max(0, Math.min(tools.length - 1, activeIndex));
511
- return `${tools.map((tool, index) => renderPlainLine(tool, index, bounded)).join('\n')}\n`;
512
+ export function renderSnapshot(activeIndex = 0, items = null) {
513
+ const list = items || tools;
514
+ const bounded = Math.max(0, Math.min(list.length - 1, activeIndex));
515
+ return `${list.map((tool, index) => renderPlainLine(tool, index, bounded)).join('\n')}\n`;
512
516
  }
513
517
 
514
518
  function commandExists(command) {
@@ -542,8 +546,26 @@ function nextAvailable(current, availability, items) {
542
546
  }
543
547
 
544
548
  // Reorder canonical tools by an array of ids; unknown ids ignored, missing appended.
545
- export function orderTools(order = []) {
546
- const byId = new Map(tools.map(tool => [tool.id, tool]));
549
+ export function buildCustomTool({id, label, command, hint, palette} = {}) {
550
+ if (!id) return null;
551
+ return {
552
+ id,
553
+ label: label || id,
554
+ command: command || id,
555
+ hint: hint || 'Custom provider',
556
+ palette: palette && palette.length ? palette : ['#9aa0a6', '#cdcdcd']
557
+ };
558
+ }
559
+
560
+ export function orderTools(order = [], customTools = []) {
561
+ const all = [...tools];
562
+ for (const raw of customTools) {
563
+ const tool = buildCustomTool(raw);
564
+ if (!tool) continue;
565
+ if (all.some(t => t.id === tool.id)) continue;
566
+ all.push(tool);
567
+ }
568
+ const byId = new Map(all.map(tool => [tool.id, tool]));
547
569
  const result = [];
548
570
  for (const id of order) {
549
571
  if (byId.has(id)) {
@@ -551,7 +573,7 @@ export function orderTools(order = []) {
551
573
  byId.delete(id);
552
574
  }
553
575
  }
554
- for (const tool of tools) {
576
+ for (const tool of all) {
555
577
  if (byId.has(tool.id)) {
556
578
  result.push(tool);
557
579
  }
package/src/config.mjs CHANGED
@@ -8,7 +8,8 @@ const configPath = join(baseDir, 'summon-cli', 'config.json');
8
8
  const DEFAULTS = {
9
9
  order: [],
10
10
  logo: true,
11
- default: null
11
+ default: null,
12
+ customTools: []
12
13
  };
13
14
 
14
15
  export function loadConfig() {