amicus 3.0.0 → 3.1.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.
Files changed (40) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +43 -0
  3. package/README.md +12 -3
  4. package/electron/setup-ui.js +27 -3
  5. package/package.json +1 -1
  6. package/skills/second-opinion/SKILL.md +2 -0
  7. package/skills/sidecar/SKILL.md +66 -38
  8. package/src/cli-handlers-resume-continue.js +31 -4
  9. package/src/cli-handlers-run.js +15 -4
  10. package/src/cli.js +13 -0
  11. package/src/mcp-server.js +99 -12
  12. package/src/mcp-tools.js +26 -4
  13. package/src/opencode-client.js +18 -2
  14. package/src/sidecar/continue.js +10 -3
  15. package/src/sidecar/fanout-leg.js +26 -1
  16. package/src/sidecar/fanout-output.js +5 -0
  17. package/src/sidecar/fanout-validate.js +81 -0
  18. package/src/sidecar/fanout.js +65 -77
  19. package/src/sidecar/session-utils.js +6 -0
  20. package/src/sidecar/setup.js +2 -1
  21. package/src/utils/alias-resolver.js +6 -35
  22. package/src/utils/api-key-store.js +1 -9
  23. package/src/utils/auth-json.js +1 -1
  24. package/src/utils/config.js +98 -16
  25. package/src/utils/curated-models.js +33 -4
  26. package/src/utils/gateway-router.js +115 -0
  27. package/src/utils/input-validators.js +12 -42
  28. package/src/utils/model-classification.js +65 -0
  29. package/src/utils/model-descriptor.js +72 -0
  30. package/src/utils/model-fetcher.js +35 -9
  31. package/src/utils/model-input-default.js +32 -0
  32. package/src/utils/model-validator.js +68 -84
  33. package/src/utils/provider-registry.js +57 -0
  34. package/src/utils/quick-picks.js +11 -3
  35. package/src/utils/result-schema-rebuild.js +98 -0
  36. package/src/utils/result-schema.js +6 -76
  37. package/src/utils/route-error.js +137 -0
  38. package/src/utils/route-launch.js +179 -0
  39. package/src/utils/start-helpers.js +96 -43
  40. package/src/utils/validators.js +1 -8
@@ -6,71 +6,124 @@
6
6
  */
7
7
 
8
8
  /**
9
- * Resolve model from args: resolve alias or config default.
10
- * Returns { model, alias } or calls process.exit(1) on error.
9
+ * Derive the alias used for resolution (needed downstream by the budget gate,
10
+ * which reports `alias || args.model` as the pricing lookup's modelInput).
11
+ * Per the #61 Task 4.5 design, only ever returns a no-slash token: an
12
+ * explicit `--model` containing '/' or a slash-bearing config default both
13
+ * resolve to undefined here.
11
14
  * @param {object} args - Parsed CLI arguments
12
- * @returns {{ model: string, alias: string|undefined }}
15
+ * @returns {string|undefined}
13
16
  */
14
- function resolveModelFromArgs(args) {
15
- const { resolveModel, loadConfig } = require('./config');
16
- const rawAlias = args.model;
17
- let model;
18
- try {
19
- model = resolveModel(args.model);
20
- } catch (err) {
21
- console.error(err.message);
22
- process.exit(1);
17
+ function deriveAlias(args) {
18
+ const raw = args.model;
19
+ if (raw !== undefined && raw !== null && !raw.includes('/')) {
20
+ return raw;
23
21
  }
24
-
25
- // Determine the alias used (explicit or config default)
26
- let alias = rawAlias;
27
- if (alias === undefined) {
22
+ if (raw === undefined || raw === null) {
23
+ const { loadConfig } = require('./config');
28
24
  const cfg = loadConfig();
29
25
  if (cfg && cfg.default && !cfg.default.includes('/')) {
30
- alias = cfg.default;
26
+ return cfg.default;
31
27
  }
32
28
  }
33
- return { model, alias };
29
+ return undefined;
34
30
  }
35
31
 
36
32
  /**
37
- * Validate models before launch (F3 #18: default-on).
38
- * --no-validate-model opts out; the old opt-in --validate-model is a no-op kept for back-compat.
39
- * Returns the (possibly corrected) model string.
33
+ * Resolve the model for a launch through the Foundation gateway router (#61
34
+ * Task 4.5), used by every launch path start, continue (Task 7.3), and the
35
+ * MCP amicus_start handler (via the same router, Task 6.2). This replaced the
36
+ * legacy resolveModelFromArgs + validateFallbackModel pair, which was retired
37
+ * once no launch path depended on it (#61 Task 4.7).
38
+ *
39
+ * On `resolved`, returns `{ model, alias, gateway, provenance }` (and prints
40
+ * any advisory `notice` to stderr). On `error` or `selection_required`,
41
+ * renders the appropriate message to stderr and exits(1) — this never
42
+ * returns in that case.
40
43
  * @param {object} args - Parsed CLI arguments
41
- * @param {string|undefined} alias - The alias used for resolution
42
- * @returns {Promise<string>} Validated model string
44
+ * @returns {Promise<{model: string, alias: string|undefined, gateway: string, provenance: object}>}
43
45
  */
44
- async function validateFallbackModel(args, alias) {
45
- // F3 #18: validation is default-on. --no-validate-model opts out; the old
46
- // opt-in --validate-model is now a no-op kept for back-compat.
47
- if (args['no-validate-model']) { return args.model; }
46
+ async function resolveLaunchModel(args) {
47
+ const { resolveGatewayMode } = require('./config');
48
+ const { resolveRouteForLaunch } = require('./route-launch');
49
+ const { toCliMessage, toStructuredError } = require('./route-error');
50
+ const { resolveModelInputOrDefault } = require('./model-input-default');
48
51
 
49
- const headless = args['no-ui'] || !process.stdin.isTTY;
50
- const { detectFallback } = require('./config');
52
+ const gatewayMode = resolveGatewayMode(args.gateway);
53
+ const validateModel = !args['no-validate-model'];
54
+ // Interactive alternatives picker (#61 Task 6.3): only offered on a real
55
+ // interactive TTY session that hasn't opted out with --no-ui. A headless/
56
+ // non-TTY run (CI, piped output, --no-ui) keeps allowSelection false, so a
57
+ // direct miss there still produces the structured error/selection_required
58
+ // rendered below rather than an unhandled prompt.
59
+ const allowSelection = !args['no-ui'] && !!process.stdin.isTTY;
60
+
61
+ // No --model given: the parser does not inject a default, so resolve the
62
+ // configured default here before handing off to the router. Without this, `undefined`
63
+ // would reach resolveRouteForLaunch -> parseDescriptor(undefined) -> an
64
+ // `invalid` result, breaking the common `amicus start` (no --model) case.
65
+ // Shared with the MCP amicus_start handler (mcp-server.js, #61 Task 6.2)
66
+ // via model-input-default.js, so this lookup lives in exactly one place.
67
+ const modelInput = resolveModelInputOrDefault(args.model);
68
+ if (modelInput === undefined) {
69
+ process.stderr.write(
70
+ 'No model specified and no default configured. Run \'amicus setup\' to set a default model.\n'
71
+ );
72
+ process.exit(1);
73
+ }
51
74
 
52
- // Direct-API fallback path: keep the provider-API existence check.
53
- if (alias && detectFallback(alias, args.model)) {
54
- const { validateDirectModel } = require('./model-validator');
75
+ const result = await resolveRouteForLaunch({
76
+ model: modelInput,
77
+ gatewayMode,
78
+ source: 'cli',
79
+ allowSelection,
80
+ validateModel,
81
+ });
82
+
83
+ if (result.kind === 'resolved') {
84
+ if (result.notice) {
85
+ process.stderr.write(`${result.notice}\n`);
86
+ }
87
+ return {
88
+ model: result.executableId,
89
+ alias: deriveAlias(args),
90
+ gateway: result.gateway,
91
+ provenance: result.provenance,
92
+ };
93
+ }
94
+
95
+ // Interactive alternatives picker (#61 Task 6.3): only reachable when
96
+ // allowSelection was true above (real TTY, no --no-ui), so the router only
97
+ // ever hands back selection_required here in that same interactive case.
98
+ if (result.kind === 'selection_required') {
99
+ const { promptRouteSelection } = require('./model-validator');
55
100
  try {
56
- return await validateDirectModel(args.model, alias, { headless });
101
+ const chosen = await promptRouteSelection(result, deriveAlias(args));
102
+ return {
103
+ model: chosen.model,
104
+ alias: deriveAlias(args),
105
+ gateway: chosen.gateway,
106
+ provenance: result.provenance || {},
107
+ };
57
108
  } catch (err) {
58
- console.error(err.message);
109
+ process.stderr.write(`${err.message || 'Model selection cancelled.'}\n`);
59
110
  process.exit(1);
60
111
  }
61
112
  }
62
113
 
63
- // OpenRouter (and any) resolved model: validate against the live catalog.
64
- const { validateAgainstCatalog } = require('./model-validator');
65
- try {
66
- return await validateAgainstCatalog(args.model, alias);
67
- } catch (err) {
68
- console.error(err.message);
69
- process.exit(1);
114
+ // 'error': render and exit. (A non-interactive run never reaches
115
+ // kind:'selection_required' allowSelection is false there, so the router
116
+ // resolves a catalog miss straight to kind:'error' with reason
117
+ // 'model_not_found' instead; see gateway-router.js's catalogGate.)
118
+ if (args.json) {
119
+ process.stderr.write(`${JSON.stringify(toStructuredError(result))}\n`);
120
+ } else {
121
+ process.stderr.write(`${toCliMessage(result)}\n`);
70
122
  }
123
+ process.exit(1);
71
124
  }
72
125
 
73
126
  module.exports = {
74
- resolveModelFromArgs,
75
- validateFallbackModel,
127
+ resolveLaunchModel,
128
+ deriveAlias,
76
129
  };
@@ -19,13 +19,7 @@ const VALID_AGENT_MODES = OPENCODE_AGENTS;
19
19
  /**
20
20
  * Provider to API key mapping
21
21
  */
22
- const PROVIDER_KEY_MAP = {
23
- 'openrouter': { key: 'OPENROUTER_API_KEY', name: 'OpenRouter' },
24
- 'google': { key: 'GOOGLE_GENERATIVE_AI_API_KEY', name: 'Google Gemini' },
25
- 'openai': { key: 'OPENAI_API_KEY', name: 'OpenAI' },
26
- 'anthropic': { key: 'ANTHROPIC_API_KEY', name: 'Anthropic' },
27
- 'deepseek': { key: 'DEEPSEEK_API_KEY', name: 'DeepSeek' },
28
- };
22
+ const { PROVIDER_KEY_MAP } = require('./provider-registry');
29
23
 
30
24
  /** Task ID format: alphanumeric, hyphens, underscores, 1-64 chars */
31
25
  const TASK_ID_PATTERN = /^[a-zA-Z0-9_-]{1,64}$/;
@@ -272,5 +266,4 @@ module.exports = {
272
266
  findSessionInProjectDirs,
273
267
  // Re-exported from input-validators.js
274
268
  validateStartInputs: require('./input-validators').validateStartInputs,
275
- findSimilar: require('./input-validators').findSimilar,
276
269
  };