@nanhara/hara 0.100.0 → 0.101.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 +9 -0
- package/dist/config.js +3 -2
- package/dist/index.js +8 -0
- package/dist/update-check.js +83 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ 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.101.0 — startup update check
|
|
9
|
+
|
|
10
|
+
- **`hara` tells you when it's out of date.** On launch (interactive TTY only), a one-line notice —
|
|
11
|
+
`⬆ Update available 0.100.0 → 0.101.0 · npm i -g @nanhara/hara` — driven by a daily background
|
|
12
|
+
probe with a 3s timeout that NEVER delays startup (the notice always comes from the previous
|
|
13
|
+
probe's cache, npm update-notifier style). npmjs first, npmmirror fallback for CN networks;
|
|
14
|
+
offline machines fail silent and back off to daily retries. Disable with
|
|
15
|
+
`hara config set updateCheck false` or `HARA_UPDATE_CHECK=0`.
|
|
16
|
+
|
|
8
17
|
## 0.100.0 — the agent keeps its own attention: system-reminders + anti-drift compaction
|
|
9
18
|
|
|
10
19
|
Distilled from a source-level study of Claude Code v1.0.33's agent internals (the Ie1/WD5 reminder
|
package/dist/config.js
CHANGED
|
@@ -30,7 +30,7 @@ const PROVIDER_DEFAULTS = {
|
|
|
30
30
|
},
|
|
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
|
-
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", "fallbackModel", "fallbackBaseURL", "fallbackApiKey", "reasoningEffort"];
|
|
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", "fallbackBaseURL", "fallbackApiKey", "reasoningEffort"];
|
|
34
34
|
export const REASONING_EFFORTS = ["off", "low", "medium", "high"];
|
|
35
35
|
export const APPROVAL_MODES = ["suggest", "auto-edit", "full-auto"];
|
|
36
36
|
export const SANDBOX_MODES = ["off", "workspace-write", "read-only"];
|
|
@@ -158,6 +158,7 @@ export function loadConfig(opts = {}) {
|
|
|
158
158
|
const vimMode = process.env.HARA_VIM === "1" || merged.vimMode === true || merged.vimMode === "true";
|
|
159
159
|
const autoCompact = !(process.env.HARA_AUTO_COMPACT === "0" || merged.autoCompact === false || merged.autoCompact === "false"); // default ON
|
|
160
160
|
const fileCheckpoints = !(process.env.HARA_CHECKPOINTS === "0" || merged.fileCheckpoints === false || merged.fileCheckpoints === "false"); // default ON
|
|
161
|
+
const updateCheck = !(process.env.HARA_UPDATE_CHECK === "0" || merged.updateCheck === false || merged.updateCheck === "false"); // default ON
|
|
161
162
|
const fallbackModel = process.env.HARA_FALLBACK_MODEL ?? merged.fallbackModel;
|
|
162
163
|
const fallbackBaseURL = process.env.HARA_FALLBACK_BASE_URL ?? merged.fallbackBaseURL;
|
|
163
164
|
const fallbackApiKey = process.env.HARA_FALLBACK_API_KEY ?? merged.fallbackApiKey;
|
|
@@ -165,7 +166,7 @@ export function loadConfig(opts = {}) {
|
|
|
165
166
|
const reasoningEffort = reasoningRaw && ["off", "low", "medium", "high"].includes(reasoningRaw)
|
|
166
167
|
? reasoningRaw
|
|
167
168
|
: undefined;
|
|
168
|
-
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, fallbackModel, fallbackBaseURL, fallbackApiKey, reasoningEffort, mcpServers, cwd: process.cwd() };
|
|
169
|
+
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, fallbackBaseURL, fallbackApiKey, reasoningEffort, mcpServers, cwd: process.cwd() };
|
|
169
170
|
}
|
|
170
171
|
export function providerEnvKey(provider) {
|
|
171
172
|
return (PROVIDER_DEFAULTS[provider] ?? PROVIDER_DEFAULTS.anthropic).envKey;
|
package/dist/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { loadActiveProfile, listProfiles, useProfile, addProfile, upsertProfile,
|
|
|
25
25
|
import { loadPermissionRules, scaffoldPermissions, globalPermissionsPath, projectPermissionsPath } from "./security/permissions.js";
|
|
26
26
|
import { routingProvider } from "./agent/route.js";
|
|
27
27
|
import { shouldAutoCompact, COMPACT_SYSTEM } from "./agent/compact.js";
|
|
28
|
+
import { checkForUpdate } from "./update-check.js";
|
|
28
29
|
import { formatContextReport } from "./agent/context-report.js";
|
|
29
30
|
import { userTurnPreviews, rewindTo } from "./agent/rewind.js";
|
|
30
31
|
import { checkpoint, listCheckpoints, restoreCheckpoint } from "./checkpoints.js";
|
|
@@ -2187,6 +2188,13 @@ program.action(async (opts) => {
|
|
|
2187
2188
|
// interactive REPL — ink TUI by default on a real terminal; HARA_TUI=0 forces the classic readline path
|
|
2188
2189
|
const useTui = stdin.isTTY && stdout.isTTY && process.env.HARA_TUI !== "0";
|
|
2189
2190
|
out(c.bold(`hara ${pkg.version}`) + c.dim(` · ${cfg.provider}:${cfg.model} · ${approval}${sandbox !== "off" ? ` · sandbox:${sandbox}` : ""} · ${cwd}\n`));
|
|
2191
|
+
// Startup update notice — cache-driven (a previous session's background probe), so it costs zero
|
|
2192
|
+
// latency; today's probe (if due) fires in the background for the NEXT launch. TTY sessions only.
|
|
2193
|
+
if (cfg.updateCheck && stdout.isTTY) {
|
|
2194
|
+
const upd = checkForUpdate(pkg.version);
|
|
2195
|
+
if (upd)
|
|
2196
|
+
out(c.yellow(`⬆ ${upd}`) + "\n");
|
|
2197
|
+
}
|
|
2190
2198
|
const rl = createInterface({
|
|
2191
2199
|
input: stdin,
|
|
2192
2200
|
output: stdout,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Startup update check (npm's update-notifier pattern): NEVER blocks or delays startup. The notice
|
|
2
|
+
// shown at launch comes from a CACHE written by a previous session's background probe; if the cache
|
|
3
|
+
// is stale (> a day) a fresh probe fires in the background with a hard timeout — its result shows on
|
|
4
|
+
// the NEXT launch. Offline / blocked registries fail silently. Interactive TTY sessions only (the
|
|
5
|
+
// caller gates); disable with `hara config set updateCheck false` or HARA_UPDATE_CHECK=0.
|
|
6
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { join, dirname } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
const cacheFile = () => join(homedir(), ".hara", "update-check.json");
|
|
10
|
+
/** Probe at most once a day — the check is a courtesy, not a heartbeat. */
|
|
11
|
+
export const CHECK_EVERY_MS = 24 * 60 * 60 * 1000;
|
|
12
|
+
/** npmjs first; the npmmirror fallback keeps the check working on CN networks where npmjs stalls. */
|
|
13
|
+
const REGISTRIES = ["https://registry.npmjs.org/@nanhara/hara/latest", "https://registry.npmmirror.com/@nanhara/hara/latest"];
|
|
14
|
+
/** Strict-ish semver compare on the numeric triple: is `latest` newer than `current`?
|
|
15
|
+
* Anything unparsable (tags, garbage, empty) → false — never nag on bad data. */
|
|
16
|
+
export function isNewer(latest, current) {
|
|
17
|
+
const parse = (v) => {
|
|
18
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
|
|
19
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
20
|
+
};
|
|
21
|
+
const a = parse(latest);
|
|
22
|
+
const b = parse(current);
|
|
23
|
+
if (!a || !b)
|
|
24
|
+
return false;
|
|
25
|
+
for (let i = 0; i < 3; i++) {
|
|
26
|
+
if (a[i] !== b[i])
|
|
27
|
+
return a[i] > b[i];
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
export function readCache(file = cacheFile()) {
|
|
32
|
+
try {
|
|
33
|
+
const j = JSON.parse(readFileSync(file, "utf8"));
|
|
34
|
+
return typeof j?.checkedAt === "number" && typeof j?.latest === "string" ? j : null;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function writeCache(c, file = cacheFile()) {
|
|
41
|
+
try {
|
|
42
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
43
|
+
writeFileSync(file, JSON.stringify(c));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* a failed cache write must never surface */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** The startup notice, decided purely from cache state (testable without I/O). */
|
|
50
|
+
export function updateNotice(current, cache) {
|
|
51
|
+
if (!cache || !isNewer(cache.latest, current))
|
|
52
|
+
return null;
|
|
53
|
+
return `Update available ${current} → ${cache.latest} · npm i -g @nanhara/hara`;
|
|
54
|
+
}
|
|
55
|
+
/** Background probe: first registry that answers wins; 3s hard timeout each; silent on total failure
|
|
56
|
+
* (checkedAt still stamps so an offline machine backs off to daily retries, not every launch). */
|
|
57
|
+
export async function refreshLatest(file = cacheFile(), fetchFn = fetch) {
|
|
58
|
+
for (const url of REGISTRIES) {
|
|
59
|
+
try {
|
|
60
|
+
const r = await fetchFn(url, { signal: AbortSignal.timeout(3000) });
|
|
61
|
+
if (!r.ok)
|
|
62
|
+
continue;
|
|
63
|
+
const j = (await r.json());
|
|
64
|
+
if (j?.version) {
|
|
65
|
+
writeCache({ checkedAt: Date.now(), latest: j.version }, file);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* offline / blocked / slow → try the next registry, then give up quietly */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const prev = readCache(file);
|
|
74
|
+
writeCache({ checkedAt: Date.now(), latest: prev?.latest ?? "" }, file);
|
|
75
|
+
}
|
|
76
|
+
/** Startup entry: return the notice to print (or null), and — when the cache is stale — fire the
|
|
77
|
+
* daily background probe (fire-and-forget; the caller never awaits it). */
|
|
78
|
+
export function checkForUpdate(current, file = cacheFile(), now = Date.now()) {
|
|
79
|
+
const cache = readCache(file);
|
|
80
|
+
if (!cache || now - cache.checkedAt > CHECK_EVERY_MS)
|
|
81
|
+
void refreshLatest(file).catch(() => { });
|
|
82
|
+
return updateNotice(current, cache);
|
|
83
|
+
}
|