@nghyane/arcane 0.1.12 → 0.1.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@nghyane/arcane",
4
- "version": "0.1.12",
4
+ "version": "0.1.13",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/nghyane/arcane",
7
7
  "author": "Can Bölük",
@@ -45,11 +45,11 @@
45
45
  "dependencies": {
46
46
  "@mozilla/readability": "0.6.0",
47
47
  "@nghyane/arcane-stats": "^0.1.8",
48
- "@nghyane/arcane-agent": "^0.1.10",
49
- "@nghyane/arcane-codemode": "^0.1.11",
48
+ "@nghyane/arcane-agent": "^0.1.11",
49
+ "@nghyane/arcane-codemode": "^0.1.12",
50
50
  "@nghyane/arcane-ai": "^0.1.8",
51
51
  "@nghyane/arcane-natives": "^0.1.7",
52
- "@nghyane/arcane-tui": "^0.1.9",
52
+ "@nghyane/arcane-tui": "^0.1.10",
53
53
  "@nghyane/arcane-utils": "^0.1.6",
54
54
  "@sinclair/typebox": "^0.34.48",
55
55
  "@xterm/headless": "^6.0.0",
@@ -47,7 +47,7 @@ export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
47
47
  fast: { tag: "FAST", name: "Fast", color: "warning" },
48
48
  reviewer: { tag: "REVIEW", name: "Reviewer", color: "accent" },
49
49
  oracle: { tag: "ORACLE", name: "Oracle", color: "accent" },
50
- commit: { name: "Commit" },
50
+ commit: { tag: "COMMIT", name: "Commit", color: "dim" },
51
51
  };
52
52
 
53
53
  export const MODEL_ROLE_IDS: ModelRole[] = ["default", "fast", "reviewer", "oracle", "commit"];
@@ -26,13 +26,8 @@ Parameters:
26
26
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
27
27
  try {
28
28
  const apiKey = await findApiKey();
29
- if (!apiKey) {
30
- return {
31
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
32
- details: { error: "EXA_API_KEY not found", toolName: "exa_company" },
33
- };
34
- }
35
- const response = await callExaTool("company_research", params, apiKey);
29
+ // Exa MCP endpoint is publicly accessible; API key is optional
30
+ const response = await callExaTool("company_research_exa", params, apiKey);
36
31
 
37
32
  if (isSearchResponse(response)) {
38
33
  const formatted = formatSearchResults(response);
@@ -26,13 +26,8 @@ Parameters:
26
26
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
27
27
  try {
28
28
  const apiKey = await findApiKey();
29
- if (!apiKey) {
30
- return {
31
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
32
- details: { error: "EXA_API_KEY not found", toolName: "exa_linkedin" },
33
- };
34
- }
35
- const response = await callExaTool("linkedin_search", params, apiKey);
29
+ // Exa MCP endpoint is publicly accessible; API key is optional
30
+ const response = await callExaTool("linkedin_search_exa", params, apiKey);
36
31
 
37
32
  if (isSearchResponse(response)) {
38
33
  const formatted = formatSearchResults(response);
@@ -18,8 +18,11 @@ export function findApiKey(): string | null {
18
18
  }
19
19
 
20
20
  /** Fetch available tools from Exa MCP */
21
- export async function fetchExaTools(apiKey: string, toolNames: string[]): Promise<MCPTool[]> {
22
- const url = `https://mcp.exa.ai/mcp?exaApiKey=${encodeURIComponent(apiKey)}&toolNames=${encodeURIComponent(toolNames.join(","))}`;
21
+ export async function fetchExaTools(apiKey: string | null, toolNames: string[]): Promise<MCPTool[]> {
22
+ const params = new URLSearchParams();
23
+ if (apiKey) params.set("exaApiKey", apiKey);
24
+ params.set("toolNames", toolNames.join(","));
25
+ const url = `https://mcp.exa.ai/mcp?${params.toString()}`;
23
26
  const response = (await callMCP(url, "tools/list")) as MCPToolsResponse;
24
27
 
25
28
  if (response.error) {
@@ -44,8 +47,15 @@ export async function fetchWebsetsTools(apiKey: string): Promise<MCPTool[]> {
44
47
  }
45
48
 
46
49
  /** Call a tool on Exa MCP (simplified: toolName as first arg for easier use) */
47
- export async function callExaTool(toolName: string, args: Record<string, unknown>, apiKey: string): Promise<unknown> {
48
- const url = `https://mcp.exa.ai/mcp?exaApiKey=${encodeURIComponent(apiKey)}&tools=${encodeURIComponent(toolName)}`;
50
+ export async function callExaTool(
51
+ toolName: string,
52
+ args: Record<string, unknown>,
53
+ apiKey: string | null,
54
+ ): Promise<unknown> {
55
+ const params = new URLSearchParams();
56
+ if (apiKey) params.set("exaApiKey", apiKey);
57
+ params.set("tools", toolName);
58
+ const url = `https://mcp.exa.ai/mcp?${params.toString()}`;
49
59
  const response = (await callMCP(url, "tools/call", {
50
60
  name: toolName,
51
61
  arguments: args,
@@ -188,7 +198,7 @@ const mcpSchemaCache = new Map<string, MCPTool>();
188
198
 
189
199
  /** Fetch and cache MCP tool schema */
190
200
  export async function fetchMCPToolSchema(
191
- apiKey: string,
201
+ apiKey: string | null,
192
202
  mcpToolName: string,
193
203
  isWebsetsTool = false,
194
204
  ): Promise<MCPTool | null> {
@@ -198,7 +208,7 @@ export async function fetchMCPToolSchema(
198
208
  }
199
209
 
200
210
  try {
201
- const tools = isWebsetsTool ? await fetchWebsetsTools(apiKey) : await fetchExaTools(apiKey, [mcpToolName]);
211
+ const tools = isWebsetsTool ? await fetchWebsetsTools(apiKey!) : await fetchExaTools(apiKey, [mcpToolName]);
202
212
  const tool = tools.find(t => t.name === mcpToolName);
203
213
  if (tool) {
204
214
  mcpSchemaCache.set(cacheKey, tool);
@@ -238,15 +248,15 @@ export class MCPWrappedTool implements CustomTool<TSchema, ExaRenderDetails> {
238
248
  ): Promise<CustomToolResult<ExaRenderDetails>> {
239
249
  try {
240
250
  const apiKey = await findApiKey();
241
- if (!apiKey) {
251
+ if (!apiKey && this.config.isWebsetsTool) {
242
252
  return {
243
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
244
- details: { error: "EXA_API_KEY not found", toolName: this.config.name },
253
+ content: [{ type: "text" as const, text: "Error: EXA_API_KEY required for Websets tools" }],
254
+ details: { error: "EXA_API_KEY required for Websets tools", toolName: this.config.name },
245
255
  };
246
256
  }
247
257
 
248
258
  const response = this.config.isWebsetsTool
249
- ? await callWebsetsTool(apiKey, this.config.mcpToolName, params as Record<string, unknown>)
259
+ ? await callWebsetsTool(apiKey!, this.config.mcpToolName, params as Record<string, unknown>)
250
260
  : await callExaTool(this.config.mcpToolName, params as Record<string, unknown>, apiKey);
251
261
 
252
262
  if (isSearchResponse(response)) {
@@ -277,7 +287,7 @@ export class MCPWrappedTool implements CustomTool<TSchema, ExaRenderDetails> {
277
287
  * Falls back to provided fallback schema if MCP fetch fails.
278
288
  */
279
289
  export async function createMCPToolFromServer(
280
- apiKey: string,
290
+ apiKey: string | null,
281
291
  config: MCPToolWrapperConfig,
282
292
  fallbackSchema: TSchema,
283
293
  fallbackDescription: string,
@@ -33,12 +33,7 @@ const researcherStartTool: CustomTool<any, ExaRenderDetails> = {
33
33
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
34
34
  try {
35
35
  const apiKey = await findApiKey();
36
- if (!apiKey) {
37
- return {
38
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
39
- details: { error: "EXA_API_KEY not found", toolName: "exa_researcher_start" },
40
- };
41
- }
36
+ // Exa MCP endpoint is publicly accessible; API key is optional
42
37
  const result = await callExaTool("deep_researcher_start", params as Record<string, unknown>, apiKey);
43
38
  return {
44
39
  content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
@@ -65,12 +60,7 @@ const researcherPollTool: CustomTool<any, ExaRenderDetails> = {
65
60
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
66
61
  try {
67
62
  const apiKey = await findApiKey();
68
- if (!apiKey) {
69
- return {
70
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
71
- details: { error: "EXA_API_KEY not found", toolName: "exa_researcher_poll" },
72
- };
73
- }
63
+ // Exa MCP endpoint is publicly accessible; API key is optional
74
64
  const result = await callExaTool("deep_researcher_check", params as Record<string, unknown>, apiKey);
75
65
  return {
76
66
  content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
package/src/exa/search.ts CHANGED
@@ -83,12 +83,7 @@ Parameters:
83
83
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
84
84
  try {
85
85
  const apiKey = await findApiKey();
86
- if (!apiKey) {
87
- return {
88
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
89
- details: { error: "EXA_API_KEY not found", toolName: "exa_search" },
90
- };
91
- }
86
+ // Exa MCP endpoint is publicly accessible; API key is optional
92
87
  const response = await callExaTool("web_search_exa", params, apiKey);
93
88
 
94
89
  if (isSearchResponse(response)) {
@@ -177,12 +172,7 @@ Similar parameters to exa_search, optimized for research depth.`,
177
172
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
178
173
  try {
179
174
  const apiKey = await findApiKey();
180
- if (!apiKey) {
181
- return {
182
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
183
- details: { error: "EXA_API_KEY not found", toolName: "exa_search_deep" },
184
- };
185
- }
175
+ // Exa MCP endpoint is publicly accessible; API key is optional
186
176
  const args = { ...params, type: "deep" };
187
177
  const response = await callExaTool("web_search_exa", args, apiKey);
188
178
 
@@ -232,12 +222,7 @@ Parameters:
232
222
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
233
223
  try {
234
224
  const apiKey = await findApiKey();
235
- if (!apiKey) {
236
- return {
237
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
238
- details: { error: "EXA_API_KEY not found", toolName: "exa_search_code" },
239
- };
240
- }
225
+ // Exa MCP endpoint is publicly accessible; API key is optional
241
226
  const response = await callExaTool("get_code_context_exa", params, apiKey);
242
227
 
243
228
  if (isSearchResponse(response)) {
@@ -292,13 +277,8 @@ Parameters:
292
277
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
293
278
  try {
294
279
  const apiKey = await findApiKey();
295
- if (!apiKey) {
296
- return {
297
- content: [{ type: "text" as const, text: "Error: EXA_API_KEY not found" }],
298
- details: { error: "EXA_API_KEY not found", toolName: "exa_crawl" },
299
- };
300
- }
301
- const response = await callExaTool("crawling", params, apiKey);
280
+ // Exa MCP endpoint is publicly accessible; API key is optional
281
+ const response = await callExaTool("crawling_exa", params, apiKey);
302
282
 
303
283
  if (isSearchResponse(response)) {
304
284
  const formatted = formatSearchResults(response);
package/src/exa/types.ts CHANGED
@@ -122,11 +122,11 @@ export const EXA_TOOL_MAPPINGS = {
122
122
  // Search tools
123
123
  web_search_exa: "exa_search",
124
124
  get_code_context_exa: "exa_search_code",
125
- crawling: "exa_crawl",
125
+ crawling_exa: "exa_crawl",
126
126
  // LinkedIn
127
- linkedin_search: "exa_linkedin",
127
+ linkedin_search_exa: "exa_linkedin",
128
128
  // Company
129
- company_research: "exa_company",
129
+ company_research_exa: "exa_company",
130
130
  // Researcher
131
131
  deep_researcher_start: "exa_researcher_start",
132
132
  deep_researcher_check: "exa_researcher_poll",
@@ -7,6 +7,7 @@ import { Shell } from "@nghyane/arcane-natives";
7
7
  import { Settings } from "../config/settings";
8
8
  import { OutputSink } from "../session/streaming-output";
9
9
  import { getOrCreateSnapshot } from "../utils/shell-snapshot";
10
+ import { NON_INTERACTIVE_ENV } from "./non-interactive-env";
10
11
 
11
12
  export interface BashExecutorOptions {
12
13
  cwd?: string;
@@ -97,7 +98,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
97
98
  {
98
99
  command: finalCommand,
99
100
  cwd: options?.cwd,
100
- env: options?.env,
101
+ env: options?.env ? { ...NON_INTERACTIVE_ENV, ...options.env } : NON_INTERACTIVE_ENV,
101
102
  timeoutMs: options?.timeout,
102
103
  signal,
103
104
  },
@@ -0,0 +1,43 @@
1
+ export const NON_INTERACTIVE_ENV: Readonly<Record<string, string>> = {
2
+ // Disable pagers so commands don't block on interactive views.
3
+ PAGER: "cat",
4
+ GIT_PAGER: "cat",
5
+ MANPAGER: "cat",
6
+ SYSTEMD_PAGER: "cat",
7
+ BAT_PAGER: "cat",
8
+ DELTA_PAGER: "cat",
9
+ GH_PAGER: "cat",
10
+ GLAB_PAGER: "cat",
11
+ PSQL_PAGER: "cat",
12
+ MYSQL_PAGER: "cat",
13
+ AWS_PAGER: "",
14
+ HOMEBREW_PAGER: "cat",
15
+ LESS: "FRX",
16
+ // Disable editor and terminal credential prompts.
17
+ GIT_EDITOR: "true",
18
+ VISUAL: "true",
19
+ EDITOR: "true",
20
+ GIT_TERMINAL_PROMPT: "0",
21
+ SSH_ASKPASS: "/usr/bin/false",
22
+ CI: "1",
23
+ // Package manager defaults for unattended execution.
24
+ npm_config_yes: "true",
25
+ npm_config_update_notifier: "false",
26
+ npm_config_fund: "false",
27
+ npm_config_audit: "false",
28
+ npm_config_progress: "false",
29
+ PNPM_DISABLE_SELF_UPDATE_CHECK: "true",
30
+ PNPM_UPDATE_NOTIFIER: "false",
31
+ YARN_ENABLE_TELEMETRY: "0",
32
+ YARN_ENABLE_PROGRESS_BARS: "0",
33
+ // Cross-language/tooling non-interactive defaults.
34
+ CARGO_TERM_PROGRESS_WHEN: "never",
35
+ DEBIAN_FRONTEND: "noninteractive",
36
+ PIP_NO_INPUT: "1",
37
+ PIP_DISABLE_PIP_VERSION_CHECK: "1",
38
+ TF_INPUT: "0",
39
+ TF_IN_AUTOMATION: "1",
40
+ GH_PROMPT_DISABLED: "1",
41
+ COMPOSER_NO_INTERACTION: "1",
42
+ CLOUDSDK_CORE_DISABLE_PROMPTS: "1",
43
+ };
@@ -298,7 +298,7 @@ class TwoColumnBody implements Component {
298
298
 
299
299
  render(width: number): string[] {
300
300
  const leftWidth = Math.floor(width * 0.5);
301
- const rightWidth = width - leftWidth - 3;
301
+ const rightWidth = Math.max(0, width - leftWidth - 3);
302
302
 
303
303
  const leftLines = this.leftPane.render(leftWidth);
304
304
  const rightLines = this.rightPane.render(rightWidth);
@@ -34,12 +34,17 @@ export class InspectorPanel implements Component {
34
34
  lines.push("");
35
35
 
36
36
  // Description (wrapped)
37
- if (ext.description) {
38
- const wrapped = wrapTextWithAnsi(ext.description, width - 2);
37
+ const desc = ext.description;
38
+ const isValidDescription = typeof desc === "string" && desc.length > 0;
39
+ if (isValidDescription && width > 2) {
40
+ const wrapped = wrapTextWithAnsi(desc, width - 2);
39
41
  for (const line of wrapped) {
40
42
  lines.push(truncateToWidth(line, width));
41
43
  }
42
44
  lines.push("");
45
+ } else if (isValidDescription) {
46
+ lines.push(truncateToWidth(desc, width));
47
+ lines.push("");
43
48
  }
44
49
 
45
50
  // Origin
@@ -663,6 +663,11 @@ export class AuthStorage {
663
663
  let credentials: OAuthCredentials;
664
664
  const saveApiKeyCredential = async (apiKey: string): Promise<void> => {
665
665
  const newCredential: ApiKeyCredential = { type: "api_key", key: apiKey };
666
+ const shouldReplaceExisting = provider === "minimax-code" || provider === "minimax-code-cn";
667
+ if (shouldReplaceExisting) {
668
+ await this.set(provider, newCredential);
669
+ return;
670
+ }
666
671
  const existing = this.#getCredentialsForProvider(provider);
667
672
  if (existing.length === 0) {
668
673
  await this.set(provider, newCredential);
@@ -77,6 +77,7 @@ function buildSshTarget(host: SSHConnectionTarget): string {
77
77
 
78
78
  function buildCommonArgs(host: SSHConnectionTarget): string[] {
79
79
  const args = [
80
+ "-n",
80
81
  "-o",
81
82
  "ControlMaster=auto",
82
83
  "-o",
@@ -11,6 +11,7 @@ import {
11
11
  } from "@nghyane/arcane-tui";
12
12
  import type { Terminal as XtermTerminalType } from "@xterm/headless";
13
13
  import xterm from "@xterm/headless";
14
+ import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
14
15
  import type { Theme } from "../modes/theme/theme";
15
16
  import { OutputSink, type OutputSummary } from "../session/streaming-output";
16
17
  import { getStateIcon } from "../tui";
@@ -276,50 +277,6 @@ class BashInteractiveOverlayComponent implements Component {
276
277
  }
277
278
  }
278
279
 
279
- const NO_PAGER_ENV = {
280
- // Disable pagers so commands don't block on interactive views.
281
- PAGER: "cat",
282
- GIT_PAGER: "cat",
283
- MANPAGER: "cat",
284
- SYSTEMD_PAGER: "cat",
285
- BAT_PAGER: "cat",
286
- DELTA_PAGER: "cat",
287
- GH_PAGER: "cat",
288
- GLAB_PAGER: "cat",
289
- PSQL_PAGER: "cat",
290
- MYSQL_PAGER: "cat",
291
- AWS_PAGER: "",
292
- HOMEBREW_PAGER: "cat",
293
- LESS: "FRX",
294
- // Disable editor and terminal credential prompts.
295
- GIT_EDITOR: "true",
296
- VISUAL: "true",
297
- EDITOR: "true",
298
- GIT_TERMINAL_PROMPT: "0",
299
- SSH_ASKPASS: "/usr/bin/false",
300
- CI: "1",
301
- // Package manager defaults for unattended execution.
302
- npm_config_yes: "true",
303
- npm_config_update_notifier: "false",
304
- npm_config_fund: "false",
305
- npm_config_audit: "false",
306
- npm_config_progress: "false",
307
- PNPM_DISABLE_SELF_UPDATE_CHECK: "true",
308
- PNPM_UPDATE_NOTIFIER: "false",
309
- YARN_ENABLE_TELEMETRY: "0",
310
- YARN_ENABLE_PROGRESS_BARS: "0",
311
- // Cross-language/tooling non-interactive defaults.
312
- CARGO_TERM_PROGRESS_WHEN: "never",
313
- DEBIAN_FRONTEND: "noninteractive",
314
- PIP_NO_INPUT: "1",
315
- PIP_DISABLE_PIP_VERSION_CHECK: "1",
316
- TF_INPUT: "0",
317
- TF_IN_AUTOMATION: "1",
318
- GH_PROMPT_DISABLED: "1",
319
- COMPOSER_NO_INTERACTION: "1",
320
- CLOUDSDK_CORE_DISABLE_PROMPTS: "1",
321
- };
322
-
323
280
  export async function runInteractiveBashPty(
324
281
  ui: NonNullable<AgentToolContext["ui"]>,
325
282
  options: {
@@ -389,8 +346,8 @@ export async function runInteractiveBashPty(
389
346
  cwd: options.cwd,
390
347
  timeoutMs: options.timeoutMs,
391
348
  env: {
349
+ ...NON_INTERACTIVE_ENV,
392
350
  ...options.env,
393
- ...NO_PAGER_ENV,
394
351
  },
395
352
  signal: options.signal,
396
353
  cols,
@@ -461,7 +461,7 @@ Parameters:
461
461
  parameters: webSearchCrawlSchema,
462
462
 
463
463
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
464
- return executeExaTool("crawling", params as Record<string, unknown>, "web_search_crawl");
464
+ return executeExaTool("crawling_exa", params as Record<string, unknown>, "web_search_crawl");
465
465
  },
466
466
 
467
467
  renderCall(args, _options, theme) {
@@ -492,7 +492,7 @@ Parameters:
492
492
  parameters: webSearchLinkedinSchema,
493
493
 
494
494
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
495
- return executeExaTool("linkedin_search", params as Record<string, unknown>, "web_search_linkedin");
495
+ return executeExaTool("linkedin_search_exa", params as Record<string, unknown>, "web_search_linkedin");
496
496
  },
497
497
 
498
498
  renderCall(args, _options, theme) {
@@ -522,7 +522,7 @@ Parameters:
522
522
  parameters: webSearchCompanySchema,
523
523
 
524
524
  async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
525
- return executeExaTool("company_research", params as Record<string, unknown>, "web_search_company");
525
+ return executeExaTool("company_research_exa", params as Record<string, unknown>, "web_search_company");
526
526
  },
527
527
 
528
528
  renderCall(args, _options, theme) {