@nanhara/hara 0.0.1 → 0.33.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +431 -0
  2. package/CLA.md +51 -0
  3. package/LICENSE +201 -21
  4. package/README.md +203 -7
  5. package/dist/activity.js +30 -0
  6. package/dist/agent/loop.js +184 -0
  7. package/dist/config.js +114 -0
  8. package/dist/context/agents-md.js +64 -0
  9. package/dist/context/mentions.js +90 -0
  10. package/dist/diff.js +103 -0
  11. package/dist/fs-walk.js +103 -0
  12. package/dist/fuzzy.js +62 -0
  13. package/dist/images.js +146 -0
  14. package/dist/index.js +1362 -0
  15. package/dist/mcp/client.js +54 -0
  16. package/dist/md.js +52 -0
  17. package/dist/memory/guard.js +51 -0
  18. package/dist/memory/store.js +93 -0
  19. package/dist/org/planner.js +155 -0
  20. package/dist/org/roles.js +140 -0
  21. package/dist/org/router.js +39 -0
  22. package/dist/plugins/plugins.js +124 -0
  23. package/dist/providers/anthropic.js +83 -0
  24. package/dist/providers/openai.js +125 -0
  25. package/dist/providers/qwen-oauth.js +139 -0
  26. package/dist/providers/types.js +2 -0
  27. package/dist/recall.js +76 -0
  28. package/dist/sandbox.js +78 -0
  29. package/dist/search/embed.js +42 -0
  30. package/dist/search/hybrid.js +38 -0
  31. package/dist/search/semindex.js +141 -0
  32. package/dist/session/store.js +95 -0
  33. package/dist/skills/skills.js +141 -0
  34. package/dist/statusbar.js +69 -0
  35. package/dist/tools/agent.js +26 -0
  36. package/dist/tools/apply-core.js +63 -0
  37. package/dist/tools/builtin.js +106 -0
  38. package/dist/tools/codebase.js +102 -0
  39. package/dist/tools/computer.js +236 -0
  40. package/dist/tools/edit.js +62 -0
  41. package/dist/tools/memory.js +147 -0
  42. package/dist/tools/patch.js +123 -0
  43. package/dist/tools/registry.js +18 -0
  44. package/dist/tools/search.js +176 -0
  45. package/dist/tools/skill.js +30 -0
  46. package/dist/tools/web.js +73 -0
  47. package/dist/tui/App.js +165 -0
  48. package/dist/tui/InputBox.js +208 -0
  49. package/dist/tui/run.js +10 -0
  50. package/dist/tui/theme.js +11 -0
  51. package/dist/ui.js +17 -0
  52. package/dist/undo.js +40 -0
  53. package/dist/vision.js +81 -0
  54. package/package.json +33 -7
  55. package/bin/hara.mjs +0 -25
@@ -0,0 +1,208 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ // The framed input box (ink): a top border carrying the session name in the right corner, the
3
+ // prompt line in the middle, and a bottom border carrying the approval modes + token usage +
4
+ // concurrency. Composed from <Text> rows (no ink border fork needed) so the title sits exactly
5
+ // where we want it. Pure-ish: pass `width` to make rendering deterministic in tests.
6
+ import { Box, Text, useInput, useStdout } from "ink";
7
+ import { useMemo, useState } from "react";
8
+ import { fileCandidates } from "../context/mentions.js";
9
+ import { imagePathFromPaste } from "../images.js";
10
+ export const MODES = ["suggest", "auto-edit", "full-auto", "plan"];
11
+ export const nextMode = (m) => MODES[(MODES.indexOf(m) + 1) % MODES.length];
12
+ const tok = (n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : `${n}`);
13
+ function TopBorder({ name, width }) {
14
+ const labelLen = name.length + 2; // "⏺ " + name
15
+ const left = Math.max(2, width - labelLen - 3);
16
+ return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: ["─".repeat(left), " "] }), _jsx(Text, { color: "cyan", children: "\u23FA" }), _jsxs(Text, { bold: true, children: [" ", name] }), _jsx(Text, { dimColor: true, children: " \u2500" })] }));
17
+ }
18
+ // Bottom border carries token usage + concurrency at the right corner (modes moved to ModeBar below).
19
+ function BottomBorder({ s, width }) {
20
+ const usage = `↑${tok(s.input)} ↓${tok(s.output)}${s.ctxPct > 0 ? ` · ctx ${s.ctxPct}%` : ""}`;
21
+ const label = s.agents > 0 ? `${usage} · ⛁${s.agents}` : `${usage} · ⛁ idle`;
22
+ const left = Math.max(2, width - label.length - 3);
23
+ return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: `${"─".repeat(left)} ${label} ─` }) }));
24
+ }
25
+ const MODE_DESC = {
26
+ suggest: "confirms edits & commands",
27
+ "auto-edit": "auto-applies edits · asks before commands",
28
+ "full-auto": "runs everything — no prompts ⚠",
29
+ plan: "investigate read-only, then propose a plan to approve",
30
+ };
31
+ // Prominent approval-mode selector below the box: all three listed, the active one highlighted (red
32
+ // for the dangerous full-auto) with a one-line description and the shift+tab hint.
33
+ function ModeBar({ approval }) {
34
+ const warn = approval === "full-auto";
35
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: MODES.map((m, i) => (_jsxs(Text, { children: [i > 0 ? " " : " ", m === approval ? _jsx(Text, { color: warn ? "red" : m === "plan" ? "cyan" : "green", bold: true, children: `◆ ${m}` }) : _jsx(Text, { dimColor: true, children: m })] }, m))) }), _jsx(Text, { dimColor: true, children: ` ${MODE_DESC[approval]} · shift+tab ⇄` })] }));
36
+ }
37
+ /** The active `@mention` token immediately left of the cursor (for the file popup), or null. */
38
+ function activeMention(value, cursor) {
39
+ const m = /(?:^|\s)@([^\s@]*)$/.exec(value.slice(0, cursor));
40
+ return m ? { query: m[1], start: cursor - m[1].length } : null;
41
+ }
42
+ // Dropdown of fuzzy @path matches, shown above the input as you type `@…` (codex / Claude-Code style).
43
+ function MentionPopup({ items, selected, query }) {
44
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: ` @${query} · ${items.length} match${items.length === 1 ? "" : "es"} — ↑↓ select · Tab/Enter insert · Esc dismiss` }), items.map((it, i) => (_jsxs(Text, { children: [i === selected ? _jsx(Text, { color: "cyan", children: " ▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: it.endsWith("/") ? "blue" : undefined, dimColor: i !== selected, bold: i === selected, children: it })] }, it)))] }));
45
+ }
46
+ const TOKEN_RE = /\[Image #\d+\]/g;
47
+ /** Render the prompt line: plain text + the cursor, with any `[Image #N]` attachment tokens highlighted
48
+ * (codex / Claude-Code style — the image lives inline in the text, visibly distinct from what you typed). */
49
+ function InputLine({ value, cursor }) {
50
+ const parts = [];
51
+ let last = 0;
52
+ let m;
53
+ TOKEN_RE.lastIndex = 0;
54
+ while ((m = TOKEN_RE.exec(value))) {
55
+ if (m.index > last)
56
+ parts.push({ text: value.slice(last, m.index), token: false });
57
+ parts.push({ text: m[0], token: true });
58
+ last = m.index + m[0].length;
59
+ }
60
+ if (last < value.length)
61
+ parts.push({ text: value.slice(last), token: false });
62
+ const seg = (token, text, k) => token ? (_jsx(Text, { backgroundColor: "magenta", color: "white", children: text }, k)) : (_jsx(Text, { children: text }, k));
63
+ const nodes = [];
64
+ let pos = 0;
65
+ let ki = 0;
66
+ for (const p of parts) {
67
+ const start = pos;
68
+ const end = pos + p.text.length;
69
+ if (cursor >= start && cursor < end) {
70
+ const rel = cursor - start;
71
+ if (rel > 0)
72
+ nodes.push(seg(p.token, p.text.slice(0, rel), `s${ki++}`));
73
+ nodes.push(_jsx(Text, { inverse: true, children: p.text[rel] }, `c${ki++}`));
74
+ if (rel + 1 < p.text.length)
75
+ nodes.push(seg(p.token, p.text.slice(rel + 1), `e${ki++}`));
76
+ }
77
+ else {
78
+ nodes.push(seg(p.token, p.text, `p${ki++}`));
79
+ }
80
+ pos = end;
81
+ }
82
+ if (cursor >= value.length)
83
+ nodes.push(_jsx(Text, { inverse: true, children: " " }, "end"));
84
+ return _jsx(Text, { children: nodes });
85
+ }
86
+ /** Top border (session) + prompt line + bottom border (usage) + ModeBar, with an @path popup. */
87
+ export function InputBox({ status, cwd, width, onSubmit, onClipboardImage, isActive = true, placeholder = "Type a task · /help · @file · Ctrl+V paste image · shift+tab mode · Esc interrupts", }) {
88
+ const { stdout } = useStdout();
89
+ const w = width ?? stdout?.columns ?? 80;
90
+ const [value, setValue] = useState("");
91
+ const [cursor, setCursor] = useState(0);
92
+ const [sel, setSel] = useState(0);
93
+ const [dismissed, setDismissed] = useState(false);
94
+ const [images, setImages] = useState([]);
95
+ const set = (v, c) => {
96
+ setValue(v);
97
+ setCursor(c);
98
+ setSel(0);
99
+ setDismissed(false);
100
+ };
101
+ // Attach an image: drop a highlighted `[Image #N]` token inline at the cursor and track the file
102
+ // (codex / Claude-Code style). Backspace over the token removes both.
103
+ const addImage = (img) => {
104
+ const tok = `[Image #${images.length + 1}]`;
105
+ const before = value.slice(0, cursor);
106
+ const ins = (before && !/\s$/.test(before) ? " " : "") + tok + " ";
107
+ setValue(before + ins + value.slice(cursor));
108
+ setCursor((before + ins).length);
109
+ setImages((xs) => [...xs, img]);
110
+ setSel(0);
111
+ setDismissed(false);
112
+ };
113
+ const submit = (text) => {
114
+ if (!text.trim() && images.length === 0)
115
+ return; // nothing to send
116
+ onSubmit?.(text, images.length ? images : undefined);
117
+ set("", 0);
118
+ setImages([]);
119
+ };
120
+ const mention = activeMention(value, cursor);
121
+ const candidates = useMemo(() => (isActive && mention && !dismissed ? fileCandidates(cwd, mention.query, 8) : []), [cwd, isActive, dismissed, mention?.query, mention?.start]);
122
+ const popupOpen = candidates.length > 0;
123
+ const selIdx = popupOpen ? Math.min(sel, candidates.length - 1) : 0;
124
+ const complete = (cand) => {
125
+ if (!mention)
126
+ return;
127
+ const before = value.slice(0, mention.start); // includes the leading '@'
128
+ const after = value.slice(cursor);
129
+ const insert = cand.endsWith("/") ? cand : cand + " "; // dirs keep drilling; files end the mention
130
+ setValue(before + insert + after);
131
+ setCursor((before + insert).length);
132
+ setSel(0);
133
+ setDismissed(false);
134
+ };
135
+ useInput((input, key) => {
136
+ if (popupOpen && (key.upArrow || key.downArrow)) {
137
+ const n = candidates.length;
138
+ setSel((s) => (key.downArrow ? (s + 1) % n : (s - 1 + n) % n));
139
+ return;
140
+ }
141
+ if (popupOpen && (key.tab || key.return)) {
142
+ complete(candidates[selIdx]);
143
+ return;
144
+ }
145
+ if (key.escape) {
146
+ if (popupOpen)
147
+ setDismissed(true);
148
+ return;
149
+ }
150
+ if (key.return) {
151
+ submit(value);
152
+ return;
153
+ }
154
+ if (key.leftArrow)
155
+ return setCursor((c) => Math.max(0, c - 1));
156
+ if (key.rightArrow)
157
+ return setCursor((c) => Math.min(value.length, c + 1));
158
+ if (key.ctrl && input === "a")
159
+ return setCursor(0);
160
+ if (key.ctrl && input === "e")
161
+ return setCursor(value.length);
162
+ if (key.ctrl && input === "u")
163
+ return set(value.slice(cursor), 0);
164
+ if (key.ctrl && input === "v") {
165
+ // paste a screenshot / image from the OS clipboard
166
+ const img = onClipboardImage?.();
167
+ if (img)
168
+ addImage(img);
169
+ return;
170
+ }
171
+ if (key.backspace || key.delete) {
172
+ if (cursor > 0) {
173
+ const head = value.slice(0, cursor);
174
+ const tm = /\[Image #(\d+)\]\s?$/.exec(head); // backspacing over an attachment token removes it whole
175
+ if (tm) {
176
+ const n = Number(tm[1]);
177
+ const kept = head.slice(0, tm.index) + value.slice(cursor);
178
+ const renumbered = kept.replace(/\[Image #(\d+)\]/g, (m2, d) => (Number(d) > n ? `[Image #${Number(d) - 1}]` : m2));
179
+ setImages((xs) => xs.filter((_, i) => i !== n - 1));
180
+ setValue(renumbered);
181
+ setCursor(tm.index);
182
+ setSel(0);
183
+ setDismissed(false);
184
+ return;
185
+ }
186
+ set(value.slice(0, cursor - 1) + value.slice(cursor), cursor - 1);
187
+ }
188
+ return;
189
+ }
190
+ if (input && !key.ctrl && !key.meta) {
191
+ const nl = input.search(/[\r\n]/); // a chunk carrying a newline (paste / fed input) submits
192
+ if (nl >= 0) {
193
+ submit(value.slice(0, cursor) + input.slice(0, nl) + value.slice(cursor));
194
+ return;
195
+ }
196
+ // a dragged-in / pasted image file path attaches instead of inserting literal text
197
+ if (input.length > 3) {
198
+ const img = imagePathFromPaste(input, cwd);
199
+ if (img) {
200
+ addImage(img);
201
+ return;
202
+ }
203
+ }
204
+ set(value.slice(0, cursor) + input + value.slice(cursor), cursor + input.length);
205
+ }
206
+ }, { isActive });
207
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(TopBorder, { name: status.sessionName || "session", width: w }), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "› " }), value.length === 0 ? (_jsxs(Text, { children: [_jsx(Text, { inverse: true, children: " " }), _jsx(Text, { dimColor: true, children: placeholder })] })) : (_jsx(InputLine, { value: value, cursor: cursor }))] }), _jsx(BottomBorder, { s: status, width: w }), popupOpen ? _jsx(MentionPopup, { items: candidates, selected: selIdx, query: mention.query }) : null, _jsx(ModeBar, { approval: status.approval })] }));
208
+ }
@@ -0,0 +1,10 @@
1
+ // Mounts the hara TUI (ink) and resolves when the user exits. Thin shell — all agent wiring
2
+ // (provider, session history, slash commands, turn execution) is passed in via AppProps.onSubmit
3
+ // from index.ts, which owns that state.
4
+ import { render } from "ink";
5
+ import { createElement } from "react";
6
+ import { App } from "./App.js";
7
+ export async function runTui(props) {
8
+ const instance = render(createElement(App, props));
9
+ await instance.waitUntilExit();
10
+ }
@@ -0,0 +1,11 @@
1
+ let current = "dark";
2
+ export function setTheme(name) {
3
+ current = name === "light" ? "light" : "dark";
4
+ }
5
+ export function themeName() {
6
+ return current;
7
+ }
8
+ /** Brand accent (warm vermilion · 朱印). */
9
+ export function accent() {
10
+ return current === "light" ? "#C0392B" : "#FF6B5C";
11
+ }
package/dist/ui.js ADDED
@@ -0,0 +1,17 @@
1
+ import { stdout } from "node:process";
2
+ const useColor = stdout.isTTY && process.env.NO_COLOR === undefined;
3
+ const wrap = (code) => (s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : s);
4
+ export const c = {
5
+ dim: wrap("2"),
6
+ bold: wrap("1"),
7
+ cyan: wrap("36"),
8
+ green: wrap("32"),
9
+ yellow: wrap("33"),
10
+ red: wrap("31"),
11
+ };
12
+ export function out(s) {
13
+ stdout.write(s);
14
+ }
15
+ export function statusLine(model, inTok, outTok) {
16
+ return c.dim(` ${model} · ↑${inTok} ↓${outTok} tok`);
17
+ }
package/dist/undo.js ADDED
@@ -0,0 +1,40 @@
1
+ // In-session undo stack for file changes. Each edit tool records the prior state of the files it
2
+ // touched; `/undo` pops the last group and restores it. Process-scoped (one REPL session).
3
+ import { writeFile, unlink, mkdir } from "node:fs/promises";
4
+ import { dirname } from "node:path";
5
+ const stack = [];
6
+ const MAX = 50;
7
+ /** Record a group of file changes (one tool call = one undo step). */
8
+ export function recordEdit(group) {
9
+ if (!group.length)
10
+ return;
11
+ stack.push(group);
12
+ if (stack.length > MAX)
13
+ stack.shift();
14
+ }
15
+ export function undoDepth() {
16
+ return stack.length;
17
+ }
18
+ /** Restore the most recent edit group. Returns the files reverted, or an error. */
19
+ export async function undoLast() {
20
+ const group = stack.pop();
21
+ if (!group)
22
+ return { error: "nothing to undo" };
23
+ const files = [];
24
+ for (const s of group) {
25
+ try {
26
+ if (s.before === null) {
27
+ await unlink(s.absPath).catch(() => { }); // was newly created → remove
28
+ }
29
+ else {
30
+ await mkdir(dirname(s.absPath), { recursive: true });
31
+ await writeFile(s.absPath, s.before, "utf8");
32
+ }
33
+ files.push(s.path);
34
+ }
35
+ catch {
36
+ /* skip a file we can't restore */
37
+ }
38
+ }
39
+ return { files };
40
+ }
package/dist/vision.js ADDED
@@ -0,0 +1,81 @@
1
+ // Built-in capability map for the major model families. First matching rule wins, so each family's
2
+ // vision pattern is listed BEFORE its text catch-all. Anything that matches nothing → "unknown"
3
+ // (we ask the user once and remember). Easy to extend — add a rule near the right family.
4
+ const MODEL_VISION_MAP = [
5
+ // OpenAI
6
+ { rx: /gpt-4o|gpt-4\.1|gpt-4-turbo|chatgpt-4o|gpt-5|(?:^|[-_/])o[134](?:[-_/]|$)/i, cap: "vision" },
7
+ { rx: /gpt-4(\b|-0|-1)|gpt-3\.5|davinci|babbage|text-(?:embedding|davinci)/i, cap: "text" },
8
+ // Qwen — Alibaba Coding Plan: qwen3.x-plus see images (verified qwen3.7-plus); max/coder are text-only.
9
+ { rx: /qwen.*vl|qwen.*omni|qvq/i, cap: "vision" },
10
+ { rx: /qwen-?3[.\d]*-plus/i, cap: "vision" }, // qwen3.5-plus / qwen3.6-plus / qwen3.7-plus
11
+ { rx: /qwen.*(?:coder|plus|max|turbo|long|math)|qwq|qwen[\d.]*-?\d+b\b|qwen-?\d/i, cap: "text" },
12
+ // GLM / Zhipu — 4v/4.5v see images; glm-5, glm-4.7, glm-4-flash are text-only.
13
+ { rx: /glm-?\d(?:\.\d+)?v|cogvlm|glm.*vision/i, cap: "vision" },
14
+ { rx: /glm-?\d(?:\.\d+)?(?:-(?:air|flash|plus|long|x|0520))?\b|glm-z|chatglm/i, cap: "text" },
15
+ // DeepSeek (VL first, then the text families)
16
+ { rx: /deepseek.*vl/i, cap: "vision" },
17
+ { rx: /deepseek/i, cap: "text" },
18
+ // Google
19
+ { rx: /gemini|gemma-3/i, cap: "vision" },
20
+ { rx: /gemma/i, cap: "text" },
21
+ // Mistral (Pixtral/small-3 see; the rest text)
22
+ { rx: /pixtral|mistral-small-3|mistral.*vision/i, cap: "vision" },
23
+ { rx: /mistral|mixtral|codestral|ministral/i, cap: "text" },
24
+ // Meta Llama (3.2-11B/90B + 4 see; the rest text)
25
+ { rx: /llama-?3\.2-(?:11|90)b|llama.*vision|llama-?4/i, cap: "vision" },
26
+ { rx: /llama|codellama/i, cap: "text" },
27
+ // Moonshot / Kimi — kimi-k2.5 sees images (Coding Plan); older Kimi text.
28
+ { rx: /kimi-?k?2\.5|kimi.*vl|moonshot.*(?:vl|vision)/i, cap: "vision" },
29
+ { rx: /kimi|moonshot/i, cap: "text" },
30
+ // xAI Grok
31
+ { rx: /grok.*vision|grok-[\d.]*v\b|grok-4/i, cap: "vision" },
32
+ { rx: /grok/i, cap: "text" },
33
+ // MiniMax — VL models see images; the M-series chat (e.g. MiniMax-M2.5) is text-only.
34
+ { rx: /minimax.*(?:vl|vision)|abab.*vl/i, cap: "vision" },
35
+ { rx: /minimax|abab/i, cap: "text" },
36
+ // Other well-known vision families
37
+ { rx: /(?:^|[-_/])vl(?:[-_/]|$)|internvl|llava|minicpm-?v|yi-vl|step-1[vo]|doubao.*(?:vl|vision)|ernie.*vl/i, cap: "vision" },
38
+ ];
39
+ /**
40
+ * Resolve a model's vision capability: explicit per-model override → Anthropic (all modern Claude see
41
+ * images) → built-in family map → "unknown" (caller asks the user). Pure + table-driven so it's testable.
42
+ */
43
+ export function classifyVision(provider, model, overrides = {}) {
44
+ const o = overrides[model];
45
+ if (o === "yes")
46
+ return "vision";
47
+ if (o === "no")
48
+ return "text";
49
+ if (provider === "anthropic")
50
+ return "vision";
51
+ const m = model || "";
52
+ for (const r of MODEL_VISION_MAP)
53
+ if (r.rx.test(m))
54
+ return r.cap;
55
+ return "unknown";
56
+ }
57
+ export const DESCRIBE_SYSTEM = [
58
+ "You are the eyes of a coding assistant that cannot see images. Transcribe and describe the attached",
59
+ "image(s) completely and precisely so the assistant can act on them without seeing them.",
60
+ "Rules:",
61
+ "1. Transcribe ALL visible text and code VERBATIM, preserving line breaks and indentation — put code,",
62
+ " terminal output, and logs in fenced code blocks.",
63
+ "2. For UI / screenshots: describe the layout, components, labels, states, and notable colors.",
64
+ "3. For diagrams / charts: describe the structure — nodes, edges, axes, and data.",
65
+ "4. Quote any error or warning messages exactly.",
66
+ "5. Be thorough and factual; do not speculate beyond what is visible.",
67
+ ].join("\n");
68
+ const PROMPT = "Describe the attached image(s) per your instructions.";
69
+ /** Send images to the vision provider and return its textual description. Throws on a provider error. */
70
+ export async function describeImages(provider, images, opts = {}) {
71
+ const r = await provider.turn({
72
+ system: DESCRIBE_SYSTEM,
73
+ history: [{ role: "user", content: PROMPT, images }],
74
+ tools: [],
75
+ onText: () => { },
76
+ signal: opts.signal,
77
+ });
78
+ if (r.stop === "error")
79
+ throw new Error(r.errorMsg || "vision provider error");
80
+ return r.text.trim();
81
+ }
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "@nanhara/hara",
3
- "version": "0.0.1",
4
- "description": "hara — a coding agent CLI that runs like an engineering org. Placeholder; under active development.",
3
+ "version": "0.33.0",
4
+ "description": "hara — a coding agent CLI that runs like an engineering org.",
5
5
  "bin": {
6
- "hara": "bin/hara.mjs"
6
+ "hara": "dist/index.js"
7
7
  },
8
8
  "type": "module",
9
9
  "files": [
10
- "bin",
10
+ "dist",
11
11
  "README.md",
12
- "LICENSE"
12
+ "CHANGELOG.md",
13
+ "LICENSE",
14
+ "CLA.md"
13
15
  ],
14
16
  "keywords": [
15
17
  "ai",
@@ -18,9 +20,11 @@
18
20
  "coding-agent",
19
21
  "llm",
20
22
  "agent-orchestration",
21
- "multi-agent"
23
+ "multi-agent",
24
+ "anthropic",
25
+ "claude"
22
26
  ],
23
- "license": "MIT",
27
+ "license": "Apache-2.0",
24
28
  "author": "Nanhara",
25
29
  "homepage": "https://hara.run",
26
30
  "repository": {
@@ -30,7 +34,29 @@
30
34
  "engines": {
31
35
  "node": ">=20"
32
36
  },
37
+ "scripts": {
38
+ "build": "tsc",
39
+ "prepare": "tsc",
40
+ "dev": "tsx src/index.ts",
41
+ "start": "node dist/index.js",
42
+ "test": "tsc && node --test test/*.test.mjs"
43
+ },
33
44
  "publishConfig": {
34
45
  "access": "public"
46
+ },
47
+ "dependencies": {
48
+ "@anthropic-ai/sdk": "^0.104.2",
49
+ "@modelcontextprotocol/sdk": "^1.29.0",
50
+ "commander": "^15.0.0",
51
+ "ink": "^6.8.0",
52
+ "openai": "^6.44.0",
53
+ "react": "^19.2.7"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^25.9.3",
57
+ "@types/react": "^19.2.17",
58
+ "ink-testing-library": "^4.0.0",
59
+ "tsx": "^4.22.4",
60
+ "typescript": "^6.0.3"
35
61
  }
36
62
  }
package/bin/hara.mjs DELETED
@@ -1,25 +0,0 @@
1
- #!/usr/bin/env node
2
- // hara — placeholder CLI. Real agent coming soon.
3
- import { readFileSync } from "node:fs";
4
- import { fileURLToPath } from "node:url";
5
- import { dirname, join } from "node:path";
6
-
7
- const __dir = dirname(fileURLToPath(import.meta.url));
8
- let version = "0.0.1";
9
- try {
10
- version = JSON.parse(readFileSync(join(__dir, "..", "package.json"), "utf8")).version;
11
- } catch {}
12
-
13
- const args = process.argv.slice(2);
14
- if (args[0] === "-v" || args[0] === "--version") {
15
- console.log(version);
16
- process.exit(0);
17
- }
18
-
19
- console.log(`hara v${version}
20
- A coding agent CLI that runs like an engineering org.
21
-
22
- This is an early placeholder — the real thing is under active development.
23
- Track it: https://github.com/hara-cli/hara
24
- Site: https://hara.run
25
- `);