@nanhara/hara 0.112.5 → 0.113.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/CHANGELOG.md +26 -0
- package/README.md +8 -0
- package/dist/agent/loop.js +9 -2
- package/dist/config.js +2 -2
- package/dist/index.js +2 -0
- package/dist/providers/anthropic.js +2 -0
- package/dist/providers/reasoning.js +10 -2
- package/dist/providers/registry.js +4 -4
- package/dist/tools/builtin.js +51 -1
- package/dist/tools/net-reachability.js +67 -0
- package/dist/tui/model-picker.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,32 @@ All notable changes to `@nanhara/hara`.
|
|
|
5
5
|
> Versioning (pre-1.0, SemVer-style): the **minor** (middle) number bumps for a **new feature**; the
|
|
6
6
|
> **patch** (last) number bumps for **optimizations/fixes of existing features**.
|
|
7
7
|
|
|
8
|
+
## 0.113.0 — DeepSeek reasoning control (thinking + effort, incl. `max`) · host-unreachable memory
|
|
9
|
+
|
|
10
|
+
- **DeepSeek reasoning is now a real dial.** DeepSeek's V4 models (`deepseek-v4-pro` / `deepseek-v4-flash`)
|
|
11
|
+
added a per-request thinking switch on the OpenAI-compatible chat path — `thinking: {type}` plus
|
|
12
|
+
`reasoning_effort` (native `high`|`max`; `low`/`medium` map → high server-side). hara now sends both via a
|
|
13
|
+
new `deepseek` reasoning style: **`off` → `thinking:{type:"disabled"}`** (reasoning_effort has no "off"),
|
|
14
|
+
any level → `thinking:{type:"enabled"}` + the effort. The `/model` picker's ←→ thinking dial now lights up
|
|
15
|
+
for DeepSeek. Verified against the live API: `off` emits no `reasoning_content`, `high`/`max` stream it
|
|
16
|
+
(`max` thinks measurably harder than `high`), and tool-calls work with reasoning on.
|
|
17
|
+
- **New `max` reasoning level.** `hara config set reasoningEffort max` (and the picker's ←→) now accept
|
|
18
|
+
**`max`**, the top of the dial. On DeepSeek it becomes `reasoning_effort:"max"`; on OpenAI reasoning models
|
|
19
|
+
it clamps to `high` (OpenAI has no `max`, so it never 400s); on Anthropic it takes the largest thinking
|
|
20
|
+
budget. Existing `off`/`low`/`medium`/`high` are unchanged.
|
|
21
|
+
- **Host-unreachable memory — stop re-hanging on a dead host.** When a network command (git clone/pull/fetch,
|
|
22
|
+
curl, …) fails to CONNECT — a TCP connect timeout or DNS failure (macOS's ~75s SYN timeout), *not* an
|
|
23
|
+
auth/404/connection-refused — hara now remembers that host for the session and **fast-fails later network
|
|
24
|
+
ops to it instantly** instead of eating another ~75s per retry. The failure output also flags that git
|
|
25
|
+
ignores the macOS system / Clash proxy unless configured (`git config --global http.proxy`). Cleared by
|
|
26
|
+
`/reset`. Paired with a system-prompt rule: reuse a local checkout before cloning, don't swap in a public
|
|
27
|
+
mirror for a private repo, and verify connectivity yourself rather than trusting "the network is fine".
|
|
28
|
+
|
|
29
|
+
## 0.112.5 — single-writer session lock
|
|
30
|
+
|
|
31
|
+
- **A double-resume can no longer corrupt session history.** A single-writer lock serializes session writes
|
|
32
|
+
so two processes resuming the same session don't interleave and clobber the transcript.
|
|
33
|
+
|
|
8
34
|
## 0.112.4 — reasoning models don't false-timeout · cross-provider fallback routes correctly
|
|
9
35
|
|
|
10
36
|
- **A reasoning model (qwen3.7-plus / GLM / DeepSeek) thinking on a long context no longer false-times-out.**
|
package/README.md
CHANGED
|
@@ -127,6 +127,14 @@ hara config set visionModel qwen-vl-max # a vision model on the same plan/key
|
|
|
127
127
|
If a model's capability is unknown, hara **asks once and remembers**. In the TUI, `/vision <model>` sets the
|
|
128
128
|
describer and `/vision main yes|no|auto` corrects a model's detected capability.
|
|
129
129
|
|
|
130
|
+
**Reasoning effort** — dial how hard a thinking model reasons: `off` · `low` · `medium` · `high` · `max`.
|
|
131
|
+
```bash
|
|
132
|
+
hara config set reasoningEffort high # or off / low / medium / max
|
|
133
|
+
```
|
|
134
|
+
hara expresses it the way each endpoint wants (OpenAI `reasoning_effort`, Anthropic thinking budget,
|
|
135
|
+
DashScope `enable_thinking`, **DeepSeek** V4 `thinking` + `reasoning_effort` where `max` genuinely raises the
|
|
136
|
+
effort). In the TUI, bare `/model` opens a picker — ↑↓ pick a model, **←→ set the thinking level**.
|
|
137
|
+
|
|
130
138
|
Config lives in `~/.hara/config.json`. Env vars override it: `HARA_PROVIDER`, `HARA_MODEL`,
|
|
131
139
|
`HARA_BASE_URL`, `HARA_API_KEY`, or the provider key (`ANTHROPIC_API_KEY` / `DASHSCOPE_API_KEY`).
|
|
132
140
|
|
package/dist/agent/loop.js
CHANGED
|
@@ -67,8 +67,15 @@ memory: use memory_search before answering about prior decisions,
|
|
|
67
67
|
conventions, or the user's preferences, and memory_write to proactively save durable facts you learn.
|
|
68
68
|
When a task matches one of the Skills listed below, call the \`skill\` tool to load its full instructions
|
|
69
69
|
before acting; save a reusable how-to as a new skill with skill_create. If you discover a durable project
|
|
70
|
-
convention, you may propose an edit to AGENTS.md via edit_file (the user reviews the diff).
|
|
71
|
-
|
|
70
|
+
convention, you may propose an edit to AGENTS.md via edit_file (the user reviews the diff).
|
|
71
|
+
Network resilience: before \`git clone\`, check the target dir isn't already present (ls / test -d) and
|
|
72
|
+
reuse a local checkout instead of re-cloning. If a network command fails to CONNECT (timeout or DNS — not
|
|
73
|
+
auth/404), treat that host as down for the session: don't retry it, don't swap in a public mirror (mirrors
|
|
74
|
+
can't serve private repos), don't switch protocols — hara already fast-fails repeats to a dead host, so
|
|
75
|
+
diagnose instead. git ignores the macOS system / Clash proxy unless configured (git config --global
|
|
76
|
+
http.proxy), so a browser that reaches a site doesn't mean the terminal does — verify connectivity yourself
|
|
77
|
+
rather than trusting "the network is fine". If a step's output artifact already exists and is newer than its
|
|
78
|
+
inputs, skip re-running it. After completing a task, give a one-line summary.`;
|
|
72
79
|
/** When running inside `hara gateway`, tell the agent it's in a chat — so it delivers files via send_file
|
|
73
80
|
* (the only channel that reaches the peer) and never reaches for the desktop client / computer tool. */
|
|
74
81
|
function gatewayNote() {
|
package/dist/config.js
CHANGED
|
@@ -31,7 +31,7 @@ const PROVIDER_DEFAULTS = {
|
|
|
31
31
|
"hara-gateway": { model: "", envKey: "HARA_GATEWAY_TOKEN" }, // B-end: enrolled device → token in ~/.hara/org.json, routed by the gateway
|
|
32
32
|
};
|
|
33
33
|
export const CONFIG_KEYS = ["provider", "apiKey", "model", "baseURL", "approval", "sandbox", "theme", "evolve", "assetCapture", "computerUse", "computerApps", "visionModel", "visionBaseURL", "visionApiKey", "embedProvider", "embedModel", "embedBaseURL", "embedApiKey", "routeModel", "routeBaseURL", "routeApiKey", "guardian", "notify", "vimMode", "autoCompact", "fileCheckpoints", "updateCheck", "fallbackModel", "fallbackProvider", "fallbackBaseURL", "fallbackApiKey", "reasoningEffort"];
|
|
34
|
-
export const REASONING_EFFORTS = ["off", "low", "medium", "high"];
|
|
34
|
+
export const REASONING_EFFORTS = ["off", "low", "medium", "high", "max"];
|
|
35
35
|
export const APPROVAL_MODES = ["suggest", "auto-edit", "full-auto"];
|
|
36
36
|
export const SANDBOX_MODES = ["off", "workspace-write", "read-only"];
|
|
37
37
|
const PROJECT_ROOT_MARKERS = [".git", "package.json", "Cargo.toml", "go.mod", "pyproject.toml", ".hg"];
|
|
@@ -164,7 +164,7 @@ export function loadConfig(opts = {}) {
|
|
|
164
164
|
const fallbackBaseURL = process.env.HARA_FALLBACK_BASE_URL ?? merged.fallbackBaseURL;
|
|
165
165
|
const fallbackApiKey = process.env.HARA_FALLBACK_API_KEY ?? merged.fallbackApiKey;
|
|
166
166
|
const reasoningRaw = process.env.HARA_REASONING_EFFORT ?? merged.reasoningEffort;
|
|
167
|
-
const reasoningEffort = reasoningRaw && ["off", "low", "medium", "high"].includes(reasoningRaw)
|
|
167
|
+
const reasoningEffort = reasoningRaw && ["off", "low", "medium", "high", "max"].includes(reasoningRaw)
|
|
168
168
|
? reasoningRaw
|
|
169
169
|
: undefined;
|
|
170
170
|
return { provider, apiKey, model, baseURL, approval, sandbox, theme, evolve, assetCapture, computerUse, computerApps, visionModel, visionBaseURL, visionApiKey, modelVision, embedProvider, embedModel, embedBaseURL, embedApiKey, routeModel, routeBaseURL, routeApiKey, guardian, hooks, notify, vimMode, autoCompact, fileCheckpoints, updateCheck, fallbackModel, fallbackProvider, fallbackBaseURL, fallbackApiKey, reasoningEffort, mcpServers, cwd: process.cwd() };
|
package/dist/index.js
CHANGED
|
@@ -39,6 +39,7 @@ import { addJob, removeJob, setEnabled, resolveJob, loadJobs, recordRun, logPath
|
|
|
39
39
|
import { runTick, runJobOnce, selfArgv } from "./cron/runner.js";
|
|
40
40
|
import { installScheduler, uninstallScheduler, isInstalled } from "./cron/install.js";
|
|
41
41
|
import { getTools } from "./tools/registry.js";
|
|
42
|
+
import { resetReachability } from "./tools/net-reachability.js";
|
|
42
43
|
import { EXPLORE_SYSTEM } from "./tools/agent.js";
|
|
43
44
|
import { createAnthropicProvider } from "./providers/anthropic.js";
|
|
44
45
|
import { createOpenAIProvider } from "./providers/openai.js";
|
|
@@ -2926,6 +2927,7 @@ program.action(async (opts) => {
|
|
|
2926
2927
|
if (nm === "reset" || nm === "clear") {
|
|
2927
2928
|
history.length = 0;
|
|
2928
2929
|
recalledContext = "";
|
|
2930
|
+
resetReachability(); // fresh start — drop any "host unreachable" marks (network may be fixed)
|
|
2929
2931
|
return void h.sink.notice("(context cleared)");
|
|
2930
2932
|
}
|
|
2931
2933
|
if (nm === "undo") {
|
|
@@ -106,6 +106,8 @@ export function buildThinkingParam(model, effort) {
|
|
|
106
106
|
return { type: "enabled", budget_tokens: 4096 };
|
|
107
107
|
if (effort === "medium")
|
|
108
108
|
return { type: "adaptive" };
|
|
109
|
+
if (effort === "max")
|
|
110
|
+
return { type: "enabled", budget_tokens: 32000 }; // top of the dial — biggest budget
|
|
109
111
|
// high
|
|
110
112
|
return { type: "enabled", budget_tokens: 24000 };
|
|
111
113
|
}
|
|
@@ -26,11 +26,19 @@ export function reasoningParams(style, effort, model = "") {
|
|
|
26
26
|
case "reasoning_effort":
|
|
27
27
|
if (!isReasoningModel(model))
|
|
28
28
|
return {};
|
|
29
|
-
|
|
29
|
+
// OpenAI's ceiling is "high"; there's no "max" — clamp so the global `max` dial never 400s here.
|
|
30
|
+
return { reasoning_effort: effort === "off" ? "minimal" : effort === "max" ? "high" : effort };
|
|
30
31
|
case "reasoning_object":
|
|
31
32
|
if (!isReasoningModel(model))
|
|
32
33
|
return {};
|
|
33
|
-
return { reasoning: { effort: effort === "off" ? "minimal" : effort } };
|
|
34
|
+
return { reasoning: { effort: effort === "off" ? "minimal" : effort === "max" ? "high" : effort } };
|
|
35
|
+
case "deepseek":
|
|
36
|
+
// off → turn thinking OFF via the object (reasoning_effort can't say "off"). Any level → thinking ON
|
|
37
|
+
// + the effort enum; DeepSeek natively honors high|max and maps low/medium→high server-side, so we
|
|
38
|
+
// pass the dial through as-is (forward-compatible if DeepSeek later distinguishes low/medium).
|
|
39
|
+
if (effort === "off")
|
|
40
|
+
return { thinking: { type: "disabled" } };
|
|
41
|
+
return { thinking: { type: "enabled" }, reasoning_effort: effort };
|
|
34
42
|
case "thinking_budget": // Anthropic — applied by anthropic.ts, not on a chat/responses merge body
|
|
35
43
|
case "none":
|
|
36
44
|
default:
|
|
@@ -20,9 +20,9 @@ const BY_BASEURL = [
|
|
|
20
20
|
// GLM, MiniMax, Aliyun apps/anthropic … all expose `.../anthropic` with thinking + explicit cache. One
|
|
21
21
|
// row covers the whole ecosystem — talk to it with the anthropic transport.
|
|
22
22
|
{ test: /\/anthropic\/?($|\?)/i, caps: { wireApi: "anthropic", reasoning: "thinking_budget", cache: "cache_control" } },
|
|
23
|
-
// DeepSeek OpenAI-compatible (chat):
|
|
24
|
-
//
|
|
25
|
-
{ test: /api\.deepseek\.com/i, caps: { wireApi: "chat", reasoning: "
|
|
23
|
+
// DeepSeek OpenAI-compatible (chat): DeepSeek V4 (v4-pro/v4-flash) exposes a per-request thinking switch
|
|
24
|
+
// (`thinking: {type}`) + `reasoning_effort` (high|max) on this path — the `deepseek` style sends both.
|
|
25
|
+
{ test: /api\.deepseek\.com/i, caps: { wireApi: "chat", reasoning: "deepseek", cache: "auto" } },
|
|
26
26
|
];
|
|
27
27
|
/** Per-provider-id overrides (built-in providers whose id alone fixes the shape). */
|
|
28
28
|
const BY_PROVIDER = {
|
|
@@ -30,7 +30,7 @@ const BY_PROVIDER = {
|
|
|
30
30
|
qwen: { wireApi: "chat", reasoning: "enable_thinking", cache: "auto" }, // DashScope
|
|
31
31
|
"qwen-oauth": { wireApi: "chat", reasoning: "enable_thinking", cache: "auto" },
|
|
32
32
|
glm: { wireApi: "chat", reasoning: "none", cache: "auto" }, // Zhipu native /paas/v4 — different thinking param; leave alone (its /anthropic endpoint resolves via baseURL)
|
|
33
|
-
deepseek: { wireApi: "chat", reasoning: "
|
|
33
|
+
deepseek: { wireApi: "chat", reasoning: "deepseek", cache: "auto" }, // V4: thinking:{type} + reasoning_effort(high|max)
|
|
34
34
|
ollama: { wireApi: "chat", reasoning: "ollama_think", cache: "none" }, // local; `think` toggles reasoning
|
|
35
35
|
openai: { wireApi: "chat", reasoning: "reasoning_effort", cache: "auto" },
|
|
36
36
|
openrouter: { wireApi: "chat", reasoning: "none", cache: "auto" },
|
package/dist/tools/builtin.js
CHANGED
|
@@ -7,7 +7,28 @@ import { nearestPaths } from "../fs-walk.js";
|
|
|
7
7
|
import { emitDiff } from "../diff.js";
|
|
8
8
|
import { recordEdit } from "../undo.js";
|
|
9
9
|
import { startJob, listJobs, tailJob, killJob } from "../exec/jobs.js";
|
|
10
|
+
import { hostsInCommand, isNetworkGitOp, hostFromConnectError, isConnectFailure, markHostUnreachable, isHostUnreachable, unreachableHostsSnapshot, } from "./net-reachability.js";
|
|
10
11
|
const MAX = 100_000;
|
|
12
|
+
/** Resolve the remote HOST a bare `git pull/fetch/push` targets (no URL in the command → host lives in the
|
|
13
|
+
* repo's remote config). Local + fast (no network); best-effort — returns "" on any hiccup. Only ever
|
|
14
|
+
* called after a host has already been marked unreachable, so it adds zero overhead on the happy path. */
|
|
15
|
+
async function gitRemoteHost(command, cwd, sandbox) {
|
|
16
|
+
const m = command.match(/\bgit\b[^\n]*\b(?:fetch|pull|push)\b\s+(?!-)(\S+)/);
|
|
17
|
+
const remote = m && /^[\w./-]+$/.test(m[1]) ? m[1] : "origin";
|
|
18
|
+
try {
|
|
19
|
+
const { stdout } = await runShell(`git remote get-url ${remote}`, cwd, sandbox, { timeout: 5000, maxBuffer: 65536 });
|
|
20
|
+
return hostsInCommand(stdout.trim())[0] ?? "";
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** git ignores the macOS system / Clash proxy unless told to — so a browser that reaches GitHub doesn't
|
|
27
|
+
* mean the terminal does. Appended to connectivity-failure output so the agent diagnoses instead of retrying. */
|
|
28
|
+
function proxyHint(host) {
|
|
29
|
+
const h = host || "the remote host";
|
|
30
|
+
return `\n\n↯ hara: this is a CONNECTIVITY failure to ${h} (timeout/DNS), not an auth error — I will NOT retry network ops to ${h} for the rest of this session (a repeat just hangs ~75s again). git does NOT use the macOS system/Clash proxy unless configured; check \`git config --global http.proxy\` and \`echo $https_proxy $ALL_PROXY\`. If you've since started a VPN/proxy or fixed DNS, tell me and I'll clear the mark and retry.`;
|
|
31
|
+
}
|
|
11
32
|
function abs(p, cwd) {
|
|
12
33
|
return isAbsolute(p) ? p : resolve(cwd, p);
|
|
13
34
|
}
|
|
@@ -89,6 +110,22 @@ registerTool({
|
|
|
89
110
|
const id = startJob(input.command, ctx.cwd, ctx.sandbox ?? "off");
|
|
90
111
|
return `Started background job ${id}: \`${input.command}\`. Manage with the \`job\` tool — {action:"tail",id:"${id}"} for output, {action:"kill",id:"${id}"} to stop, {action:"list"} for all.`;
|
|
91
112
|
}
|
|
113
|
+
// Network fault tolerance — short-circuit if this command targets a host already found unreachable this
|
|
114
|
+
// session, so a repeat doesn't burn another ~75s OS connect timeout. Only pays the git-remote lookup
|
|
115
|
+
// once something is actually marked (unreachableHostsSnapshot empty ⇒ zero overhead on the happy path).
|
|
116
|
+
if (unreachableHostsSnapshot().length) {
|
|
117
|
+
const explicit = hostsInCommand(input.command);
|
|
118
|
+
let blocked = explicit.find(isHostUnreachable) ?? "";
|
|
119
|
+
if (!blocked && !explicit.length && isNetworkGitOp(input.command)) {
|
|
120
|
+
const h = await gitRemoteHost(input.command, ctx.cwd, ctx.sandbox ?? "off");
|
|
121
|
+
if (h && isHostUnreachable(h))
|
|
122
|
+
blocked = h;
|
|
123
|
+
}
|
|
124
|
+
if (blocked) {
|
|
125
|
+
ctx.ui?.notice(`↯ skipping — ${blocked} was unreachable earlier this session`);
|
|
126
|
+
return `Skipped without running: host "${blocked}" already failed to connect earlier in THIS session — hara does not retry network operations to a host known unreachable this session (a retry just hangs ~75s again). Do not swap in a public mirror (won't serve private repos) or switch protocols; diagnose instead.${proxyHint(blocked)}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
92
129
|
let buf = ""; // TUI: line-buffer live output into the sink (one notice per line)
|
|
93
130
|
const live = ctx.ui
|
|
94
131
|
? (s) => {
|
|
@@ -114,7 +151,20 @@ registerTool({
|
|
|
114
151
|
return capHeadTail(combined.trim() || "(no output)");
|
|
115
152
|
}
|
|
116
153
|
catch (e) {
|
|
117
|
-
|
|
154
|
+
const base = `Command failed: ${e.message}\n${e.stdout || ""}${e.stderr || ""}`;
|
|
155
|
+
// Network fault tolerance — if this was a genuine host-unreachability (connect timeout / DNS, NOT
|
|
156
|
+
// auth / 404 / connection-refused), remember the host so we fast-fail future ops to it this session.
|
|
157
|
+
if (isConnectFailure(base)) {
|
|
158
|
+
let host = hostFromConnectError(base) || hostsInCommand(input.command)[0] || "";
|
|
159
|
+
if (!host && isNetworkGitOp(input.command))
|
|
160
|
+
host = await gitRemoteHost(input.command, ctx.cwd, ctx.sandbox ?? "off");
|
|
161
|
+
if (host) {
|
|
162
|
+
markHostUnreachable(host);
|
|
163
|
+
ctx.ui?.notice(`↯ ${host} marked unreachable for this session — won't retry network ops to it`);
|
|
164
|
+
return capHeadTail(base + proxyHint(host));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return capHeadTail(base);
|
|
118
168
|
}
|
|
119
169
|
},
|
|
120
170
|
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Session-scoped "host unreachable" memory. When a network command (git / curl / …) fails because the
|
|
2
|
+
// HOST could not be reached — a TCP connect timeout or a DNS failure, NOT an auth/404/protocol error on a
|
|
3
|
+
// host that's actually up — we remember that host for the rest of this hara process. A later command aimed
|
|
4
|
+
// at the same host is then short-circuited INSTANTLY (see builtin.ts bash tool) instead of eating another
|
|
5
|
+
// ~75s OS-level connect timeout. This is the deterministic half of hara's network fault-tolerance: the
|
|
6
|
+
// model can't "forget" it the way a system-prompt rule can be ignored under pressure.
|
|
7
|
+
//
|
|
8
|
+
// One hara process == one session (same process-local pattern as session-model.ts's force-model flag), so
|
|
9
|
+
// a module-local Set is exactly session scope — no need to thread state through ~17 runAgent call sites.
|
|
10
|
+
// This module is PURE + fully unit-tested; the only impure step (resolving a bare `git pull`'s remote host
|
|
11
|
+
// with a local `git remote get-url`) lives in the bash tool, keeping the classification logic deterministic.
|
|
12
|
+
const unreachable = new Set();
|
|
13
|
+
/** Remember a host as unreachable for the rest of this session. */
|
|
14
|
+
export function markHostUnreachable(host) {
|
|
15
|
+
if (host)
|
|
16
|
+
unreachable.add(host.toLowerCase());
|
|
17
|
+
}
|
|
18
|
+
/** Has this host already failed to connect this session? */
|
|
19
|
+
export function isHostUnreachable(host) {
|
|
20
|
+
return !!host && unreachable.has(host.toLowerCase());
|
|
21
|
+
}
|
|
22
|
+
/** Snapshot of the hosts marked unreachable (for the pre-check fast path + tests). */
|
|
23
|
+
export function unreachableHostsSnapshot() {
|
|
24
|
+
return [...unreachable];
|
|
25
|
+
}
|
|
26
|
+
/** Clear the memory — called on /reset (network may have been fixed) and between tests. */
|
|
27
|
+
export function resetReachability() {
|
|
28
|
+
unreachable.clear();
|
|
29
|
+
}
|
|
30
|
+
/** Explicit hosts a command references: URLs (http/https/git/ssh/ftp) + scp/ssh `user@host:path` specs.
|
|
31
|
+
* Pure. A bare `git pull origin main` yields [] — its host lives in the repo's remote config, resolved
|
|
32
|
+
* separately by the bash tool. */
|
|
33
|
+
export function hostsInCommand(command) {
|
|
34
|
+
const hosts = new Set();
|
|
35
|
+
// scheme://[user@]host[:port]/… (github.com from https://github.com/owner/repo.git)
|
|
36
|
+
for (const m of command.matchAll(/\b(?:https?|ssh|git|ftp):\/\/(?:[^/\s'"@]+@)?([^/\s:'"]+)/gi)) {
|
|
37
|
+
hosts.add(m[1].toLowerCase());
|
|
38
|
+
}
|
|
39
|
+
// scp/ssh syntax: user@host:path (github.com from git@github.com:owner/repo.git)
|
|
40
|
+
for (const m of command.matchAll(/(?:^|\s)[A-Za-z0-9._-]+@([A-Za-z0-9.-]+):/g)) {
|
|
41
|
+
hosts.add(m[1].toLowerCase());
|
|
42
|
+
}
|
|
43
|
+
return [...hosts];
|
|
44
|
+
}
|
|
45
|
+
/** Does this command reach out over the network for git? Catches the bare forms (`git pull`, `git fetch`)
|
|
46
|
+
* that carry no URL, so the tool knows to resolve their remote host before deciding to short-circuit. */
|
|
47
|
+
export function isNetworkGitOp(command) {
|
|
48
|
+
return /\bgit\b[^\n]*\b(?:clone|fetch|pull|push|ls-remote|remote\s+(?:update|show)|submodule\s+update)\b/.test(command);
|
|
49
|
+
}
|
|
50
|
+
/** Pull the connect/DNS-failure HOST out of a command's error text — the deterministic signal for WHICH
|
|
51
|
+
* host is down. Returns "" if the text names no host. Pure. */
|
|
52
|
+
export function hostFromConnectError(errText) {
|
|
53
|
+
const m = errText.match(/Failed to connect to ([^\s:]+)\s+port/i) || // git/curl: Failed to connect to github.com port 443
|
|
54
|
+
errText.match(/Could not resolve host:?\s*([^\s'"]+)/i) || // git/curl DNS: Could not resolve host: github.com
|
|
55
|
+
errText.match(/Resolving [^\s]*?\(?([A-Za-z0-9.-]+?)\)? timed out/i) || // curl (28) Resolving api.x.com timed out
|
|
56
|
+
errText.match(/unable to access '[a-z]+:\/\/([^/'"]+)/i); // git: unable to access 'https://github.com/…'
|
|
57
|
+
return m ? m[1].toLowerCase().replace(/[.,]+$/, "") : "";
|
|
58
|
+
}
|
|
59
|
+
/** Is this error a genuine host-unreachability — a connect TIMEOUT / DNS failure / no-route, the kind that
|
|
60
|
+
* burns ~75s — as opposed to (a) auth/404/protocol errors on a host that IS up, or (b) "connection
|
|
61
|
+
* refused", which is a host that's up and fast-rejecting (a dev server not started yet), NOT the slow
|
|
62
|
+
* waste we're guarding against. We cache ONLY genuine unreachability. Pure. */
|
|
63
|
+
export function isConnectFailure(errText) {
|
|
64
|
+
if (/Connection refused|ECONNREFUSED/i.test(errText))
|
|
65
|
+
return false; // host is UP (fast reject) — never cache
|
|
66
|
+
return /(?:Failed to connect to [^\n]*port|Could not resolve host|Couldn't connect to server|Connection timed out|Resolving [^\n]*timed out|Operation timed out|[Nn]etwork is unreachable|[Nn]o route to host|ETIMEDOUT|ENETUNREACH|EHOSTUNREACH|EAI_AGAIN)/.test(errText);
|
|
67
|
+
}
|
package/dist/tui/model-picker.js
CHANGED
|
@@ -12,6 +12,8 @@ export function levelsFor(style) {
|
|
|
12
12
|
return [];
|
|
13
13
|
if (style === "enable_thinking" || style === "ollama_think")
|
|
14
14
|
return ["off", "high"]; // "high" renders as "on"
|
|
15
|
+
if (style === "deepseek")
|
|
16
|
+
return ["off", "low", "medium", "high", "max"]; // DeepSeek V4 honors a real "max"
|
|
15
17
|
return ["off", "low", "medium", "high"];
|
|
16
18
|
}
|
|
17
19
|
/** Label a level for display — binary styles read as on/off, graded ones as the level name. */
|