@nghyane/arcane 0.1.11 → 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/CHANGELOG.md +11 -0
- package/package.json +4 -4
- package/src/config/model-registry.ts +1 -1
- package/src/exa/company.ts +2 -7
- package/src/exa/linkedin.ts +2 -7
- package/src/exa/mcp-client.ts +21 -11
- package/src/exa/researcher.ts +2 -12
- package/src/exa/search.ts +5 -25
- package/src/exa/types.ts +3 -3
- package/src/exec/bash-executor.ts +2 -1
- package/src/exec/non-interactive-env.ts +43 -0
- package/src/extensibility/plugins/installer.ts +6 -21
- package/src/extensibility/plugins/manager.ts +8 -29
- package/src/modes/components/extensions/extension-dashboard.ts +1 -1
- package/src/modes/components/extensions/inspector-panel.ts +7 -2
- package/src/modes/components/welcome.ts +8 -9
- package/src/modes/controllers/event-controller.ts +51 -12
- package/src/patch/index.ts +1 -1
- package/src/session/auth-storage.ts +5 -0
- package/src/ssh/connection-manager.ts +1 -0
- package/src/stt/downloader.ts +1 -4
- package/src/stt/setup.ts +2 -4
- package/src/tools/bash-interactive.ts +2 -45
- package/src/utils/open.ts +2 -1
- package/src/web/search/index.ts +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.1.12] - 2026-02-24
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Preserve single blank line content in edit tool — `hashlineParseContent` no longer strips the only line when it is empty
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Stream codemode intent immediately during LLM generation instead of waiting for execution start
|
|
14
|
+
- Hide loader spinner when codemode group is active to avoid duplicate status indicators
|
|
15
|
+
|
|
5
16
|
## [0.1.8] - 2026-02-22
|
|
6
17
|
|
|
7
18
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@nghyane/arcane",
|
|
4
|
-
"version": "0.1.
|
|
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.
|
|
49
|
-
"@nghyane/arcane-codemode": "^0.1.
|
|
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.
|
|
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"];
|
package/src/exa/company.ts
CHANGED
|
@@ -26,13 +26,8 @@ Parameters:
|
|
|
26
26
|
async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
|
|
27
27
|
try {
|
|
28
28
|
const apiKey = await findApiKey();
|
|
29
|
-
|
|
30
|
-
|
|
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);
|
package/src/exa/linkedin.ts
CHANGED
|
@@ -26,13 +26,8 @@ Parameters:
|
|
|
26
26
|
async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
|
|
27
27
|
try {
|
|
28
28
|
const apiKey = await findApiKey();
|
|
29
|
-
|
|
30
|
-
|
|
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);
|
package/src/exa/mcp-client.ts
CHANGED
|
@@ -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
|
|
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(
|
|
48
|
-
|
|
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
|
|
244
|
-
details: { error: "EXA_API_KEY
|
|
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
|
|
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,
|
package/src/exa/researcher.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
296
|
-
|
|
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
|
-
|
|
125
|
+
crawling_exa: "exa_crawl",
|
|
126
126
|
// LinkedIn
|
|
127
|
-
|
|
127
|
+
linkedin_search_exa: "exa_linkedin",
|
|
128
128
|
// Company
|
|
129
|
-
|
|
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
|
+
};
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { isEnoent } from "@nghyane/arcane-utils";
|
|
4
4
|
import { getAgentDir, getProjectDir } from "@nghyane/arcane-utils/dirs";
|
|
5
|
+
import { $ } from "bun";
|
|
5
6
|
import type { InstalledPlugin } from "./types";
|
|
6
7
|
|
|
7
8
|
const PLUGINS_DIR = path.join(getAgentDir(), "plugins");
|
|
@@ -45,17 +46,9 @@ export async function installPlugin(packageName: string): Promise<InstalledPlugi
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
// Run npm install in plugins directory
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
stdout: "pipe",
|
|
52
|
-
stderr: "pipe",
|
|
53
|
-
windowsHide: true,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const exitCode = await proc.exited;
|
|
57
|
-
if (exitCode !== 0) {
|
|
58
|
-
const stderr = await new Response(proc.stderr).text();
|
|
49
|
+
const result = await $`bun install ${packageName}`.cwd(PLUGINS_DIR).quiet().nothrow();
|
|
50
|
+
if (result.exitCode !== 0) {
|
|
51
|
+
const stderr = result.stderr.toString().trim();
|
|
59
52
|
throw new Error(`Failed to install ${packageName}: ${stderr}`);
|
|
60
53
|
}
|
|
61
54
|
|
|
@@ -87,16 +80,8 @@ export async function uninstallPlugin(name: string): Promise<void> {
|
|
|
87
80
|
|
|
88
81
|
await ensurePluginsDir();
|
|
89
82
|
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
stdin: "ignore",
|
|
93
|
-
stdout: "pipe",
|
|
94
|
-
stderr: "pipe",
|
|
95
|
-
windowsHide: true,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const exitCode = await proc.exited;
|
|
99
|
-
if (exitCode !== 0) {
|
|
83
|
+
const result = await $`bun uninstall ${name}`.cwd(PLUGINS_DIR).quiet().nothrow();
|
|
84
|
+
if (result.exitCode !== 0) {
|
|
100
85
|
throw new Error(`Failed to uninstall ${name}`);
|
|
101
86
|
}
|
|
102
87
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getProjectDir,
|
|
10
10
|
getProjectPluginOverridesPath,
|
|
11
11
|
} from "@nghyane/arcane-utils/dirs";
|
|
12
|
+
import { $ } from "bun";
|
|
12
13
|
import { extractPackageName, parsePluginSpec } from "./parser";
|
|
13
14
|
import type {
|
|
14
15
|
DoctorCheck,
|
|
@@ -155,17 +156,9 @@ export class PluginManager {
|
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
// Run npm install
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
stdout: "pipe",
|
|
162
|
-
stderr: "pipe",
|
|
163
|
-
windowsHide: true,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
const exitCode = await proc.exited;
|
|
167
|
-
if (exitCode !== 0) {
|
|
168
|
-
const stderr = await new Response(proc.stderr).text();
|
|
159
|
+
const result = await $`bun install ${spec.packageName}`.cwd(getPluginsDir()).quiet().nothrow();
|
|
160
|
+
if (result.exitCode !== 0) {
|
|
161
|
+
const stderr = result.stderr.toString().trim();
|
|
169
162
|
throw new Error(`npm install failed: ${stderr}`);
|
|
170
163
|
}
|
|
171
164
|
|
|
@@ -236,16 +229,8 @@ export class PluginManager {
|
|
|
236
229
|
validatePackageName(name);
|
|
237
230
|
await this.#ensurePackageJson();
|
|
238
231
|
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
stdin: "ignore",
|
|
242
|
-
stdout: "pipe",
|
|
243
|
-
stderr: "pipe",
|
|
244
|
-
windowsHide: true,
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
const exitCode = await proc.exited;
|
|
248
|
-
if (exitCode !== 0) {
|
|
232
|
+
const result = await $`bun uninstall ${name}`.cwd(getPluginsDir()).quiet().nothrow();
|
|
233
|
+
if (result.exitCode !== 0) {
|
|
249
234
|
throw new Error(`npm uninstall failed for ${name}`);
|
|
250
235
|
}
|
|
251
236
|
|
|
@@ -619,14 +604,8 @@ export class PluginManager {
|
|
|
619
604
|
|
|
620
605
|
async #fixMissingPlugin(): Promise<boolean> {
|
|
621
606
|
try {
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
stdin: "ignore",
|
|
625
|
-
stdout: "pipe",
|
|
626
|
-
stderr: "pipe",
|
|
627
|
-
windowsHide: true,
|
|
628
|
-
});
|
|
629
|
-
return (await proc.exited) === 0;
|
|
607
|
+
const result = await $`bun install`.cwd(getPluginsDir()).quiet().nothrow();
|
|
608
|
+
return result.exitCode === 0;
|
|
630
609
|
} catch {
|
|
631
610
|
return false;
|
|
632
611
|
}
|
|
@@ -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
|
-
|
|
38
|
-
|
|
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
|
|
@@ -67,9 +67,9 @@ export class WelcomeComponent implements Component {
|
|
|
67
67
|
const leftCol = showRightColumn ? dualLeftCol : boxWidth - 2;
|
|
68
68
|
const rightCol = showRightColumn ? dualRightCol : 0;
|
|
69
69
|
|
|
70
|
-
// Block-based
|
|
70
|
+
// Block-based ARC logo (gradient: blue → cyan → green / Nord Frost)
|
|
71
71
|
// biome-ignore format: preserve ASCII art layout
|
|
72
|
-
const piLogo = ["
|
|
72
|
+
const piLogo = ["╭━━━╮╭━━━╮╭━━━╮", "┃╭━╮┃┃╭━╮┃┃╭━━╯", "┃╰━╯┃┃╰━╯┃┃┃ ", "┃┃ ┃┃┃╭╮╭╯┃╰━━╮", "╰╯ ╰╯╰╯╰╯ ╰━━━╯"];
|
|
73
73
|
|
|
74
74
|
// Apply gradient to logo
|
|
75
75
|
const logoColored = piLogo.map(line => this.#gradientLine(line));
|
|
@@ -190,15 +190,14 @@ export class WelcomeComponent implements Component {
|
|
|
190
190
|
return padding(leftPad) + text + padding(rightPad);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
/** Apply
|
|
193
|
+
/** Apply Nord Frost gradient (blue → cyan → green) to a string */
|
|
194
194
|
#gradientLine(line: string): string {
|
|
195
195
|
const colors = [
|
|
196
|
-
"\x1b[38;
|
|
197
|
-
"\x1b[38;
|
|
198
|
-
"\x1b[38;
|
|
199
|
-
"\x1b[38;
|
|
200
|
-
"\x1b[38;
|
|
201
|
-
"\x1b[38;5;51m", // bright cyan
|
|
196
|
+
"\x1b[38;2;136;192;208m", // #88c0d0 blue
|
|
197
|
+
"\x1b[38;2;141;200;200m", // blend
|
|
198
|
+
"\x1b[38;2;143;188;187m", // #8fbcbb cyan
|
|
199
|
+
"\x1b[38;2;153;189;170m", // blend
|
|
200
|
+
"\x1b[38;2;163;190;140m", // #a3be8c green
|
|
202
201
|
];
|
|
203
202
|
const reset = "\x1b[0m";
|
|
204
203
|
|
|
@@ -42,6 +42,39 @@ export class EventController {
|
|
|
42
42
|
this.ctx.setWorkingMessage(`${trimmed} (esc to interrupt)`);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
#ensureCodemodeGroup(id: string): CodeModeGroupComponent {
|
|
46
|
+
let group = this.#codemodeGroups.get(id);
|
|
47
|
+
if (!group) {
|
|
48
|
+
this.#resetReadGroup();
|
|
49
|
+
group = new CodeModeGroupComponent(this.ctx.ui);
|
|
50
|
+
group.setExpanded(this.ctx.toolOutputExpanded);
|
|
51
|
+
this.ctx.chatContainer.addChild(group);
|
|
52
|
+
this.#codemodeGroups.set(id, group);
|
|
53
|
+
this.ctx.pendingTools.set(id, group);
|
|
54
|
+
this.#hideLoader();
|
|
55
|
+
}
|
|
56
|
+
return group;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#hideLoader(): void {
|
|
60
|
+
if (!this.ctx.loadingAnimation) return;
|
|
61
|
+
this.ctx.loadingAnimation.stop();
|
|
62
|
+
this.ctx.statusContainer.clear();
|
|
63
|
+
this.ctx.loadingAnimation = undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#restoreLoader(): void {
|
|
67
|
+
if (this.ctx.loadingAnimation || this.#codemodeGroups.size > 0) return;
|
|
68
|
+
this.ctx.loadingAnimation = new Loader(
|
|
69
|
+
this.ctx.ui,
|
|
70
|
+
spinner => theme.fg("accent", spinner),
|
|
71
|
+
text => theme.fg("muted", text),
|
|
72
|
+
`Working\u2026 (esc to interrupt)`,
|
|
73
|
+
getSymbolTheme().spinnerFrames,
|
|
74
|
+
);
|
|
75
|
+
this.ctx.statusContainer.addChild(this.ctx.loadingAnimation);
|
|
76
|
+
}
|
|
77
|
+
|
|
45
78
|
subscribeToAgent(): void {
|
|
46
79
|
this.ctx.unsubscribe = this.ctx.session.subscribe(async (event: AgentSessionEvent) => {
|
|
47
80
|
await this.handleEvent(event);
|
|
@@ -132,8 +165,16 @@ export class EventController {
|
|
|
132
165
|
|
|
133
166
|
for (const content of this.ctx.streamingMessage.content) {
|
|
134
167
|
if (content.type !== "toolCall") continue;
|
|
135
|
-
// Code Mode:
|
|
136
|
-
if (content.name === "code")
|
|
168
|
+
// Code Mode: create group component early during streaming for intent display
|
|
169
|
+
if (content.name === "code") {
|
|
170
|
+
const group = this.#ensureCodemodeGroup(content.id);
|
|
171
|
+
const args = content.arguments;
|
|
172
|
+
if (args && typeof args === "object" && INTENT_FIELD in args) {
|
|
173
|
+
const intent = (args[INTENT_FIELD] as string | undefined)?.trim();
|
|
174
|
+
if (intent) group.setIntent(intent);
|
|
175
|
+
}
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
137
178
|
|
|
138
179
|
if (!this.ctx.pendingTools.has(content.id)) {
|
|
139
180
|
if (content.name === "read") {
|
|
@@ -169,9 +210,10 @@ export class EventController {
|
|
|
169
210
|
}
|
|
170
211
|
}
|
|
171
212
|
|
|
172
|
-
// Update working message with intent
|
|
213
|
+
// Update working message with intent — skip for code tools that already have a visible group
|
|
173
214
|
for (const content of this.ctx.streamingMessage.content) {
|
|
174
215
|
if (content.type !== "toolCall") continue;
|
|
216
|
+
if (this.#codemodeGroups.has(content.id)) continue;
|
|
175
217
|
const args = content.arguments;
|
|
176
218
|
if (!args || typeof args !== "object" || !(INTENT_FIELD in args)) continue;
|
|
177
219
|
this.#updateWorkingMessageFromIntent(args[INTENT_FIELD] as string | undefined);
|
|
@@ -218,19 +260,15 @@ export class EventController {
|
|
|
218
260
|
break;
|
|
219
261
|
|
|
220
262
|
case "tool_execution_start": {
|
|
221
|
-
this.#updateWorkingMessageFromIntent(event.intent);
|
|
222
|
-
// Code Mode: create a group component for the "code" tool
|
|
263
|
+
if (!this.#codemodeGroups.has(event.toolCallId)) this.#updateWorkingMessageFromIntent(event.intent);
|
|
223
264
|
if (event.toolName === "code") {
|
|
224
|
-
this.#
|
|
225
|
-
const
|
|
226
|
-
|
|
265
|
+
const group = this.#ensureCodemodeGroup(event.toolCallId);
|
|
266
|
+
const intent = (event.intent ?? (event.args as Record<string, unknown>)?.agent__intent) as
|
|
267
|
+
| string
|
|
268
|
+
| undefined;
|
|
227
269
|
if (typeof intent === "string" && intent.trim()) {
|
|
228
270
|
group.setIntent(intent.trim());
|
|
229
271
|
}
|
|
230
|
-
group.setExpanded(this.ctx.toolOutputExpanded);
|
|
231
|
-
this.ctx.chatContainer.addChild(group);
|
|
232
|
-
this.#codemodeGroups.set(event.toolCallId, group);
|
|
233
|
-
this.ctx.pendingTools.set(event.toolCallId, group);
|
|
234
272
|
this.ctx.ui.requestRender();
|
|
235
273
|
break;
|
|
236
274
|
}
|
|
@@ -316,6 +354,7 @@ export class EventController {
|
|
|
316
354
|
group.setDone();
|
|
317
355
|
this.#codemodeGroups.delete(event.toolCallId);
|
|
318
356
|
}
|
|
357
|
+
this.#restoreLoader();
|
|
319
358
|
}
|
|
320
359
|
// Update todo display when todo_write tool completes
|
|
321
360
|
if (event.toolName === "todo_write" && !event.isError) {
|
package/src/patch/index.ts
CHANGED
|
@@ -199,7 +199,7 @@ function hashlineParseContent(edit: string | string[] | null): string[] {
|
|
|
199
199
|
if (Array.isArray(edit)) return edit;
|
|
200
200
|
const lines = stripNewLinePrefixes(edit.split("\n"));
|
|
201
201
|
if (lines.length === 0) return [];
|
|
202
|
-
if (lines[lines.length - 1].trim() === "") return lines.slice(0, -1);
|
|
202
|
+
if (lines.length > 1 && lines[lines.length - 1].trim() === "") return lines.slice(0, -1);
|
|
203
203
|
return lines;
|
|
204
204
|
}
|
|
205
205
|
|
|
@@ -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);
|
package/src/stt/downloader.ts
CHANGED
|
@@ -46,10 +46,7 @@ async function ensurePythonWhisper(options?: EnsureOptions): Promise<void> {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// Check if whisper module is already importable
|
|
49
|
-
const check =
|
|
50
|
-
stdout: "pipe",
|
|
51
|
-
stderr: "pipe",
|
|
52
|
-
});
|
|
49
|
+
const check = await $`${pythonCmd} -c ${"import whisper"}`.quiet().nothrow();
|
|
53
50
|
if (check.exitCode === 0) return;
|
|
54
51
|
|
|
55
52
|
options?.onProgress?.({ stage: "Installing openai-whisper (this may take a few minutes)..." });
|
package/src/stt/setup.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { $ } from "bun";
|
|
1
2
|
import { detectRecordingTools } from "./recorder";
|
|
2
3
|
import { resolvePython } from "./transcriber";
|
|
3
4
|
|
|
@@ -20,10 +21,7 @@ export async function checkDependencies(): Promise<STTDependencyStatus> {
|
|
|
20
21
|
|
|
21
22
|
let whisperAvailable = false;
|
|
22
23
|
if (pythonCmd) {
|
|
23
|
-
const check =
|
|
24
|
-
stdout: "pipe",
|
|
25
|
-
stderr: "pipe",
|
|
26
|
-
});
|
|
24
|
+
const check = await $`${pythonCmd} -c ${"import whisper"}`.quiet().nothrow();
|
|
27
25
|
whisperAvailable = check.exitCode === 0;
|
|
28
26
|
}
|
|
29
27
|
const whisperHint = "Run 'arc setup stt' to auto-install, or: pip install openai-whisper";
|
|
@@ -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,
|
package/src/utils/open.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { $ } from "bun";
|
|
1
2
|
/** Open a URL or file path in the default browser/application. Best-effort, never throws. */
|
|
2
3
|
export function openPath(urlOrPath: string): void {
|
|
3
4
|
let cmd: string[];
|
|
@@ -13,7 +14,7 @@ export function openPath(urlOrPath: string): void {
|
|
|
13
14
|
break;
|
|
14
15
|
}
|
|
15
16
|
try {
|
|
16
|
-
|
|
17
|
+
$`${cmd}`.quiet().nothrow();
|
|
17
18
|
} catch {
|
|
18
19
|
// Best-effort: browser opening is non-critical
|
|
19
20
|
}
|
package/src/web/search/index.ts
CHANGED
|
@@ -461,7 +461,7 @@ Parameters:
|
|
|
461
461
|
parameters: webSearchCrawlSchema,
|
|
462
462
|
|
|
463
463
|
async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
|
|
464
|
-
return executeExaTool("
|
|
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("
|
|
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("
|
|
525
|
+
return executeExaTool("company_research_exa", params as Record<string, unknown>, "web_search_company");
|
|
526
526
|
},
|
|
527
527
|
|
|
528
528
|
renderCall(args, _options, theme) {
|