@nanhara/hara 0.122.0 → 0.122.2
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 +62 -0
- package/README.md +24 -11
- package/SECURITY.md +48 -9
- package/dist/agent/loop.js +33 -10
- package/dist/agent/touched.js +4 -0
- package/dist/checkpoints.js +103 -17
- package/dist/cli.js +16 -0
- package/dist/config.js +141 -12
- package/dist/context/agents-md.js +44 -9
- package/dist/context/mentions.js +10 -4
- package/dist/context/subdir-hints.js +40 -7
- package/dist/cron/runner.js +372 -37
- package/dist/cron/store.js +11 -3
- package/dist/exec/jobs.js +88 -20
- package/dist/fs-read.js +318 -3
- package/dist/fs-walk.js +8 -2
- package/dist/fs-write.js +197 -11
- package/dist/gateway/discord.js +2 -4
- package/dist/gateway/feishu.js +4 -6
- package/dist/gateway/flows-pending.js +38 -31
- package/dist/gateway/matrix.js +3 -5
- package/dist/gateway/mattermost.js +2 -4
- package/dist/gateway/outbound-files.js +379 -0
- package/dist/gateway/serve.js +121 -73
- package/dist/gateway/signal.js +4 -6
- package/dist/gateway/slack.js +4 -6
- package/dist/gateway/telegram.js +3 -5
- package/dist/gateway/tmux-routes.js +11 -3
- package/dist/gateway/wecom.js +7 -8
- package/dist/gateway/weixin.js +22 -12
- package/dist/hooks.js +12 -6
- package/dist/index.js +142 -61
- package/dist/mcp/client.js +164 -12
- package/dist/memory/store.js +68 -22
- package/dist/org/planner.js +36 -10
- package/dist/org/review-chain.js +360 -24
- package/dist/org/roles.js +4 -2
- package/dist/profile/profile.js +152 -27
- package/dist/recall.js +4 -2
- package/dist/runtime.js +37 -0
- package/dist/sandbox.js +142 -33
- package/dist/search/semindex.js +182 -53
- package/dist/search/zvec-store.js +121 -42
- package/dist/security/permissions.js +326 -19
- package/dist/security/private-state.js +299 -0
- package/dist/security/project-trust.js +6 -0
- package/dist/security/sensitive-files.js +723 -0
- package/dist/security/subprocess-env.js +210 -0
- package/dist/serve/server.js +2 -1
- package/dist/skills/skills.js +16 -7
- package/dist/tools/builtin.js +53 -27
- package/dist/tools/cron.js +6 -0
- package/dist/tools/edit.js +15 -5
- package/dist/tools/external_agent.js +110 -16
- package/dist/tools/memory.js +38 -8
- package/dist/tools/patch.js +37 -17
- package/dist/tools/search.js +100 -40
- package/dist/tools/send.js +11 -5
- package/package.json +11 -10
- package/runtime-bootstrap.cjs +72 -0
package/dist/tools/search.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Search/listing tools — grep (regex across files), glob (path patterns), ls (one directory).
|
|
2
2
|
// All read-only (kind: "read"), so they never hit the approval gate and run in parallel.
|
|
3
|
-
import { readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { lstatSync, readdirSync, statSync } from "node:fs";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
import { basename, dirname, isAbsolute, resolve, join, relative, sep } from "node:path";
|
|
6
6
|
import { registerTool } from "./registry.js";
|
|
7
7
|
import { IGNORE_DIRS, walkFiles } from "../fs-walk.js";
|
|
8
|
+
import { isSensitiveFilePath, sensitiveFileError, sensitiveFilesAllowed, SENSITIVE_SEARCH_GLOBS, } from "../security/sensitive-files.js";
|
|
9
|
+
import { toolSubprocessEnv } from "../security/subprocess-env.js";
|
|
8
10
|
const MAX_OUT = 60_000;
|
|
9
11
|
const MAX_MATCHES = 300;
|
|
10
12
|
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
@@ -99,10 +101,16 @@ function runRipgrep(pattern, root, isFile, cwd, glob, ignoreCase) {
|
|
|
99
101
|
args.push("--glob", `!**/${ignored}/**`);
|
|
100
102
|
if (glob)
|
|
101
103
|
args.push("--glob", glob);
|
|
104
|
+
// Keep protected exclusions last: ripgrep resolves overlapping globs in argument order, so a caller's
|
|
105
|
+
// positive glob must not re-include a safe-looking .env template during a broad directory search.
|
|
106
|
+
if (!isFile && !sensitiveFilesAllowed()) {
|
|
107
|
+
for (const protectedGlob of SENSITIVE_SEARCH_GLOBS)
|
|
108
|
+
args.push("--glob", `!${protectedGlob}`);
|
|
109
|
+
}
|
|
102
110
|
if (ignoreCase)
|
|
103
111
|
args.push("--ignore-case");
|
|
104
112
|
args.push("--", pattern, target);
|
|
105
|
-
const child = spawn("rg", args, { cwd: runCwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
113
|
+
const child = spawn("rg", args, { cwd: runCwd, stdio: ["ignore", "pipe", "pipe"], env: toolSubprocessEnv() });
|
|
106
114
|
const lines = [];
|
|
107
115
|
let matches = 0;
|
|
108
116
|
let scanned = 0;
|
|
@@ -151,6 +159,8 @@ function runRipgrep(pattern, root, isFile, cwd, glob, ignoreCase) {
|
|
|
151
159
|
if (typeof pathText !== "string" || !Number.isFinite(lineNo) || typeof content !== "string")
|
|
152
160
|
return;
|
|
153
161
|
const absolute = resolve(runCwd, pathText);
|
|
162
|
+
if (isSensitiveFilePath(absolute))
|
|
163
|
+
return; // defense if a user glob overrides rg's exclusion ordering
|
|
154
164
|
const shown = toPosix(relative(cwd, absolute)) || toPosix(relative(root, absolute)) || basename(absolute);
|
|
155
165
|
const rendered = `${shown}:${lineNo}: ${content.replace(/\r?\n$/, "").trim().slice(0, 300)}`;
|
|
156
166
|
lines.push(rendered);
|
|
@@ -212,7 +222,7 @@ const NODE_GREP_SOURCE = String.raw `
|
|
|
212
222
|
"use strict";
|
|
213
223
|
const fs = require("node:fs");
|
|
214
224
|
const path = require("node:path");
|
|
215
|
-
const MAX_INPUT_BYTES =
|
|
225
|
+
const MAX_INPUT_BYTES = 16 * 1024 * 1024;
|
|
216
226
|
let input = "";
|
|
217
227
|
let inputRejected = false;
|
|
218
228
|
function execute() {
|
|
@@ -272,33 +282,23 @@ function execute() {
|
|
|
272
282
|
return patternAt === patternParts.length;
|
|
273
283
|
}
|
|
274
284
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
else {
|
|
279
|
-
const stack = [cfg.root];
|
|
280
|
-
while (stack.length && files.length < cfg.maxFiles) {
|
|
281
|
-
const dir = stack.pop();
|
|
282
|
-
let entries;
|
|
283
|
-
try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
|
|
284
|
-
catch (error) { continue; }
|
|
285
|
-
for (const entry of entries) {
|
|
286
|
-
if (files.length >= cfg.maxFiles) break;
|
|
287
|
-
const absolute = path.join(dir, entry.name);
|
|
288
|
-
if (entry.isDirectory()) {
|
|
289
|
-
if (!ignored.has(entry.name)) stack.push(absolute);
|
|
290
|
-
} else if (entry.isFile()) files.push(absolute);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
285
|
+
// Candidate discovery and the complete protected-file policy run in the parent process. The worker gets
|
|
286
|
+
// only paths whose exact identity was already approved, then verifies that identity again on the opened fd.
|
|
287
|
+
const files = Array.isArray(cfg.files) ? cfg.files : [];
|
|
294
288
|
|
|
295
289
|
const readBuffer = Buffer.allocUnsafe(cfg.maxFileBytes + 1);
|
|
296
|
-
function
|
|
290
|
+
function sameIdentity(info, candidate) {
|
|
291
|
+
return String(info.dev) === candidate.dev && String(info.ino) === candidate.ino;
|
|
292
|
+
}
|
|
293
|
+
function safeReadText(candidate) {
|
|
294
|
+
const absolute = candidate.path;
|
|
297
295
|
let fd;
|
|
298
296
|
try {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
297
|
+
const before = fs.lstatSync(absolute, { bigint: true });
|
|
298
|
+
if (!before.isFile() || before.nlink !== 1n || !sameIdentity(before, candidate)) return null;
|
|
299
|
+
fd = fs.openSync(absolute, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK || 0) | (fs.constants.O_NOFOLLOW || 0));
|
|
300
|
+
const info = fs.fstatSync(fd, { bigint: true });
|
|
301
|
+
if (!info.isFile() || info.nlink !== 1n || !sameIdentity(info, candidate) || info.size > BigInt(cfg.maxFileBytes)) return null;
|
|
302
302
|
let total = 0;
|
|
303
303
|
while (total <= cfg.maxFileBytes) {
|
|
304
304
|
const count = fs.readSync(fd, readBuffer, total, cfg.maxFileBytes + 1 - total, total);
|
|
@@ -306,6 +306,10 @@ function execute() {
|
|
|
306
306
|
total += count;
|
|
307
307
|
}
|
|
308
308
|
if (total > cfg.maxFileBytes) return null;
|
|
309
|
+
const latest = fs.fstatSync(fd, { bigint: true });
|
|
310
|
+
const current = fs.lstatSync(absolute, { bigint: true });
|
|
311
|
+
if (!latest.isFile() || latest.nlink !== 1n || !sameIdentity(latest, candidate) || !sameIdentity(current, candidate)) return null;
|
|
312
|
+
if (latest.size !== info.size || latest.mtimeNs !== info.mtimeNs || latest.ctimeNs !== info.ctimeNs) return null;
|
|
309
313
|
const bytes = readBuffer.subarray(0, total);
|
|
310
314
|
if (bytes.subarray(0, Math.min(bytes.length, 4096)).includes(0)) return null;
|
|
311
315
|
return bytes.toString("utf8");
|
|
@@ -320,12 +324,13 @@ function execute() {
|
|
|
320
324
|
let matches = 0;
|
|
321
325
|
let scanned = 0;
|
|
322
326
|
let outputChars = 0;
|
|
323
|
-
let truncated =
|
|
324
|
-
for (const
|
|
327
|
+
let truncated = cfg.candidatesTruncated === true;
|
|
328
|
+
for (const candidate of files) {
|
|
329
|
+
const absolute = candidate.path;
|
|
325
330
|
if (matches >= cfg.maxMatches || outputChars >= cfg.maxOutputChars) { truncated = true; break; }
|
|
326
331
|
const relativeForGlob = path.relative(cfg.isFile ? cfg.cwd : cfg.root, absolute).split(path.sep).join("/");
|
|
327
332
|
if (cfg.glob && !matchesGlob(cfg.glob, relativeForGlob)) continue;
|
|
328
|
-
const text = safeReadText(
|
|
333
|
+
const text = safeReadText(candidate);
|
|
329
334
|
if (text === null) continue;
|
|
330
335
|
scanned++;
|
|
331
336
|
const fileLines = text.split("\n");
|
|
@@ -364,6 +369,30 @@ function normalizeGrepWorkerResult(parsed) {
|
|
|
364
369
|
};
|
|
365
370
|
}
|
|
366
371
|
function runNodeGrep(pattern, root, isFile, cwd, glob, ignoreCase) {
|
|
372
|
+
const allowSensitive = sensitiveFilesAllowed();
|
|
373
|
+
const discovered = isFile ? [root] : walkFiles(root, 8_001).map((path) => join(root, path));
|
|
374
|
+
const candidatesTruncated = !isFile && discovered.length > 8_000;
|
|
375
|
+
const files = [];
|
|
376
|
+
for (const path of discovered.slice(0, 8_000)) {
|
|
377
|
+
// Broad searches intentionally omit every .env variant, including safe templates. Explicitly searching
|
|
378
|
+
// one safe template remains supported, matching the ripgrep branch.
|
|
379
|
+
const securityBase = basename(path).replace(/:.*$/u, "").replace(/[. ]+$/u, "").toLowerCase();
|
|
380
|
+
if (!allowSensitive && !isFile && (securityBase === ".env" || securityBase.startsWith(".env.")))
|
|
381
|
+
continue;
|
|
382
|
+
if (sensitiveFileError(path, "search"))
|
|
383
|
+
continue;
|
|
384
|
+
try {
|
|
385
|
+
const info = lstatSync(path, { bigint: true });
|
|
386
|
+
// A hard-linked safe alias cannot prove where its other name lives. Reject every multi-link candidate
|
|
387
|
+
// before it can cross into the isolated regex worker.
|
|
388
|
+
if (!info.isFile() || info.nlink !== 1n || info.size > BigInt(MAX_FILE_BYTES))
|
|
389
|
+
continue;
|
|
390
|
+
files.push({ path, dev: String(info.dev), ino: String(info.ino) });
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
// Raced, unreadable, symlink, FIFO, or device: omit it rather than weakening the read boundary.
|
|
394
|
+
}
|
|
395
|
+
}
|
|
367
396
|
const payload = JSON.stringify({
|
|
368
397
|
pattern,
|
|
369
398
|
root,
|
|
@@ -371,13 +400,14 @@ function runNodeGrep(pattern, root, isFile, cwd, glob, ignoreCase) {
|
|
|
371
400
|
cwd,
|
|
372
401
|
glob,
|
|
373
402
|
ignoreCase,
|
|
374
|
-
|
|
403
|
+
files,
|
|
404
|
+
candidatesTruncated,
|
|
375
405
|
maxFiles: 8_000,
|
|
376
406
|
maxFileBytes: MAX_FILE_BYTES,
|
|
377
407
|
maxMatches: MAX_MATCHES,
|
|
378
408
|
maxOutputChars: MAX_OUT,
|
|
379
409
|
});
|
|
380
|
-
if (Buffer.byteLength(payload, "utf8") >
|
|
410
|
+
if (Buffer.byteLength(payload, "utf8") > 16 * 1024 * 1024) {
|
|
381
411
|
return Promise.resolve({ kind: "error", lines: [], matches: 0, scanned: 0, truncated: false, error: "grep input exceeds its safety limit" });
|
|
382
412
|
}
|
|
383
413
|
return new Promise((resolveResult) => {
|
|
@@ -385,7 +415,7 @@ function runNodeGrep(pattern, root, isFile, cwd, glob, ignoreCase) {
|
|
|
385
415
|
const args = isBun ? ["-e", NODE_GREP_SOURCE] : ["--max-old-space-size=128", "-e", NODE_GREP_SOURCE];
|
|
386
416
|
// In a Bun --compile build process.execPath is the hara executable. BUN_BE_BUN makes that embedded
|
|
387
417
|
// runtime act as the Bun CLI for this isolated eval subprocess instead of recursively launching hara.
|
|
388
|
-
const env = isBun ? {
|
|
418
|
+
const env = toolSubprocessEnv(process.env, isBun ? { BUN_BE_BUN: "1" } : {});
|
|
389
419
|
const child = spawn(process.execPath, args, { stdio: ["pipe", "pipe", "pipe"], env });
|
|
390
420
|
let stdout = "";
|
|
391
421
|
let stderr = "";
|
|
@@ -460,6 +490,9 @@ registerTool({
|
|
|
460
490
|
if (typeof input.glob === "string" && input.glob.length > MAX_GLOB_CHARS)
|
|
461
491
|
return `Error: grep glob exceeds ${MAX_GLOB_CHARS} characters.`;
|
|
462
492
|
const root = absOf(input.path, ctx.cwd);
|
|
493
|
+
const denied = sensitiveFileError(root, "search");
|
|
494
|
+
if (denied)
|
|
495
|
+
return denied;
|
|
463
496
|
let isFile = false;
|
|
464
497
|
try {
|
|
465
498
|
const info = statSync(root);
|
|
@@ -470,19 +503,35 @@ registerTool({
|
|
|
470
503
|
catch {
|
|
471
504
|
return `Error: no such path: ${input.path ?? "."}`;
|
|
472
505
|
}
|
|
473
|
-
const
|
|
474
|
-
|
|
506
|
+
const searchArgs = [
|
|
507
|
+
pattern,
|
|
508
|
+
root,
|
|
509
|
+
isFile,
|
|
510
|
+
ctx.cwd,
|
|
511
|
+
typeof input.glob === "string" ? input.glob : undefined,
|
|
512
|
+
input.ignore_case === true,
|
|
513
|
+
];
|
|
514
|
+
// ripgrep opens pathnames itself, so a cooperating background process could exchange a candidate after
|
|
515
|
+
// discovery and restore it before the post-filter. While the protected-file policy is active, use the
|
|
516
|
+
// hardened worker that binds device+inode and reads through O_NOFOLLOW fds. The faster rg path is safe to
|
|
517
|
+
// opt back into only with the explicit one-process sensitive-file waiver.
|
|
518
|
+
const primary = sensitiveFilesAllowed()
|
|
519
|
+
? await runRipgrep(...searchArgs)
|
|
520
|
+
: await runNodeGrep(...searchArgs);
|
|
521
|
+
if (primary.kind === "timeout")
|
|
475
522
|
return `Error: grep exceeded its ${GREP_TIMEOUT_MS}ms safety timeout and was stopped.`;
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
523
|
+
if (primary.kind === "invalid")
|
|
524
|
+
return `Error: invalid regex: ${primary.error ?? "invalid pattern"}`;
|
|
525
|
+
let { lines, matches, scanned } = primary;
|
|
526
|
+
let truncated = primary.truncated;
|
|
527
|
+
if (sensitiveFilesAllowed() && (primary.kind === "missing" || primary.kind === "error")) {
|
|
479
528
|
const fallback = await runNodeGrep(pattern, root, isFile, ctx.cwd, typeof input.glob === "string" ? input.glob : undefined, input.ignore_case === true);
|
|
480
529
|
if (fallback.kind === "timeout")
|
|
481
530
|
return `Error: grep exceeded its ${GREP_TIMEOUT_MS}ms safety timeout and was stopped.`;
|
|
482
531
|
if (fallback.kind === "invalid")
|
|
483
532
|
return `Error: invalid regex: ${fallback.error ?? "invalid pattern"}`;
|
|
484
533
|
if (fallback.kind !== "ok") {
|
|
485
|
-
const rgDetail =
|
|
534
|
+
const rgDetail = primary.kind === "error" && primary.error ? `; ripgrep: ${primary.error}` : "";
|
|
486
535
|
return `Error: grep regex failed safely: ${fallback.error ?? "unknown worker error"}${rgDetail}`;
|
|
487
536
|
}
|
|
488
537
|
lines = fallback.lines;
|
|
@@ -490,6 +539,9 @@ registerTool({
|
|
|
490
539
|
scanned = fallback.scanned;
|
|
491
540
|
truncated = fallback.truncated;
|
|
492
541
|
}
|
|
542
|
+
else if (primary.kind !== "ok") {
|
|
543
|
+
return `Error: grep regex failed safely: ${primary.error ?? "unknown worker error"}`;
|
|
544
|
+
}
|
|
493
545
|
if (!lines.length)
|
|
494
546
|
return `No matches for /${input.pattern}/ (scanned ${scanned} files).`;
|
|
495
547
|
let body = lines.join("\n");
|
|
@@ -513,6 +565,9 @@ registerTool({
|
|
|
513
565
|
kind: "read",
|
|
514
566
|
async run(input, ctx) {
|
|
515
567
|
const root = absOf(input.path, ctx.cwd);
|
|
568
|
+
const denied = sensitiveFileError(root, "search");
|
|
569
|
+
if (denied)
|
|
570
|
+
return denied;
|
|
516
571
|
const pattern = typeof input.pattern === "string" ? input.pattern : "";
|
|
517
572
|
if (pattern.length > MAX_GLOB_CHARS)
|
|
518
573
|
return `Error: glob pattern exceeds ${MAX_GLOB_CHARS} characters.`;
|
|
@@ -552,6 +607,9 @@ registerTool({
|
|
|
552
607
|
kind: "read",
|
|
553
608
|
async run(input, ctx) {
|
|
554
609
|
const dir = absOf(input.path, ctx.cwd);
|
|
610
|
+
const denied = sensitiveFileError(dir, "list");
|
|
611
|
+
if (denied)
|
|
612
|
+
return denied;
|
|
555
613
|
let entries;
|
|
556
614
|
try {
|
|
557
615
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -559,8 +617,9 @@ registerTool({
|
|
|
559
617
|
catch (e) {
|
|
560
618
|
return `Error: cannot list ${input.path ?? "."}: ${e.message}`;
|
|
561
619
|
}
|
|
620
|
+
const protectedCount = entries.filter((entry) => !entry.isDirectory() && isSensitiveFilePath(join(dir, entry.name))).length;
|
|
562
621
|
const rows = entries
|
|
563
|
-
.filter((e) => !(e.isDirectory() && e.name === ".git"))
|
|
622
|
+
.filter((e) => !(e.isDirectory() && e.name === ".git") && (e.isDirectory() || !isSensitiveFilePath(join(dir, e.name))))
|
|
564
623
|
.sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name))
|
|
565
624
|
.map((e) => {
|
|
566
625
|
if (e.isDirectory())
|
|
@@ -574,7 +633,8 @@ registerTool({
|
|
|
574
633
|
}
|
|
575
634
|
return ` ${e.name} ${c_size(size)}`;
|
|
576
635
|
});
|
|
577
|
-
|
|
636
|
+
const note = protectedCount ? `(${protectedCount} protected file${protectedCount === 1 ? "" : "s"} hidden)` : "";
|
|
637
|
+
return rows.length ? rows.join("\n") + (note ? `\n${note}` : "") : note || "(empty directory)";
|
|
578
638
|
},
|
|
579
639
|
});
|
|
580
640
|
function c_size(n) {
|
package/dist/tools/send.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { registerTool } from "./registry.js";
|
|
2
|
-
import { appendFileSync, existsSync, statSync } from "node:fs";
|
|
3
2
|
import { resolve } from "node:path";
|
|
3
|
+
import { queueOutboundSnapshot } from "../gateway/outbound-files.js";
|
|
4
4
|
// `send_file` — the agent's ONLY way to push a file/image to the user when running inside `hara gateway`
|
|
5
5
|
// (Telegram / WeChat). The gateway sets HARA_GATEWAY_OUTBOX to a per-message temp file; this tool appends the
|
|
6
6
|
// path there, and after the headless run ends the daemon delivers each queued file to the chat via the platform
|
|
@@ -26,10 +26,16 @@ if (process.env.HARA_GATEWAY) {
|
|
|
26
26
|
if (!outbox)
|
|
27
27
|
return "send_file only works inside `hara gateway` — there is no chat to send to here.";
|
|
28
28
|
const p = resolve(ctx.cwd, String(input.path ?? ""));
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
try {
|
|
30
|
+
await queueOutboundSnapshot(p, outbox);
|
|
31
|
+
return `Queued a private snapshot for delivery to the user in this chat: ${p}`;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error?.code === "ENOENT") {
|
|
35
|
+
return `No such file: ${p || "(none)"} — create the file first, then call send_file with its absolute path.`;
|
|
36
|
+
}
|
|
37
|
+
return error instanceof Error ? error.message : `Unable to queue ${p} for delivery.`;
|
|
38
|
+
}
|
|
33
39
|
},
|
|
34
40
|
});
|
|
35
41
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nanhara/hara",
|
|
3
|
-
"version": "0.122.
|
|
3
|
+
"version": "0.122.2",
|
|
4
4
|
"description": "hara — a coding agent CLI that runs like an engineering org.",
|
|
5
5
|
"bin": {
|
|
6
|
-
"hara": "
|
|
6
|
+
"hara": "runtime-bootstrap.cjs"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
11
|
"!dist/bin",
|
|
12
|
+
"runtime-bootstrap.cjs",
|
|
12
13
|
"README.md",
|
|
13
14
|
"CHANGELOG.md",
|
|
14
15
|
"SECURITY.md",
|
|
@@ -35,16 +36,16 @@
|
|
|
35
36
|
"url": "git+https://github.com/hara-cli/hara.git"
|
|
36
37
|
},
|
|
37
38
|
"engines": {
|
|
38
|
-
"node": ">=
|
|
39
|
+
"node": ">=22.12.0"
|
|
39
40
|
},
|
|
40
41
|
"scripts": {
|
|
41
|
-
"build": "tsc",
|
|
42
|
-
"prepare": "
|
|
43
|
-
"dev": "tsx src/
|
|
44
|
-
"start": "node
|
|
45
|
-
"test": "
|
|
46
|
-
"build:binary": "
|
|
47
|
-
"build:binaries": "
|
|
42
|
+
"build": "tsc && node scripts/normalize-dist-modes.mjs",
|
|
43
|
+
"prepare": "npm run build",
|
|
44
|
+
"dev": "tsx src/cli.ts",
|
|
45
|
+
"start": "node runtime-bootstrap.cjs",
|
|
46
|
+
"test": "npm run build && node --test test/*.test.mjs",
|
|
47
|
+
"build:binary": "npm run build && bun scripts/build-binary.ts dist/bin/hara",
|
|
48
|
+
"build:binaries": "npm run build && bun scripts/build-binary.ts dist/bin/hara-darwin-arm64 bun-darwin-arm64 && bun scripts/build-binary.ts dist/bin/hara-darwin-x64 bun-darwin-x64 && bun scripts/build-binary.ts dist/bin/hara-linux-x64 bun-linux-x64 && bun scripts/build-binary.ts dist/bin/hara-linux-arm64 bun-linux-arm64"
|
|
48
49
|
},
|
|
49
50
|
"publishConfig": {
|
|
50
51
|
"access": "public"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// This file is the npm/Docker entry point. Keep it dependency-free and parseable by old Node releases so
|
|
5
|
+
// users get an upgrade instruction instead of a SyntaxError from Hara's ESM output or its dependencies.
|
|
6
|
+
var MIN_NODE_MAJOR = 22;
|
|
7
|
+
var MIN_NODE_VERSION = "22.12.0";
|
|
8
|
+
|
|
9
|
+
function supportedNodeVersion(version) {
|
|
10
|
+
var match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
|
|
11
|
+
if (!match) return false;
|
|
12
|
+
var current = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
13
|
+
var minimum = MIN_NODE_VERSION.split(".").map(Number);
|
|
14
|
+
for (var index = 0; index < minimum.length; index += 1) {
|
|
15
|
+
if (current[index] > minimum[index]) return true;
|
|
16
|
+
if (current[index] < minimum[index]) return false;
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function unsupportedNodeMessage(versions) {
|
|
22
|
+
versions = versions || process.versions;
|
|
23
|
+
if (versions.bun) return null;
|
|
24
|
+
|
|
25
|
+
var version = String(versions.node || "unknown");
|
|
26
|
+
if (supportedNodeVersion(version)) return null;
|
|
27
|
+
var major = Number.parseInt(version, 10);
|
|
28
|
+
var detail = major === MIN_NODE_MAJOR
|
|
29
|
+
? "This Node.js 22 release is below Hara's supported " + MIN_NODE_VERSION + " floor."
|
|
30
|
+
: "This Node.js release is below Hara's supported " + MIN_NODE_VERSION + " floor.";
|
|
31
|
+
return [
|
|
32
|
+
"Hara requires Node.js " + MIN_NODE_VERSION + " or newer (detected " + version + ").",
|
|
33
|
+
detail,
|
|
34
|
+
"Upgrade with: nvm install " + MIN_NODE_MAJOR + " && nvm use " + MIN_NODE_MAJOR,
|
|
35
|
+
"Or install the standalone Hara binary, which does not require Node.js:",
|
|
36
|
+
" curl -fsSL https://raw.githubusercontent.com/hara-cli/hara/main/install.sh | sh",
|
|
37
|
+
].join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function failStart(error) {
|
|
41
|
+
var message = error && error.message ? error.message : String(error);
|
|
42
|
+
process.stderr.write("hara: failed to start: " + message + "\n");
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function main() {
|
|
47
|
+
var runtimeError = unsupportedNodeMessage(process.versions);
|
|
48
|
+
if (runtimeError) {
|
|
49
|
+
process.stderr.write(runtimeError + "\n");
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
// Keeping import() inside a string prevents legacy parsers from seeing unsupported ESM syntax. This
|
|
56
|
+
// branch is reached only on the supported Node floor (or Bun when used as a script runtime).
|
|
57
|
+
var load = Function("specifier", "return import(specifier)");
|
|
58
|
+
var entry = require("url").pathToFileURL(require("path").join(__dirname, "dist", "index.js")).href;
|
|
59
|
+
load(entry).catch(failStart);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
failStart(error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
MIN_NODE_MAJOR: MIN_NODE_MAJOR,
|
|
67
|
+
MIN_NODE_VERSION: MIN_NODE_VERSION,
|
|
68
|
+
supportedNodeVersion: supportedNodeVersion,
|
|
69
|
+
unsupportedNodeMessage: unsupportedNodeMessage,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (require.main === module) main();
|