@parallel-cli/parallel 0.3.3 → 0.4.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.
package/dist/config.js CHANGED
@@ -69,6 +69,76 @@ export const PROVIDER_PRESETS = [
69
69
  models: ['openai/gpt-oss-120b', 'openai/gpt-oss-20b'],
70
70
  defaultModel: 'openai/gpt-oss-120b',
71
71
  },
72
+ {
73
+ name: 'xAI',
74
+ baseUrl: 'https://api.x.ai/v1',
75
+ apiKey: '',
76
+ models: ['grok-4', 'grok-3-beta', 'grok-3-mini'],
77
+ defaultModel: 'grok-3-beta',
78
+ },
79
+ {
80
+ name: 'Perplexity',
81
+ baseUrl: 'https://api.perplexity.ai',
82
+ apiKey: '',
83
+ models: ['sonar-pro', 'sonar', 'sonar-reasoning'],
84
+ defaultModel: 'sonar-pro',
85
+ },
86
+ {
87
+ name: 'Cohere',
88
+ baseUrl: 'https://api.cohere.com/v2',
89
+ apiKey: '',
90
+ models: ['command-a', 'command-r-plus', 'command-r'],
91
+ defaultModel: 'command-a',
92
+ },
93
+ {
94
+ name: 'DeepInfra',
95
+ baseUrl: 'https://api.deepinfra.com/v1/openai',
96
+ apiKey: '',
97
+ models: ['meta-llama/llama-4-maverick', 'deepseek-ai/deepseek-chat', 'microsoft/wizardlm-2-8x22b'],
98
+ defaultModel: 'meta-llama/llama-4-maverick',
99
+ },
100
+ {
101
+ name: 'Fireworks',
102
+ baseUrl: 'https://api.fireworks.ai/inference/v1',
103
+ apiKey: '',
104
+ models: ['llama-4-maverick', 'llama-4-scout', 'mixtral-8x22b'],
105
+ defaultModel: 'llama-4-maverick',
106
+ },
107
+ {
108
+ name: 'Cerebras',
109
+ baseUrl: 'https://api.cerebras.ai/v1',
110
+ apiKey: '',
111
+ models: ['llama-3.3-70b', 'llama-3.1-8b'],
112
+ defaultModel: 'llama-3.3-70b',
113
+ },
114
+ {
115
+ name: 'Novita',
116
+ baseUrl: 'https://api.novita.ai/v3/openai',
117
+ apiKey: '',
118
+ models: ['deepseek-r1', 'deepseek-v3', 'llama-3.1-70b'],
119
+ defaultModel: 'deepseek-v3',
120
+ },
121
+ {
122
+ name: 'Hyperbolic',
123
+ baseUrl: 'https://api.hyperbolic.xyz/v1',
124
+ apiKey: '',
125
+ models: ['deepseek-v3', 'llama-4-maverick', 'qwen3-235b'],
126
+ defaultModel: 'deepseek-v3',
127
+ },
128
+ {
129
+ name: 'SambaNova',
130
+ baseUrl: 'https://api.sambanova.ai/v1',
131
+ apiKey: '',
132
+ models: ['llama-4-maverick', 'llama-4-scout', 'deepseek-r1'],
133
+ defaultModel: 'llama-4-maverick',
134
+ },
135
+ {
136
+ name: 'Ollama',
137
+ baseUrl: 'http://localhost:11434/v1',
138
+ apiKey: '',
139
+ models: ['llama3', 'codellama', 'mistral'],
140
+ defaultModel: 'llama3',
141
+ },
72
142
  ];
73
143
  export const DEFAULTS = {
74
144
  providers: [],
@@ -78,6 +148,13 @@ export const DEFAULTS = {
78
148
  soundEnabled: true,
79
149
  recentFolders: [],
80
150
  };
151
+ function normalizeApprovalMode(mode) {
152
+ if (mode === 'ask' || mode === 'auto-safe' || mode === 'yolo')
153
+ return mode;
154
+ if (mode === 'auto')
155
+ return 'auto-safe';
156
+ return 'ask';
157
+ }
81
158
  export function getProvider(cfg, name) {
82
159
  const n = (name ?? cfg.defaultProvider).toLowerCase();
83
160
  return cfg.providers.find((p) => p.name.toLowerCase() === n) ?? (name ? undefined : cfg.providers[0]);
@@ -117,6 +194,7 @@ export function loadConfig() {
117
194
  cfg = { ...cfg, ...raw };
118
195
  if (!Array.isArray(cfg.providers))
119
196
  cfg.providers = [];
197
+ cfg.approvalMode = normalizeApprovalMode(raw.approvalMode);
120
198
  migrate(raw, cfg);
121
199
  }
122
200
  }
@@ -146,6 +224,7 @@ export function loadConfig() {
146
224
  }
147
225
  if (!Array.isArray(cfg.recentFolders))
148
226
  cfg.recentFolders = [];
227
+ cfg.approvalMode = normalizeApprovalMode(cfg.approvalMode);
149
228
  return cfg;
150
229
  }
151
230
  export function saveConfig(cfg) {
@@ -10,6 +10,35 @@ import { priceFor, fmtCost } from './pricing.js';
10
10
  import { loadSkills, loadSpecialists } from './skills.js';
11
11
  import { t } from './i18n.js';
12
12
  const AGENT_COLORS = ['cyan', 'magenta', 'yellow', 'green', 'blue', 'redBright', 'cyanBright', 'magentaBright'];
13
+ export function normalizeShellApprovalMode(mode) {
14
+ if (mode === 'ask' || mode === 'auto-safe' || mode === 'yolo')
15
+ return mode;
16
+ if (mode === 'auto')
17
+ return 'auto-safe';
18
+ return null;
19
+ }
20
+ export function isRiskyCommand(command) {
21
+ const c = command.toLowerCase();
22
+ if (/\b(sudo|su|dd|mkfs|fdisk|parted)\b/.test(c))
23
+ return true;
24
+ if (/\b(rm|unlink|rmdir)\b/.test(c))
25
+ return true;
26
+ if (/\b(chmod|chown)\s+-[^\s]*r\b/.test(c) || /\b(chmod|chown)\s+.*\s-r\b/.test(c))
27
+ return true;
28
+ if (/\bmv\b.*\s(\/|~|\.\.)/.test(c))
29
+ return true;
30
+ if (/\b(curl|wget)\b.*\|\s*(sh|bash|zsh|python|node)\b/.test(c))
31
+ return true;
32
+ if (/\bgit\s+(reset|clean)\b/.test(c))
33
+ return true;
34
+ if (/\bgit\s+push\b.*(--force|-f)\b/.test(c))
35
+ return true;
36
+ if (/\b(drop|truncate)\s+(table|database|schema)\b/.test(c))
37
+ return true;
38
+ if (/\b(prisma|knex|typeorm|sequelize|rails)\b.*\b(drop|reset|rollback|migrate)\b/.test(c))
39
+ return true;
40
+ return false;
41
+ }
13
42
  /**
14
43
  * The Controller glues everything together: it owns the blackboard, the LLM
15
44
  * clients, the live agents and the approval queue. The UI talks only to it.
@@ -52,7 +81,7 @@ export class Controller extends EventEmitter {
52
81
  this.session = {
53
82
  providerName: p?.name ?? '',
54
83
  model: p?.defaultModel || p?.models[0] || '',
55
- approvalMode: config.approvalMode,
84
+ approvalMode: normalizeShellApprovalMode(config.approvalMode) ?? 'ask',
56
85
  soundEnabled: config.soundEnabled,
57
86
  };
58
87
  this.board.on('update', () => this.emit('update'));
@@ -145,11 +174,13 @@ export class Controller extends EventEmitter {
145
174
  }
146
175
  // ---------- approvals ----------
147
176
  requestApproval = (agentId, command) => {
148
- if (this.session.approvalMode === 'auto')
149
- return Promise.resolve(true);
150
177
  const base = command.trim().split(/\s+/)[0];
151
178
  if (this.sessionAllowedCommands.has(base))
152
179
  return Promise.resolve(true);
180
+ if (this.session.approvalMode === 'yolo')
181
+ return Promise.resolve(true);
182
+ if (this.session.approvalMode === 'auto-safe' && !isRiskyCommand(command))
183
+ return Promise.resolve(true);
153
184
  return new Promise((resolve) => {
154
185
  const agent = this.board.agents.get(agentId);
155
186
  this.approvals.push({
@@ -223,7 +254,7 @@ export class Controller extends EventEmitter {
223
254
  }
224
255
  }
225
256
  /** Launch agent N+1 — works at any time, even while others are running. */
226
- spawnAgent(task, name, modelSpec, images, specialistName, initialHistory) {
257
+ spawnAgent(task, name, modelSpec, images, specialistName, initialHistory, mode = 'task') {
227
258
  // Specialist persona: role appended to the system prompt, may pin a model.
228
259
  let specialist;
229
260
  if (specialistName) {
@@ -263,6 +294,7 @@ export class Controller extends EventEmitter {
263
294
  alias,
264
295
  color,
265
296
  task,
297
+ mode,
266
298
  model: resolved.model,
267
299
  llm: this.llmFor(resolved.provider, resolved.model),
268
300
  board: this.board,
@@ -364,7 +396,7 @@ export class Controller extends EventEmitter {
364
396
  }
365
397
  if (history.length === 0)
366
398
  return 'no-conversation';
367
- return this.spawnAgent(sa.task, sa.name, sa.model, undefined, undefined, history);
399
+ return this.spawnAgent(sa.task, sa.name, sa.model, undefined, undefined, history, sa.mode ?? 'task');
368
400
  }
369
401
  pauseAgent(name) {
370
402
  const a = this.findAgent(name);
@@ -562,6 +594,7 @@ export class Controller extends EventEmitter {
562
594
  agents: [...this.board.agents.values()].map((a) => ({
563
595
  name: a.name,
564
596
  task: a.task,
597
+ mode: a.mode,
565
598
  state: a.state,
566
599
  lastResult: a.lastResult,
567
600
  steps: a.steps,
@@ -685,6 +718,26 @@ export class Controller extends EventEmitter {
685
718
  this.emit('update');
686
719
  return true;
687
720
  }
721
+ /** Remove a provider by name. Clears the default if it was the removed one. */
722
+ removeProvider(name) {
723
+ const idx = this.config.providers.findIndex(p => p.name.toLowerCase() === name.toLowerCase());
724
+ if (idx < 0)
725
+ return false;
726
+ this.config.providers.splice(idx, 1);
727
+ if (this.config.defaultProvider.toLowerCase() === name.toLowerCase()) {
728
+ this.config.defaultProvider = this.config.providers[0]?.name ?? '';
729
+ }
730
+ // If the session was using the removed provider, reset it
731
+ if (this.session.providerName.toLowerCase() === name.toLowerCase()) {
732
+ const fallback = this.config.providers[0];
733
+ this.session.providerName = fallback?.name ?? '';
734
+ this.session.model = fallback?.defaultModel ?? '';
735
+ }
736
+ saveConfig(this.config);
737
+ this.llmCache.clear();
738
+ this.emit('update');
739
+ return true;
740
+ }
688
741
  setGlobalApprovalMode(mode) {
689
742
  this.config.approvalMode = mode;
690
743
  saveConfig(this.config);