@nanhara/hara 0.99.3 → 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 +28 -0
- package/README.md +1 -1
- package/dist/agent/compact.js +17 -0
- package/dist/agent/loop.js +28 -1
- package/dist/agent/reminders.js +38 -0
- package/dist/config.js +3 -2
- package/dist/index.js +10 -11
- package/dist/org/roles.js +34 -8
- package/dist/update-check.js +83 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ 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
|
+
|
|
17
|
+
## 0.100.0 — the agent keeps its own attention: system-reminders + anti-drift compaction
|
|
18
|
+
|
|
19
|
+
Distilled from a source-level study of Claude Code v1.0.33's agent internals (the Ie1/WD5 reminder
|
|
20
|
+
layer and the AU2 compaction template).
|
|
21
|
+
|
|
22
|
+
- **system-reminder injection layer.** An event queue (`agent/reminders.ts`) that lands queued context
|
|
23
|
+
as ONE `<system-reminder>`-wrapped message before the next model call — visible to the model, never
|
|
24
|
+
rendered in the transcript, and always carrying the "ignore unless relevant" disclaimer so a nudge
|
|
25
|
+
can't derail unrelated work. Quiet (sub-agent) runs neither drain nor push, so parallel fan-outs
|
|
26
|
+
can't steal the main conversation's reminders. First wired event:
|
|
27
|
+
- **Todo attention-refresh.** When a checklist has unfinished items and goes 5 tool-rounds untouched,
|
|
28
|
+
the model gets a reminder re-showing the authoritative list and asking for a status pass — so long
|
|
29
|
+
tasks stop silently abandoning their own plan. Any `todo_write` resets the clock; after firing it
|
|
30
|
+
re-arms (at most one nag per 5 rounds).
|
|
31
|
+
- **Compaction brief: 6 → 8 sections (anti-drift).** `/compact` and auto-compaction now also preserve
|
|
32
|
+
**All user messages** — your own words survive verbatim, in order, however hard the history is
|
|
33
|
+
squeezed — and **Key technical concepts**, so the next turn doesn't re-derive the stack.
|
|
34
|
+
(`COMPACT_SYSTEM` moved to `agent/compact.ts` where tests pin the structure.)
|
|
35
|
+
|
|
8
36
|
## 0.99.3 — TUI: rock-steady input box (constant-height chrome) + plan mode grows a real handshake
|
|
9
37
|
|
|
10
38
|
Built from a source-level study of codex-rs (bottom-anchored viewport, plan cells) and Claude Code
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
**Highlights**
|
|
14
14
|
- **An org, not just an agent** — `hara org "<task>"` routes work to the role that *owns* it; `hara plan "<task>"` decomposes a task into a verified DAG of atoms (frame → atomize → sequence → execute → **verify gate**), and `hara plan --parallel` runs independent atoms concurrently.
|
|
15
15
|
- **Drive it from chat** — `hara gateway` runs your local hara from **Telegram · WeChat · Discord · Feishu/Lark · Slack · Mattermost · Matrix · DingTalk · WeCom · Signal** (10 platforms), with **two-way images** (send a photo → it sees it; ask for a file → it sends one), per-chat resumable sessions, and project roaming. Connects out — no public webhook. See **[docs/gateway.md](docs/gateway.md)**.
|
|
16
|
-
- **Real terminal UX** — an **ink TUI**: bottom-pinned input box, **plan mode** (read-only →
|
|
16
|
+
- **Real terminal UX** — an **ink TUI**: bottom-pinned input box, **plan mode** (read-only investigation → the model submits its plan via `exit_plan` → approve → execute), selectable approvals with "don't ask again", windowed reasoning, **paste images** (Ctrl+V) for vision models, light/dark theme.
|
|
17
17
|
- **Persistent memory + self-evolution** — `memory_*` tools over global/project `MEMORY.md`; the agent recalls before acting, **proactively saves** durable facts, and grows its own playbooks (a lexical guard screens what it writes). Inspect/consolidate it with **`hara memory show`** and **`hara memory distill`** (promote recent daily logs → durable memory). Lexical-first by design — semantic search is opt-in, never required.
|
|
18
18
|
- **Multi-provider, all streamed** — Anthropic (Claude) or any OpenAI-compatible endpoint (Qwen/DashScope, GLM, Kimi, OpenAI) with live Markdown + visible reasoning.
|
|
19
19
|
- **Delegate to other agents** — the **`external_agent`** tool hands a self-contained task to **Claude Code** or **Codex** running headless, and returns the result — so you pick the best engine per task. Gated by approval, trust-tiered, and never exposed to read-only sub-agents.
|
package/dist/agent/compact.js
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
3
3
|
// which reuses the manual /compact path; this just decides *when* to fire.
|
|
4
4
|
/** Auto-compact once the last turn used ≥ this % of the model's context window. */
|
|
5
5
|
export const AUTO_COMPACT_PCT = 85;
|
|
6
|
+
/** The compaction brief (shared by /compact and auto-compaction). Eight sections, mirroring Claude
|
|
7
|
+
* Code's AU2 template — the two that matter most beyond the obvious: **All user messages** (the
|
|
8
|
+
* user's own words survive verbatim, so however hard the history is squeezed, intent never drifts)
|
|
9
|
+
* and **Key technical concepts** (the frameworks/APIs in play, so the next turn doesn't re-derive
|
|
10
|
+
* the stack). Lives here (not index.ts) so tests can pin the structure. */
|
|
11
|
+
export const COMPACT_SYSTEM = "Summarize the conversation so far into a structured, complete brief so the assistant can continue with NO " +
|
|
12
|
+
"loss of context. First think privately in a brief <analysis> scratchpad (what matters, what's in flight), " +
|
|
13
|
+
"then output ONLY the summary under these exact headings:\n" +
|
|
14
|
+
"1. Goal — the user's overall intent, in their own framing.\n" +
|
|
15
|
+
"2. Key technical concepts — the frameworks, tools, APIs, and domain ideas central to the work.\n" +
|
|
16
|
+
"3. Key decisions — choices made and why (so they aren't relitigated).\n" +
|
|
17
|
+
"4. Files & code — files created/changed and the important snippets, with why each matters.\n" +
|
|
18
|
+
"5. Errors & fixes — failures hit, how they were resolved, and any correction the user gave (quote pointed feedback verbatim).\n" +
|
|
19
|
+
"6. Current state — what works now / what is verified.\n" +
|
|
20
|
+
"7. All user messages — EVERY user message so far (excluding tool results), verbatim and in order; abbreviate only huge pasted blobs with […]. These are the ground truth of intent.\n" +
|
|
21
|
+
"8. Next step — the immediate next action, INCLUDING a direct verbatim quote of the user's most recent request so there is no drift.\n" +
|
|
22
|
+
"Be specific and concrete. Drop the <analysis>; output only the headed summary.";
|
|
6
23
|
/** Whether to auto-compact now: enabled, the history is substantial enough to be worth summarizing, and the
|
|
7
24
|
* last turn filled the context past the threshold (so the NEXT turn would risk overflow). */
|
|
8
25
|
export function shouldAutoCompact(ctxPct, historyLen, autoCompact, threshold = AUTO_COMPACT_PCT) {
|
package/dist/agent/loop.js
CHANGED
|
@@ -10,7 +10,8 @@ import { decideCommand, loadPermissionRules } from "../security/permissions.js";
|
|
|
10
10
|
import { classifyRisk, guardianVeto, guardianEnabled, newBreaker, recordBlock } from "../security/guardian.js";
|
|
11
11
|
import { subdirHint } from "../context/subdir-hints.js";
|
|
12
12
|
import { classifyError, failoverAction, errorHint } from "./failover.js";
|
|
13
|
-
import { currentTodos } from "../tools/todo.js";
|
|
13
|
+
import { currentTodos, renderTodos } from "../tools/todo.js";
|
|
14
|
+
import { drainReminders, wrapReminders, pushReminder, todoStaleReminder, TODO_STALE_ROUNDS } from "./reminders.js";
|
|
14
15
|
/** Spinner verb (terminal mode + reused by TUI tests): when the agent has an in_progress todo,
|
|
15
16
|
* surface its activeForm/text so the bottom-of-screen line reads concretely ("▶ updating tests… 3s")
|
|
16
17
|
* instead of "working 3s". Pure: takes a snapshot + elapsed seconds. */
|
|
@@ -86,6 +87,9 @@ export async function runAgent(history, opts) {
|
|
|
86
87
|
const guardianOn = !!opts.guardian && (opts.guardian.enabled ?? true) && guardianEnabled();
|
|
87
88
|
const breaker = newBreaker();
|
|
88
89
|
let breakerHalt = false; // set when a tripped breaker aborts this run
|
|
90
|
+
// Todo attention-refresh (à la Claude Code): tool rounds since the checklist was last touched while
|
|
91
|
+
// unfinished items exist. Main loop only — quiet (sub-agent) runs share the global list and must not nag.
|
|
92
|
+
let todoIdleRounds = 0;
|
|
89
93
|
for (;;) {
|
|
90
94
|
// Type-ahead steering: fold in anything the user submitted while the previous step ran, so it
|
|
91
95
|
// reaches the model on this next call (drained after the last tool round; empty on the 1st pass).
|
|
@@ -93,6 +97,14 @@ export async function runAgent(history, opts) {
|
|
|
93
97
|
for (const m of await opts.pendingInput())
|
|
94
98
|
history.push(m);
|
|
95
99
|
}
|
|
100
|
+
// system-reminder injection: event-driven context queued since the last call (todo staleness today)
|
|
101
|
+
// lands as ONE wrapped user message the UI never renders. Quiet runs don't drain — a parallel
|
|
102
|
+
// sub-agent must not steal the main conversation's reminders.
|
|
103
|
+
if (!opts.quiet) {
|
|
104
|
+
const reminders = drainReminders();
|
|
105
|
+
if (reminders.length)
|
|
106
|
+
history.push({ role: "user", content: wrapReminders(reminders) });
|
|
107
|
+
}
|
|
96
108
|
const baseSpecs = opts.toolFilter ? toolSpecs().filter((t) => opts.toolFilter(t.name)) : toolSpecs();
|
|
97
109
|
const specs = opts.extraTools?.length
|
|
98
110
|
? [...baseSpecs, ...opts.extraTools.map((t) => ({ name: t.name, description: t.description, input_schema: t.input_schema }))]
|
|
@@ -349,6 +361,21 @@ export async function runAgent(history, opts) {
|
|
|
349
361
|
}
|
|
350
362
|
await flush();
|
|
351
363
|
history.push({ role: "tool", results });
|
|
364
|
+
// Todo attention-refresh: a round that touched the checklist resets the clock; rounds that leave
|
|
365
|
+
// unfinished items untouched accumulate, and at TODO_STALE_ROUNDS the model gets a system-reminder
|
|
366
|
+
// re-showing the authoritative list (then the counter re-arms — at most one nag per N rounds).
|
|
367
|
+
if (!opts.quiet) {
|
|
368
|
+
if (r.toolUses.some((tu) => tu.name === "todo_write")) {
|
|
369
|
+
todoIdleRounds = 0;
|
|
370
|
+
}
|
|
371
|
+
else if (currentTodos().some((t) => t.status !== "done")) {
|
|
372
|
+
todoIdleRounds++;
|
|
373
|
+
if (todoIdleRounds >= TODO_STALE_ROUNDS) {
|
|
374
|
+
pushReminder(todoStaleReminder(renderTodos(currentTodos())));
|
|
375
|
+
todoIdleRounds = 0;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
352
379
|
if (breakerHalt) {
|
|
353
380
|
// A tripped-and-declined circuit-breaker is a hard stop: end the run cleanly (the denial messages are
|
|
354
381
|
// already in `results` so the model/user see why). Never spin further.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// system-reminder injection (à la Claude Code's Ie1/WD5 event layer): event-driven context the model
|
|
2
|
+
// should see on its NEXT call, injected as ONE `<system-reminder>`-wrapped user message. It rides the
|
|
3
|
+
// provider history only — the UI transcript never renders it, so the user isn't bothered while the
|
|
4
|
+
// model stays synchronized with system state (todo staleness today; file-change/diagnostic events can
|
|
5
|
+
// plug in later via pushReminder).
|
|
6
|
+
//
|
|
7
|
+
// Claude Code's disclaimer is preserved: the model is told the context may be irrelevant, so an
|
|
8
|
+
// injected nudge never derails an unrelated task.
|
|
9
|
+
const queue = [];
|
|
10
|
+
/** Queue a reminder for injection before the next model call (main loop only — quiet/sub-agent runs
|
|
11
|
+
* neither push nor drain, so a parallel fan-out can't steal the main conversation's reminders). */
|
|
12
|
+
export function pushReminder(text) {
|
|
13
|
+
const t = text.trim();
|
|
14
|
+
if (t)
|
|
15
|
+
queue.push(t);
|
|
16
|
+
}
|
|
17
|
+
/** Take everything queued (FIFO), clearing the queue. */
|
|
18
|
+
export function drainReminders() {
|
|
19
|
+
if (!queue.length)
|
|
20
|
+
return [];
|
|
21
|
+
return queue.splice(0, queue.length);
|
|
22
|
+
}
|
|
23
|
+
/** Merge queued reminders into the single injected message. */
|
|
24
|
+
export function wrapReminders(items) {
|
|
25
|
+
return ("<system-reminder>\n" +
|
|
26
|
+
items.join("\n\n") +
|
|
27
|
+
"\n\nThis context may or may not be relevant to your task — do not respond to it directly; ignore it unless it is relevant.\n" +
|
|
28
|
+
"</system-reminder>");
|
|
29
|
+
}
|
|
30
|
+
/** How many tool rounds a checklist may sit untouched (with unfinished items) before the model gets an
|
|
31
|
+
* attention refresh. Reset on every todo_write; re-arms after firing so it nags at most once per N. */
|
|
32
|
+
export const TODO_STALE_ROUNDS = 5;
|
|
33
|
+
/** The staleness nudge: re-show the authoritative list + ask for a status pass. */
|
|
34
|
+
export function todoStaleReminder(renderedTodos) {
|
|
35
|
+
return (`Your todo list has not been updated in a while. Current state:\n\n${renderedTodos}\n\n` +
|
|
36
|
+
"If you have completed or started items, update them with todo_write now (statuses drive the user's progress view). " +
|
|
37
|
+
"If the list no longer matches the work, rewrite it.");
|
|
38
|
+
}
|
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
|
@@ -24,7 +24,8 @@ import { clearEnrollment, enrollDevice, heartbeat, syncOrgRoles } from "./org-fl
|
|
|
24
24
|
import { loadActiveProfile, listProfiles, useProfile, addProfile, upsertProfile, removeProfile, setModel as setProfileModel, resetModel as resetProfileModel, getProfile, effectiveModel, routingLabel, routeHost, activeId, resolveActive, setFlagOverride, writePin, removePin, pinFilePath, DEFAULT_ORG_ID, PERSONAL_ID, } from "./profile/profile.js";
|
|
25
25
|
import { loadPermissionRules, scaffoldPermissions, globalPermissionsPath, projectPermissionsPath } from "./security/permissions.js";
|
|
26
26
|
import { routingProvider } from "./agent/route.js";
|
|
27
|
-
import { shouldAutoCompact } from "./agent/compact.js";
|
|
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";
|
|
@@ -716,16 +717,7 @@ const MEMORY_DISTILL_SYSTEM = "You consolidate an agent's short-term daily memor
|
|
|
716
717
|
"conventions / user preferences from the logs that are NOT already captured, and persist each with " +
|
|
717
718
|
"memory_write (target=memory, or target=user for preferences; pick the right scope=project|global). " +
|
|
718
719
|
"Skip the ephemeral, the one-off, and anything already known. Be terse and de-duplicated. Then reply DONE.";
|
|
719
|
-
|
|
720
|
-
"loss of context. First think privately in a brief <analysis> scratchpad (what matters, what's in flight), " +
|
|
721
|
-
"then output ONLY the summary under these exact headings:\n" +
|
|
722
|
-
"1. Goal — the user's overall intent, in their own framing.\n" +
|
|
723
|
-
"2. Key decisions — choices made and why (so they aren't relitigated).\n" +
|
|
724
|
-
"3. Files & code — files created/changed and the important snippets, with why each matters.\n" +
|
|
725
|
-
"4. Errors & fixes — failures hit, how they were resolved, and any correction the user gave (quote pointed feedback verbatim).\n" +
|
|
726
|
-
"5. Current state — what works now / what is verified.\n" +
|
|
727
|
-
"6. Next step — the immediate next action, INCLUDING a direct verbatim quote of the user's most recent request so there is no drift.\n" +
|
|
728
|
-
"Be specific and concrete. Drop the <analysis>; output only the headed summary.";
|
|
720
|
+
// COMPACT_SYSTEM (the 8-section brief) lives in agent/compact.ts so tests can pin its structure.
|
|
729
721
|
const workingSetFromSummary = (s) => s
|
|
730
722
|
.split("\n")
|
|
731
723
|
.map((l) => l.replace(/^[-*\d.\s]+/, "").trim())
|
|
@@ -2196,6 +2188,13 @@ program.action(async (opts) => {
|
|
|
2196
2188
|
// interactive REPL — ink TUI by default on a real terminal; HARA_TUI=0 forces the classic readline path
|
|
2197
2189
|
const useTui = stdin.isTTY && stdout.isTTY && process.env.HARA_TUI !== "0";
|
|
2198
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
|
+
}
|
|
2199
2198
|
const rl = createInterface({
|
|
2200
2199
|
input: stdin,
|
|
2201
2200
|
output: stdout,
|
package/dist/org/roles.js
CHANGED
|
@@ -22,13 +22,37 @@ export function orgRolesDir() {
|
|
|
22
22
|
export function claudeAgentsDir(cwd) {
|
|
23
23
|
return join(findProjectRoot(cwd), ".claude", "agents");
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
/** Claude-Code tool names → hara tool names, for `.claude/agents` interop. Without this, a CC agent
|
|
26
|
+
* with `tools: Read, Edit, Bash` produced allowTools that matched ZERO hara tools — the role spawned
|
|
27
|
+
* with an empty toolbox. Unknown names pass through verbatim (they may be hara names already). */
|
|
28
|
+
const CLAUDE_TOOL_MAP = {
|
|
29
|
+
read: "read_file",
|
|
30
|
+
edit: "edit_file",
|
|
31
|
+
write: "write_file",
|
|
32
|
+
bash: "bash",
|
|
33
|
+
grep: "grep",
|
|
34
|
+
glob: "glob",
|
|
35
|
+
ls: "ls",
|
|
36
|
+
webfetch: "web_fetch",
|
|
37
|
+
websearch: "web_search",
|
|
38
|
+
agent: "agent",
|
|
39
|
+
task: "agent",
|
|
40
|
+
todowrite: "todo_write",
|
|
41
|
+
notebookedit: "edit_file",
|
|
42
|
+
};
|
|
43
|
+
/** Accept Claude-Code `tools:` (comma string or list) as an alias for hara's allowTools —
|
|
44
|
+
* translating CC tool names to hara's. "All tools" / "*" means unrestricted → undefined. */
|
|
45
|
+
export function claudeTools(v) {
|
|
46
|
+
const raw = Array.isArray(v)
|
|
47
|
+
? v
|
|
48
|
+
: typeof v === "string" && v.trim()
|
|
49
|
+
? v.split(",").map((s) => s.trim()).filter(Boolean)
|
|
50
|
+
: null;
|
|
51
|
+
if (!raw || !raw.length)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (raw.some((t) => /^(\*|all tools?)$/i.test(t)))
|
|
54
|
+
return undefined; // unrestricted
|
|
55
|
+
return raw.map((t) => CLAUDE_TOOL_MAP[t.toLowerCase().replace(/[^a-z]/g, "")] ?? t);
|
|
32
56
|
}
|
|
33
57
|
function parseFrontmatter(text) {
|
|
34
58
|
const m = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/.exec(text);
|
|
@@ -83,7 +107,9 @@ export function loadRoles(cwd) {
|
|
|
83
107
|
description: fm.description || "",
|
|
84
108
|
owns: Array.isArray(fm.owns) ? fm.owns : [],
|
|
85
109
|
rejects: Array.isArray(fm.rejects) ? fm.rejects : [],
|
|
86
|
-
model
|
|
110
|
+
// Claude-Code model ALIASES (sonnet/opus/haiku/inherit) aren't hara model ids — treat as
|
|
111
|
+
// "inherit the session model" rather than passing a string no provider resolves.
|
|
112
|
+
model: fm.model && !/^(sonnet|opus|haiku|inherit)$/i.test(String(fm.model)) ? fm.model : undefined,
|
|
87
113
|
allowTools: Array.isArray(fm.allowTools) ? fm.allowTools : claudeTools(fm.tools),
|
|
88
114
|
denyTools: Array.isArray(fm.denyTools) ? fm.denyTools : undefined,
|
|
89
115
|
system: body,
|
|
@@ -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
|
+
}
|