@nanhara/hara 0.122.5 → 0.122.7
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 +33 -0
- package/README.md +10 -2
- package/dist/agent/loop.js +8 -2
- package/dist/context/mentions.js +10 -1
- package/dist/context/workspace-scope.js +10 -6
- package/dist/index.js +11 -3
- package/dist/serve/server.js +1 -0
- package/dist/serve/sessions.js +3 -2
- package/dist/tools/builtin.js +1 -0
- package/dist/tools/codebase.js +4 -2
- package/dist/tools/edit.js +1 -0
- package/dist/tools/patch.js +1 -0
- package/dist/tools/registry.js +2 -2
- package/dist/tools/search.js +7 -1
- package/dist/tui/InputBox.js +8 -2
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,39 @@ 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.122.7 — 2026-07-15 — isolated build-context parity
|
|
9
|
+
|
|
10
|
+
- **Docker builds carry every package-build dependency.** The image build stage now copies the local-link
|
|
11
|
+
diagnostic script before `npm run build`, fixing the `MODULE_NOT_FOUND` failure that blocked the 0.122.6
|
|
12
|
+
Docker CI and GHCR multi-architecture image while npm and native standalone lanes succeeded.
|
|
13
|
+
- **The gate follows the build graph instead of one remembered filename.** Runtime packaging tests parse every
|
|
14
|
+
`node scripts/...` dependency from the package build command and require a matching Dockerfile `COPY`, so a
|
|
15
|
+
future build helper cannot pass host-only gates while disappearing from an isolated container context.
|
|
16
|
+
- `v0.122.6` remains immutable and is treated as a partial release; 0.122.7 supersedes it after npm, native
|
|
17
|
+
binaries, GitHub Release, and both GHCR architectures are independently verified.
|
|
18
|
+
|
|
19
|
+
## 0.122.6 — 2026-07-15 — resumed continuity and workspace/input hardening
|
|
20
|
+
|
|
21
|
+
- **Resume continues the persisted task instead of rediscovering the workspace.** CLI `--resume`/`-c`,
|
|
22
|
+
headless gateway/cron continuations, Desktop `session.resume`, and session forks tell the model that the
|
|
23
|
+
existing history is already authoritative context. A process restart is no longer a reason to restart the
|
|
24
|
+
task, inventory files, or answer with an unrelated workspace summary.
|
|
25
|
+
- **Home cannot be promoted into a project one child directory at a time.** When Hara is rooted at the user's
|
|
26
|
+
Home, runtime guards now reject `ls`, directory `grep`, `glob`, `codebase_search`, `@directory` expansion,
|
|
27
|
+
fuzzy `@` inventory, and coding mutations even when the model supplies a named child path. Explicit
|
|
28
|
+
single-file reads remain available; launching Hara from that concrete child project restores normal project
|
|
29
|
+
search, completion, and coding behavior.
|
|
30
|
+
- **The regression exercises a disobedient model, not only the prompt.** An end-to-end resumed session uses a
|
|
31
|
+
temporary Home containing a sentinel project, forces the provider to call `ls`, and verifies the tool result
|
|
32
|
+
is a bounded refusal while the persisted history and latest request reach the model without any Home entry
|
|
33
|
+
or file name.
|
|
34
|
+
- **Composer tabs no longer move the cursor away from the text.** Literal tabs render and wrap as one-column
|
|
35
|
+
spaces while the editable and submitted prompt preserves the original tab characters.
|
|
36
|
+
- **Source builds diagnose stale pre-bootstrap npm links.** If an old `npm link` still points `hara` at the
|
|
37
|
+
now-internal non-executable `dist/index.js`, builds and `npm run doctor:local-link` print the owning Node bin
|
|
38
|
+
directory and the exact relink/`rehash` repair instead of suggesting an unsafe `chmod` workaround. Clean npm
|
|
39
|
+
installs and standalone binaries are unaffected.
|
|
40
|
+
|
|
8
41
|
## 0.122.5 — 2026-07-15 — standalone ambient-config security boundary
|
|
9
42
|
|
|
10
43
|
- **Standalone binaries no longer trust the directory they are launched from.** Bun's runtime `.env` and
|
package/README.md
CHANGED
|
@@ -59,6 +59,12 @@ npm install # builds via the prepare script
|
|
|
59
59
|
npm install -g . # or: npm link
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
If a source checkout linked before 0.122.1 later reports `zsh: permission denied: hara`, the old npm
|
|
63
|
+
link is still targeting the now-internal `dist/index.js`. Activate the same Node installation that owns
|
|
64
|
+
the link, run `npm link` again from this directory, then run `rehash`. `npm run build` and
|
|
65
|
+
`npm run doctor:local-link` detect this stale link and print its exact owning bin directory. Do not repair
|
|
66
|
+
it with `chmod`; the guarded executable entry is `runtime-bootstrap.cjs`.
|
|
67
|
+
|
|
62
68
|
## Quickstart
|
|
63
69
|
|
|
64
70
|
```bash
|
|
@@ -177,8 +183,9 @@ hara -m glm-5 # pick a model
|
|
|
177
183
|
|
|
178
184
|
Run Hara from a project directory. When the current directory resolves to your Home root, Hara does not treat
|
|
179
185
|
the whole Home tree as a repository: project init/index and default recursive grep/glob/codebase inventory are
|
|
180
|
-
disabled with a `cd /path/to/project` hint.
|
|
181
|
-
|
|
186
|
+
disabled with a `cd /path/to/project` hint. Directory listing and child-directory recursion are also refused so
|
|
187
|
+
the model cannot promote a discovered Home folder into an implicit project; explicitly named single files still
|
|
188
|
+
work. Resuming a session reuses its persisted conversation and must not trigger workspace rediscovery.
|
|
182
189
|
|
|
183
190
|
For automation, `--schema` accepts inline JSON Schema or a schema file. The model must return through the
|
|
184
191
|
validated `structured_output` tool; on success stdout contains only the JSON value, while diagnostics go to
|
|
@@ -314,6 +321,7 @@ turn, or **`/undo`** to revert the last edit. In-session **`/diff`**, **`/review
|
|
|
314
321
|
- **Type-ahead steering**: keep typing while hara works — your message is held, then **folded into the next model call** (not deferred to a new turn), so a clarification or "also do X" course-corrects the task already in flight (codex-style). Messages typed after the final step start a fresh turn; **Esc** drops the queue and stops.
|
|
315
322
|
- **Project context**: auto-loads `AGENTS.md` (the cross-tool standard) walking up to the repo root; `hara init` writes one by analyzing the repo.
|
|
316
323
|
- **`@file` mentions**: attach file contents to a message (`@path`); Tab-completes with a **fuzzy** matcher over the project (subdirs, git-tracked + untracked) — `@idx` → `src/index.ts`. `@<dir>` loads a directory listing, `@src/`+Tab drills into a folder, and mistyped tool/file paths get a "did you mean" suggestion.
|
|
324
|
+
- **Explicit workspace boundary**: launching at Home does not inventory its directories or permit coding mutations. Start Hara from a concrete project to enable search, `@` completion, shell/external agents, and file edits; explicitly named single-file reads remain available at Home.
|
|
317
325
|
- **Multi-provider**: Anthropic (Claude) or any OpenAI-compatible endpoint (Qwen/DashScope, GLM, Kimi, OpenAI) — **all streamed live**.
|
|
318
326
|
- **Chat gateway**: drive your local hara from **Telegram · WeChat · Discord · Feishu/Lark · Slack · Mattermost · Matrix · DingTalk · WeCom · Signal**. The daemon connects out (no public webhook), with per-chat sessions, project roaming (`/cd`), agent switching (`/agent`), and **two-way images on byte-upload-capable platforms**. Setup, platform capability details, and the group-flow security model: **[docs/gateway.md](docs/gateway.md)**.
|
|
319
327
|
|
package/dist/agent/loop.js
CHANGED
|
@@ -127,11 +127,17 @@ function gatewayNote() {
|
|
|
127
127
|
`PLAIN TEXT ONLY: chat bubbles render markdown literally — never use **bold**, # headers, backticks, tables, ` +
|
|
128
128
|
`or [text](url); write list items as "- " lines and links as bare URLs.`);
|
|
129
129
|
}
|
|
130
|
-
|
|
130
|
+
const CONTINUATION_SYSTEM = "# Existing-session continuity\n" +
|
|
131
|
+
"This turn continues a persisted conversation. Its history is already the authoritative context: do not restart the task, " +
|
|
132
|
+
"re-inventory the workspace, or summarize files merely to understand what happened before. Follow the latest user request " +
|
|
133
|
+
"and reuse prior conclusions and tool results. Inspect files only when the latest request requires it. If the working " +
|
|
134
|
+
"directory is Home, ask the user to start Hara from a concrete project instead of enumerating Home or its children.";
|
|
135
|
+
function composeSystem(cwd, projectContext, override, memory, continuationSession = false) {
|
|
131
136
|
const head = override ? `${override}\n\nWorking directory: ${cwd}` : HARA_SYSTEM(cwd);
|
|
132
137
|
const skills = skillsDigest(cwd);
|
|
133
138
|
return (head +
|
|
134
139
|
gatewayNote() +
|
|
140
|
+
(continuationSession ? `\n\n${CONTINUATION_SYSTEM}` : "") +
|
|
135
141
|
(projectContext ? `\n\n# Project context (AGENTS.md)\n${projectContext}` : "") +
|
|
136
142
|
(memory ? `\n\n# Memory (durable — facts/decisions/prefs you've saved; use memory_search/get for more)\n${memory}` : "") +
|
|
137
143
|
(skills ? `\n\n# Skills (capabilities you can load — call the \`skill\` tool with the id for full instructions before using one)\n${skills}` : ""));
|
|
@@ -421,7 +427,7 @@ async function runAgentInner(history, opts, life) {
|
|
|
421
427
|
return { text: "", toolUses: [], stop: "error", errorMsg: "interrupted" };
|
|
422
428
|
}
|
|
423
429
|
return activeProvider.turn({
|
|
424
|
-
system: composeSystem(ctx.cwd, opts.projectContext, opts.systemOverride, opts.memory),
|
|
430
|
+
system: composeSystem(ctx.cwd, opts.projectContext, opts.systemOverride, opts.memory, opts.continuationSession),
|
|
425
431
|
history,
|
|
426
432
|
tools: specs,
|
|
427
433
|
// Any stream chunk keeps the connection considered alive — even suppressed reasoning_content, so a
|
package/dist/context/mentions.js
CHANGED
|
@@ -7,7 +7,7 @@ import { fuzzyRank } from "../fuzzy.js";
|
|
|
7
7
|
import { mediaTypeFor } from "../images.js";
|
|
8
8
|
import { readModelContextPrefixSync } from "../fs-read.js";
|
|
9
9
|
import { sensitiveFileError } from "../security/sensitive-files.js";
|
|
10
|
-
import { recursiveRootContainsHome, recursiveHomeSearchError } from "./workspace-scope.js";
|
|
10
|
+
import { homeWorkspaceDirectoryScanError, isHomeWorkspace, recursiveRootContainsHome, recursiveHomeSearchError, } from "./workspace-scope.js";
|
|
11
11
|
const MAX_FILE = 50_000;
|
|
12
12
|
// @ at start-of-string or after whitespace; capture a path with no spaces/@ (avoids emails like a@b.com)
|
|
13
13
|
const MENTION_RE = /(?:^|\s)@([^\s@]+)/g;
|
|
@@ -108,6 +108,8 @@ async function expandRefAsync(ref, cwd, options) {
|
|
|
108
108
|
return `\nReferenced file \`${ref}\`:\n\`\`\`\n${txt}\n\`\`\`\n`;
|
|
109
109
|
}
|
|
110
110
|
if (st.isDirectory()) {
|
|
111
|
+
if (isHomeWorkspace(cwd))
|
|
112
|
+
return homeWorkspaceDirectoryScanError("directory attachment");
|
|
111
113
|
if (recursiveRootContainsHome(abs))
|
|
112
114
|
return recursiveHomeSearchError("directory attachment");
|
|
113
115
|
const inventory = await walkFilesAsync(abs, options);
|
|
@@ -148,6 +150,8 @@ function expandRef(ref, cwd) {
|
|
|
148
150
|
return `\nReferenced file \`${ref}\`:\n\`\`\`\n${txt}\n\`\`\`\n`;
|
|
149
151
|
}
|
|
150
152
|
if (st.isDirectory()) {
|
|
153
|
+
if (isHomeWorkspace(cwd))
|
|
154
|
+
return homeWorkspaceDirectoryScanError("directory attachment");
|
|
151
155
|
if (recursiveRootContainsHome(abs))
|
|
152
156
|
return recursiveHomeSearchError("directory attachment");
|
|
153
157
|
// `@dir` loads a listing of the directory's files (the agent can then read specific ones)
|
|
@@ -188,6 +192,11 @@ export function invalidateFileCandidates(cwd) {
|
|
|
188
192
|
* ranks path-prefix and basename matches first. Directories carry a trailing `/`.
|
|
189
193
|
*/
|
|
190
194
|
export function fileCandidates(cwd, query, limit = 25) {
|
|
195
|
+
// Autocomplete is also directory discovery. Do not populate the cache or expose even top-level
|
|
196
|
+
// names when the user launched Hara at Home; the user establishes project scope by starting in a
|
|
197
|
+
// concrete child directory. Canonical comparison closes symlink aliases of Home.
|
|
198
|
+
if (isHomeWorkspace(cwd))
|
|
199
|
+
return [];
|
|
191
200
|
const entries = projectEntries(cwd);
|
|
192
201
|
if (!query) {
|
|
193
202
|
// bare `@`: top-level entries, directories first
|
|
@@ -32,8 +32,13 @@ export function homeWorkspaceActionError(action) {
|
|
|
32
32
|
}
|
|
33
33
|
export function recursiveHomeSearchError(tool) {
|
|
34
34
|
return (`Error: ${tool} will not recursively scan the home directory. ` +
|
|
35
|
-
"Run Hara from a project (`cd /path/to/project`)
|
|
36
|
-
|
|
35
|
+
"Run Hara from a project (`cd /path/to/project`). Explicit single-file reads remain available.");
|
|
36
|
+
}
|
|
37
|
+
/** A model rooted at Home must not discover a child and silently promote it into the project scope. The
|
|
38
|
+
* user establishes that scope by launching Hara from the concrete project directory. */
|
|
39
|
+
export function homeWorkspaceDirectoryScanError(tool) {
|
|
40
|
+
return (`Error: ${tool} will not enumerate or recursively scan directories while Hara is rooted at the home directory. ` +
|
|
41
|
+
"Run `cd /path/to/project` and start Hara there. Explicit single-file reads remain available.");
|
|
37
42
|
}
|
|
38
43
|
/** Injected into model context when Hara was intentionally launched at ~/. Runtime checks enforce the same
|
|
39
44
|
* policy, but guidance avoids wasting turns on calls that are guaranteed to be rejected. */
|
|
@@ -42,8 +47,7 @@ export function homeWorkspaceGuidance(cwd) {
|
|
|
42
47
|
return "";
|
|
43
48
|
return ("# Home-directory workspace boundary\n" +
|
|
44
49
|
"The working directory resolves to the user's home directory, which is not an implicit project. " +
|
|
45
|
-
"Do not initialize a project, build a repository index, run shell/external
|
|
46
|
-
"grep/glob/search
|
|
47
|
-
"Ask the user to run `cd /path/to/project` for project work.
|
|
48
|
-
"and explicitly named child directories remain available.");
|
|
50
|
+
"Do not initialize a project, create or modify files, build a repository index, run shell/external " +
|
|
51
|
+
"executable tools, enumerate directories, or grep/glob/search Home or one of its child directories. " +
|
|
52
|
+
"Ask the user to run `cd /path/to/project` for project work. Only explicitly named single-file reads remain available.");
|
|
49
53
|
}
|
package/dist/index.js
CHANGED
|
@@ -2916,6 +2916,7 @@ program.action(async (opts) => {
|
|
|
2916
2916
|
// saves it back — so `hara -p … --resume <id>` continues a thread (used by cron, scripts, the chat gateway).
|
|
2917
2917
|
// Plain `hara -p` stays stateless. A --resume id with no match is created WITH that id (stable per caller).
|
|
2918
2918
|
let meta = null;
|
|
2919
|
+
let continuationSession = false;
|
|
2919
2920
|
const history = [];
|
|
2920
2921
|
if (opts.resume || opts.continue) {
|
|
2921
2922
|
const resumeArg = opts.resume ? String(opts.resume) : undefined;
|
|
@@ -2945,6 +2946,7 @@ program.action(async (opts) => {
|
|
|
2945
2946
|
// Re-read only after acquiring the single-writer lock. Loading first would leave a race window where
|
|
2946
2947
|
// another gateway/cron process appends history that this stale snapshot later overwrites.
|
|
2947
2948
|
const prior = loadSession(rid);
|
|
2949
|
+
continuationSession = Boolean(prior?.history.length);
|
|
2948
2950
|
if (sessionFileExists(rid) && !prior) {
|
|
2949
2951
|
process.stderr.write(`hara: session ${shortId(rid)} exists but is unreadable or corrupt; refusing to overwrite it. Inspect ~/.hara/sessions/${rid}.json.\n`);
|
|
2950
2952
|
process.exitCode = 2;
|
|
@@ -3106,6 +3108,7 @@ program.action(async (opts) => {
|
|
|
3106
3108
|
confirm: async () => true,
|
|
3107
3109
|
projectContext,
|
|
3108
3110
|
memory: memoryDigest(cwd),
|
|
3111
|
+
continuationSession,
|
|
3109
3112
|
...(roleOverride ? { systemOverride: roleOverride } : {}),
|
|
3110
3113
|
...(headlessToolFilter ? { toolFilter: headlessToolFilter } : {}),
|
|
3111
3114
|
hooks: headlessHooks,
|
|
@@ -3347,6 +3350,7 @@ program.action(async (opts) => {
|
|
|
3347
3350
|
}
|
|
3348
3351
|
}
|
|
3349
3352
|
const history = resumed?.history ? [...resumed.history] : [];
|
|
3353
|
+
let continuationSession = Boolean(resumed?.history.length);
|
|
3350
3354
|
const memorySnap = memoryDigest(cwd); // durable memory, read once (frozen snapshot)
|
|
3351
3355
|
const buildMemory = () => (meta.workingSet?.length ? `## Working memory (this task)\n${meta.workingSet.map((w) => `- ${w}`).join("\n")}\n\n` : "") + memorySnap;
|
|
3352
3356
|
if (resumed)
|
|
@@ -3868,6 +3872,7 @@ program.action(async (opts) => {
|
|
|
3868
3872
|
return void h.sink.notice(getTools().map((t) => `${t.name}${t.kind !== "read" ? " *" : ""} — ${t.description}`).join("\n"));
|
|
3869
3873
|
if (nm === "reset" || nm === "clear") {
|
|
3870
3874
|
history.length = 0;
|
|
3875
|
+
continuationSession = false;
|
|
3871
3876
|
recalledContext = "";
|
|
3872
3877
|
resetReachability(); // fresh start — drop any "host unreachable" marks (network may be fixed)
|
|
3873
3878
|
resetRepeatGuard(); // …and the repeated-failure streaks (the user may have fixed the cause)
|
|
@@ -4105,7 +4110,7 @@ program.action(async (opts) => {
|
|
|
4105
4110
|
const __skApproval = h.approval === "plan" ? "suggest" : h.approval;
|
|
4106
4111
|
let skillOutcome;
|
|
4107
4112
|
try {
|
|
4108
|
-
skillOutcome = await runAgent(history, { provider, ctx: { cwd, sandbox, spawn, ui: { text: h.sink.assistantDelta, reasoning: h.sink.reasoningDelta, tool: h.sink.tool, diff: h.sink.diff, notice: h.sink.notice }, ask: h.ask, describeImage: describeScreenshot, locate: locateScreenshot }, approval: __skApproval, confirm: h.confirm, autoApprove, projectContext, memory: buildMemory(), stats, signal: h.signal, fallback: fbOpt, guardian: guardianOpt, ...agentRunLimits(cfg) });
|
|
4113
|
+
skillOutcome = await runAgent(history, { provider, ctx: { cwd, sandbox, spawn, ui: { text: h.sink.assistantDelta, reasoning: h.sink.reasoningDelta, tool: h.sink.tool, diff: h.sink.diff, notice: h.sink.notice }, ask: h.ask, describeImage: describeScreenshot, locate: locateScreenshot }, approval: __skApproval, confirm: h.confirm, autoApprove, projectContext, memory: buildMemory(), continuationSession, stats, signal: h.signal, fallback: fbOpt, guardian: guardianOpt, ...agentRunLimits(cfg) });
|
|
4109
4114
|
}
|
|
4110
4115
|
catch (e) {
|
|
4111
4116
|
h.sink.notice(`[error] ${e?.message ?? e}`);
|
|
@@ -4181,6 +4186,7 @@ program.action(async (opts) => {
|
|
|
4181
4186
|
systemOverride: PLAN_SYSTEM,
|
|
4182
4187
|
memory: buildMemory(),
|
|
4183
4188
|
projectContext,
|
|
4189
|
+
continuationSession,
|
|
4184
4190
|
stats,
|
|
4185
4191
|
signal: h.signal,
|
|
4186
4192
|
pendingInput,
|
|
@@ -4226,6 +4232,7 @@ program.action(async (opts) => {
|
|
|
4226
4232
|
confirm: h.confirm,
|
|
4227
4233
|
autoApprove,
|
|
4228
4234
|
projectContext,
|
|
4235
|
+
continuationSession,
|
|
4229
4236
|
stats,
|
|
4230
4237
|
signal: h.signal,
|
|
4231
4238
|
pendingInput,
|
|
@@ -4260,6 +4267,7 @@ program.action(async (opts) => {
|
|
|
4260
4267
|
confirm: h.confirm,
|
|
4261
4268
|
autoApprove,
|
|
4262
4269
|
projectContext,
|
|
4270
|
+
continuationSession,
|
|
4263
4271
|
stats,
|
|
4264
4272
|
signal: h.signal,
|
|
4265
4273
|
pendingInput,
|
|
@@ -4331,7 +4339,7 @@ program.action(async (opts) => {
|
|
|
4331
4339
|
currentTurn = skillTurn;
|
|
4332
4340
|
let skillOutcome;
|
|
4333
4341
|
try {
|
|
4334
|
-
skillOutcome = await runAgent(history, { provider, ctx: { cwd, sandbox, spawn, ask: askUser }, approval, confirm, autoApprove, projectContext, memory: buildMemory(), stats, signal: skillTurn.signal, fallback: fbOpt, guardian: guardianOpt, ...agentRunLimits(cfg) });
|
|
4342
|
+
skillOutcome = await runAgent(history, { provider, ctx: { cwd, sandbox, spawn, ask: askUser }, approval, confirm, autoApprove, projectContext, memory: buildMemory(), continuationSession, stats, signal: skillTurn.signal, fallback: fbOpt, guardian: guardianOpt, ...agentRunLimits(cfg) });
|
|
4335
4343
|
}
|
|
4336
4344
|
catch (e) {
|
|
4337
4345
|
out(c.red(`\n[error] ${e.message}\n`));
|
|
@@ -4369,7 +4377,7 @@ program.action(async (opts) => {
|
|
|
4369
4377
|
const t0 = Date.now();
|
|
4370
4378
|
let turnOutcome;
|
|
4371
4379
|
try {
|
|
4372
|
-
turnOutcome = await runAgent(history, { provider, ctx: { cwd, sandbox, spawn, ask: askUser }, approval, confirm, autoApprove, projectContext, memory: buildMemory(), stats, signal: turnController.signal, fallback: fbOpt, guardian: guardianOpt, ...agentRunLimits(cfg) });
|
|
4380
|
+
turnOutcome = await runAgent(history, { provider, ctx: { cwd, sandbox, spawn, ask: askUser }, approval, confirm, autoApprove, projectContext, memory: buildMemory(), continuationSession, stats, signal: turnController.signal, fallback: fbOpt, guardian: guardianOpt, ...agentRunLimits(cfg) });
|
|
4373
4381
|
}
|
|
4374
4382
|
catch (e) {
|
|
4375
4383
|
out(c.red(`\n[error] ${e.message}\n`));
|
package/dist/serve/server.js
CHANGED
|
@@ -375,6 +375,7 @@ export async function startServe(opts, deps) {
|
|
|
375
375
|
autoApprove: s.autoApprove,
|
|
376
376
|
projectContext: s.projectContext,
|
|
377
377
|
memory: memoryDigest(s.meta.cwd),
|
|
378
|
+
continuationSession: s.continuationSession,
|
|
378
379
|
stats: s.stats,
|
|
379
380
|
signal: turnAbort.signal,
|
|
380
381
|
onProviderTurn: (turn) => {
|
package/dist/serve/sessions.js
CHANGED
|
@@ -47,7 +47,7 @@ export class SessionHub {
|
|
|
47
47
|
const lock = this.store.acquire(meta.id); // fresh UUID, but filesystem errors must still fail closed
|
|
48
48
|
if (!lock.ok)
|
|
49
49
|
throw new Error(`could not acquire session lock for ${meta.id}${lock.pid ? ` (held by pid ${lock.pid})` : ""}`);
|
|
50
|
-
const s = { meta, history: [], provider: o.provider, approval: o.approval, autoApprove: new Set(), stats: { input: 0, output: 0 }, projectContext: o.projectContext, busy: false, configuring: false, pendingProviderTurns: 0, pendingToolRuns: 0, abort: null };
|
|
50
|
+
const s = { meta, history: [], provider: o.provider, approval: o.approval, autoApprove: new Set(), stats: { input: 0, output: 0 }, projectContext: o.projectContext, continuationSession: false, busy: false, configuring: false, pendingProviderTurns: 0, pendingToolRuns: 0, abort: null };
|
|
51
51
|
try {
|
|
52
52
|
this.sessions.set(meta.id, s);
|
|
53
53
|
this.store.save(meta, []); // an empty newly-created thread must survive restart and appear in lists
|
|
@@ -76,7 +76,7 @@ export class SessionHub {
|
|
|
76
76
|
return { missing: true };
|
|
77
77
|
// Credential/provider routing is live, while the model remains the session's explicit pin.
|
|
78
78
|
prior.meta.provider = o.provider.id;
|
|
79
|
-
const s = { meta: prior.meta, history: [...prior.history], provider: o.provider, approval: o.approval, autoApprove: new Set(), stats: { input: 0, output: 0 }, projectContext: o.projectContext, busy: false, configuring: false, pendingProviderTurns: 0, pendingToolRuns: 0, abort: null, effort: prior.meta.effort };
|
|
79
|
+
const s = { meta: prior.meta, history: [...prior.history], provider: o.provider, approval: o.approval, autoApprove: new Set(), stats: { input: 0, output: 0 }, projectContext: o.projectContext, continuationSession: prior.history.length > 0, busy: false, configuring: false, pendingProviderTurns: 0, pendingToolRuns: 0, abort: null, effort: prior.meta.effort };
|
|
80
80
|
this.sessions.set(id, s);
|
|
81
81
|
keepLock = true; // live session owns it until delete/releaseAll
|
|
82
82
|
return { session: s };
|
|
@@ -177,6 +177,7 @@ export class SessionHub {
|
|
|
177
177
|
autoApprove: new Set(),
|
|
178
178
|
stats: { input: 0, output: 0 },
|
|
179
179
|
projectContext: o.projectContext,
|
|
180
|
+
continuationSession: src.history.length > 0,
|
|
180
181
|
busy: false,
|
|
181
182
|
configuring: false,
|
|
182
183
|
pendingProviderTurns: 0,
|
package/dist/tools/builtin.js
CHANGED
package/dist/tools/codebase.js
CHANGED
|
@@ -10,7 +10,7 @@ import { findProjectRoot } from "../context/agents-md.js";
|
|
|
10
10
|
import { loadConfig } from "../config.js";
|
|
11
11
|
import { getEmbedder } from "../search/embed.js";
|
|
12
12
|
import { queryIndex, indexExists } from "../search/semindex.js";
|
|
13
|
-
import { recursiveRootContainsHome, recursiveHomeSearchError } from "../context/workspace-scope.js";
|
|
13
|
+
import { homeWorkspaceDirectoryScanError, isHomeWorkspace, recursiveRootContainsHome, recursiveHomeSearchError, } from "../context/workspace-scope.js";
|
|
14
14
|
import { readVerifiedRegularFileSnapshotSync } from "../fs-read.js";
|
|
15
15
|
const MAX_FILE = 200_000; // skip very large files
|
|
16
16
|
const INVENTORY_TIMEOUT_MS = 2_000;
|
|
@@ -21,7 +21,7 @@ registerTool({
|
|
|
21
21
|
description: "Find code in THIS project relevant to a natural-language query — ranked by relevance (not exact match). " +
|
|
22
22
|
"Use it to locate similar/related code while working ('where is auth handled?', 'retry logic'); use grep " +
|
|
23
23
|
"for exact strings/regex. Returns the top files with their most relevant snippet (file:line). When Hara " +
|
|
24
|
-
"was started in the home directory,
|
|
24
|
+
"was started in the home directory, cd to a specific project before searching.",
|
|
25
25
|
input_schema: {
|
|
26
26
|
type: "object",
|
|
27
27
|
properties: {
|
|
@@ -43,6 +43,8 @@ registerTool({
|
|
|
43
43
|
const requestedRoot = typeof input.path === "string" && input.path.trim()
|
|
44
44
|
? (isAbsolute(input.path) ? input.path : resolve(ctx.cwd, input.path))
|
|
45
45
|
: ctx.cwd;
|
|
46
|
+
if (isHomeWorkspace(ctx.cwd))
|
|
47
|
+
return homeWorkspaceDirectoryScanError("codebase_search");
|
|
46
48
|
try {
|
|
47
49
|
if (!statSync(requestedRoot).isDirectory())
|
|
48
50
|
return `Error: codebase_search path is not a directory: ${input.path ?? "."}`;
|
package/dist/tools/edit.js
CHANGED
package/dist/tools/patch.js
CHANGED
package/dist/tools/registry.js
CHANGED
|
@@ -25,8 +25,8 @@ export function registerTool(t) {
|
|
|
25
25
|
if (ctx.signal?.aborted) {
|
|
26
26
|
return `Error: ${t.name} cancelled before execution because the agent run has ended.`;
|
|
27
27
|
}
|
|
28
|
-
// Only explicitly project-scoped
|
|
29
|
-
// management/delivery side effects, so
|
|
28
|
+
// Only explicitly project-scoped operations are blocked. Tool kinds are also used for safe
|
|
29
|
+
// management/delivery side effects, so a kind must not itself imply a project workspace requirement.
|
|
30
30
|
// Canonical comparison closes Home symlink aliases.
|
|
31
31
|
if (t.requiresProjectWorkspace && isHomeWorkspace(ctx.cwd)) {
|
|
32
32
|
return `Error: ${homeWorkspaceActionError(`run ${t.name}`)}`;
|
package/dist/tools/search.js
CHANGED
|
@@ -7,7 +7,7 @@ import { registerTool } from "./registry.js";
|
|
|
7
7
|
import { IGNORE_DIRS, walkFilesAsync } from "../fs-walk.js";
|
|
8
8
|
import { isSensitiveFilePath, sensitiveFileError, sensitiveFilesAllowed, SENSITIVE_SEARCH_GLOBS, } from "../security/sensitive-files.js";
|
|
9
9
|
import { toolSubprocessEnv } from "../security/subprocess-env.js";
|
|
10
|
-
import { recursiveRootContainsHome, recursiveHomeSearchError } from "../context/workspace-scope.js";
|
|
10
|
+
import { homeWorkspaceDirectoryScanError, isHomeWorkspace, recursiveRootContainsHome, recursiveHomeSearchError, } from "../context/workspace-scope.js";
|
|
11
11
|
const MAX_OUT = 60_000;
|
|
12
12
|
const MAX_MATCHES = 300;
|
|
13
13
|
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
@@ -538,6 +538,8 @@ registerTool({
|
|
|
538
538
|
catch {
|
|
539
539
|
return `Error: no such path: ${input.path ?? "."}`;
|
|
540
540
|
}
|
|
541
|
+
if (!isFile && isHomeWorkspace(ctx.cwd))
|
|
542
|
+
return homeWorkspaceDirectoryScanError("grep");
|
|
541
543
|
if (!isFile && recursiveRootContainsHome(root))
|
|
542
544
|
return recursiveHomeSearchError("grep");
|
|
543
545
|
const searchArgs = [
|
|
@@ -605,6 +607,8 @@ registerTool({
|
|
|
605
607
|
const denied = sensitiveFileError(root, "search");
|
|
606
608
|
if (denied)
|
|
607
609
|
return denied;
|
|
610
|
+
if (isHomeWorkspace(ctx.cwd))
|
|
611
|
+
return homeWorkspaceDirectoryScanError("glob");
|
|
608
612
|
if (recursiveRootContainsHome(root))
|
|
609
613
|
return recursiveHomeSearchError("glob");
|
|
610
614
|
const pattern = typeof input.pattern === "string" ? input.pattern : "";
|
|
@@ -665,6 +669,8 @@ registerTool({
|
|
|
665
669
|
},
|
|
666
670
|
kind: "read",
|
|
667
671
|
async run(input, ctx) {
|
|
672
|
+
if (isHomeWorkspace(ctx.cwd))
|
|
673
|
+
return homeWorkspaceDirectoryScanError("ls");
|
|
668
674
|
const dir = absOf(input.path, ctx.cwd);
|
|
669
675
|
const denied = sensitiveFileError(dir, "list");
|
|
670
676
|
if (denied)
|
package/dist/tui/InputBox.js
CHANGED
|
@@ -140,6 +140,12 @@ export function cells(s) {
|
|
|
140
140
|
n += charCells(ch);
|
|
141
141
|
return n;
|
|
142
142
|
}
|
|
143
|
+
/** One-cell display representation for composer text. The editable/submitted value keeps literal tabs,
|
|
144
|
+
* while wrapping, cursor math, and Ink all see the same one-cell glyph. Removing newlines here preserves
|
|
145
|
+
* renderRow's invariant that hard line breaks are represented by rows rather than nested Text output. */
|
|
146
|
+
export function composerTextForDisplay(text) {
|
|
147
|
+
return text.replace(/\n/g, "").replace(/\t/g, " ");
|
|
148
|
+
}
|
|
143
149
|
/** Wrap `value` into rows that each fit within `cols` cells, breaking on spaces where possible but
|
|
144
150
|
* never inside an `[Image #N]` token. Deterministic (no reliance on ink's soft-wrap) so wrapped rows
|
|
145
151
|
* align under a stable gutter and the cursor position is exact. Always returns at least one row. */
|
|
@@ -230,7 +236,7 @@ export function wrapRows(value, cols) {
|
|
|
230
236
|
function renderRow(value, row, cursor, showCursor, isLastRow, keyPrefix) {
|
|
231
237
|
// `\n` chars are row SEPARATORS (wrapRows already broke the row on them) — never render them, or ink
|
|
232
238
|
// would insert a second line break and desync the deterministic layout.
|
|
233
|
-
const seg = (token, text, k) => token ? (_jsx(Text, { backgroundColor: "magenta", color: "white", children: text
|
|
239
|
+
const seg = (token, text, k) => token ? (_jsx(Text, { backgroundColor: "magenta", color: "white", children: composerTextForDisplay(text) }, k)) : (_jsx(Text, { children: composerTextForDisplay(text) }, k));
|
|
234
240
|
// Segments intersected with this row's [start,end) range.
|
|
235
241
|
const parts = segmentize(value);
|
|
236
242
|
const nodes = [];
|
|
@@ -250,7 +256,7 @@ function renderRow(value, row, cursor, showCursor, isLastRow, keyPrefix) {
|
|
|
250
256
|
const rel = cursor - from;
|
|
251
257
|
if (rel > 0)
|
|
252
258
|
nodes.push(seg(p.token, slice.slice(0, rel), `${keyPrefix}s${ki++}`));
|
|
253
|
-
nodes.push(_jsx(Text, { inverse: true, children: slice[rel] === "\n" ? " " : slice[rel] }, `${keyPrefix}c${ki++}`));
|
|
259
|
+
nodes.push(_jsx(Text, { inverse: true, children: slice[rel] === "\n" || slice[rel] === "\t" ? " " : slice[rel] }, `${keyPrefix}c${ki++}`));
|
|
254
260
|
if (rel + 1 < slice.length)
|
|
255
261
|
nodes.push(seg(p.token, slice.slice(rel + 1), `${keyPrefix}e${ki++}`));
|
|
256
262
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanhara/hara",
|
|
3
|
-
"version": "0.122.
|
|
3
|
+
"version": "0.122.7",
|
|
4
4
|
"description": "hara — a coding agent CLI that runs like an engineering org.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"hara": "runtime-bootstrap.cjs"
|
|
@@ -39,8 +39,9 @@
|
|
|
39
39
|
"node": ">=22.12.0"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
-
"build": "tsc && node scripts/normalize-dist-modes.mjs",
|
|
42
|
+
"build": "tsc && node scripts/normalize-dist-modes.mjs && node scripts/check-local-bin-link.mjs",
|
|
43
43
|
"prepare": "npm run build",
|
|
44
|
+
"doctor:local-link": "node scripts/check-local-bin-link.mjs",
|
|
44
45
|
"dev": "tsx src/cli.ts",
|
|
45
46
|
"start": "node runtime-bootstrap.cjs",
|
|
46
47
|
"test": "npm run build && node --test --test-timeout=120000 test/*.test.mjs",
|