heartcode 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fayaz Bin Salam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,130 @@
1
- # Temporary Holding Version
1
+ # HeartCode
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A coding agent for your terminal that works with any OpenAI-compatible model.
4
+
5
+ **Zero dependencies.** Nothing in `node_modules`, no install scripts, no packages pulled in
6
+ behind your back: only Node.js (20 or newer) and HeartCode's own code. What you install is
7
+ what you can read.
8
+
9
+ <!-- image: HeartCode in the terminal, answering a message -->
10
+ ![HeartCode in the terminal](docs/images/heartcode.png)
11
+
12
+ ## Why I made it
13
+
14
+ Not to compete with other coding CLIs. I made it for my own use, because none of the ones I
15
+ tried did what I needed. In short, HeartCode:
16
+
17
+ - **Works with any OpenAI-compatible provider**, several at once: OpenRouter, Groq, NVIDIA,
18
+ Gemini, Ollama, LM Studio, your own proxy…
19
+ - **Keeps a list of models and falls back on its own.** When one hits a rate limit or goes down,
20
+ the next one in your list answers. Free models run out often; this way the work goes on.
21
+ - **Works with models that can't call tools.** It guides them step by step instead.
22
+ - **Knows each model from [models.dev](https://models.dev)**: its context window, whether it calls
23
+ tools, thinks or reads images. Nothing is sent to the model to find out.
24
+ - **Is set up in a web page** on your own machine (`/settings`), not by editing config files.
25
+ - **Never sends `max_tokens`**, so no provider cuts the reply short by a limit you didn't choose.
26
+ - **Can be used by other tools and AI agents**: `heart -p` runs one task headless, like
27
+ `claude -p` (see [SKILL.md](SKILL.md)).
28
+ - **Logs each request and its reply** in a file of its own, when you switch logging on, so you
29
+ can see what went wrong.
30
+ - **Takes skills, MCP servers and hooks** in Claude Code's formats; `/import` brings yours over.
31
+ - **Has tabs** (`Ctrl+T`), each with its own conversation, and git worktrees for work kept apart.
32
+
33
+ ## How I set it up and use it
34
+
35
+ ### 1. Install
36
+
37
+ From npm:
38
+
39
+ ```sh
40
+ npm install -g heartcode
41
+ ```
42
+
43
+ Or from source:
44
+
45
+ ```sh
46
+ git clone https://github.com/p32929/HeartCode.git
47
+ cd HeartCode
48
+ npm start
49
+ ```
50
+
51
+ `npm start` links the `heart` command to that folder, so a `git pull` updates it.
52
+
53
+ ### 2. Run `heart`
54
+
55
+ In the project you want to work on:
56
+
57
+ ```sh
58
+ cd ~/code/my-app
59
+ heart
60
+ ```
61
+
62
+ <!-- image: HeartCode's first screen -->
63
+ ![HeartCode's first screen](docs/images/start.png)
64
+
65
+ ### 3. Open the settings page
66
+
67
+ Type `/settings`. The settings page opens in your browser. It runs on your machine only
68
+ (127.0.0.1, with a secret key in its link).
69
+
70
+ <!-- image: the settings page -->
71
+ ![The settings page](docs/images/settings.png)
72
+
73
+ ### 4. Providers & models
74
+
75
+ 1. **Add a provider**: its name, base URL and API key. Pick one of the presets to fill in the URL.
76
+ 2. **Choose the models to use**: click a model to add it to your list. I use the free models.
77
+
78
+ <!-- image: Providers & models, with a provider added and models chosen -->
79
+ ![Providers and models](docs/images/providers.png)
80
+
81
+ ### 5. Your models
82
+
83
+ 1. **Sort them by context and abilities.** Click the *Abilities* column, then *Context*: the
84
+ models that can do the most, with the biggest context window, come first. Click
85
+ **Use this order** to save it.
86
+ 2. **Rearrange a bit more.** Drag a row to move it. #1 is the one HeartCode uses; the others are
87
+ its fallbacks, tried in order.
88
+
89
+ If a model says *not found* in models.dev, **Retry** looks it up again, or **Set up** tells
90
+ HeartCode what it can do.
91
+
92
+ <!-- image: Your models, sorted -->
93
+ ![Your models](docs/images/your-models.png)
94
+
95
+ ### 6. Turn on logging (optional)
96
+
97
+ On the **Logging** page: a log of each run, and each request with its reply, in files of their
98
+ own. Logs older than 30 days are deleted (you can change that).
99
+
100
+ <!-- image: the Logging page -->
101
+ ![Logging](docs/images/logging.png)
102
+
103
+ ### 7. Back to HeartCode
104
+
105
+ Go back to the terminal. While the settings page is on, HeartCode shows a dialog and waits.
106
+ Your changes are already in.
107
+
108
+ ### 8. Press Enter to stop the settings page
109
+
110
+ **Turn off** is selected: press Enter. The page stops and HeartCode is yours again.
111
+
112
+ <!-- image: the "settings page is on" dialog -->
113
+ ![The settings dialog](docs/images/settings-dialog.png)
114
+
115
+ ### 9. Send "Hi" as a test
116
+
117
+ Type `Hi` and press Enter. Wait for the reply: your model works.
118
+
119
+ <!-- image: the first reply -->
120
+ ![The first reply](docs/images/hi.png)
121
+
122
+ ### 10. Start using it
123
+
124
+ Use HeartCode like any other coding agent: ask it to read, change, run and fix things in your
125
+ project. `/` shows every command, `Shift+Tab` changes what it may do without asking, `Esc` stops
126
+ it, and `heart --help` lists everything else.
127
+
128
+ ## License
129
+
130
+ [MIT](LICENSE)
package/SKILL.md ADDED
@@ -0,0 +1,170 @@
1
+ ---
2
+ name: heartcode
3
+ description: Run a coding task with HeartCode, a coding agent for any OpenAI-compatible model (OpenRouter, Groq, Ollama, LM Studio, your own server…), from a script or another AI agent. Use it to hand a task to a different or cheaper model, get a second opinion on code, or run work in an isolated git worktree. Headless: `heart -p`, JSON out, exit code 0 or 1.
4
+ ---
5
+
6
+ # HeartCode, for AI agents
7
+
8
+ HeartCode is a coding agent in the terminal. Interactively it takes the whole screen, which an
9
+ agent can't drive. Use it **headless**: `heart -p` runs one task without the screen, prints the
10
+ answer (or JSON) and exits. Its flags and output match Claude Code's `claude -p`.
11
+
12
+ ## Check it's there
13
+
14
+ ```sh
15
+ heart --version # installed? (else: node /path/to/heart_cli/bin/heart.js --version)
16
+ heart providers # the providers set up, and their URLs (keys are masked)
17
+ heart models --json # the models to pass to -m: "default" first (the user's list, with fallbacks), then each model
18
+ heart models --all --json # every provider's models too (slower: asks each provider)
19
+ ```
20
+
21
+ Read `--json`, not the text: the text layout is for people and may change.
22
+
23
+ ```json
24
+ { "models": [
25
+ { "place": null, "provider": null, "model": "default", "role": "default", "rotates": true, "about": "your models in order: #1, then the next when one fails (-m default, or no -m)" },
26
+ { "place": 1, "provider": "groq", "model": "openai/gpt-oss-120b", "role": "in use", "rotates": false },
27
+ { "place": 2, "provider": "openrouter", "model": "qwen/qwen3-coder", "role": "fallback", "rotates": false } ],
28
+ "providers": [
29
+ { "name": "groq", "baseUrl": "https://api.groq.com/openai/v1", "list": true, "models": ["…"] },
30
+ { "name": "local", "baseUrl": "http://127.0.0.1:9/v1", "list": true, "models": [], "error": "cannot reach …" } ] }
31
+ ```
32
+
33
+ Each entry in `models` is something to pass to `-m`, in order:
34
+
35
+ - **`default`** (always first; `place` and `provider` are `null`): the user's list. `rotates: true`
36
+ when they have more than one model: #1 is used, and the next one in order when it fails.
37
+ - **the user's models** (`place` 1, 2, …): `role` is `in use` for #1 and `fallback` for the rest;
38
+ `rotates: false`, since a model named on its own is used alone.
39
+
40
+ No model set up yet: `models` is empty (and `heart -p` fails with "no model set up").
41
+ `heart models [--all] <filter>` keeps only the entries whose `model` contains `<filter>`.
42
+
43
+ `providers` (only with `--all`): every provider's models. `list: false`: the user switched that
44
+ provider's list off, and `models` are the ones they added by name. A provider that can't be
45
+ reached has an `error` and is skipped; the command still exits `0`.
46
+
47
+ **Which model to pass:**
48
+
49
+ | `-m` | What runs |
50
+ |---|---|
51
+ | `default`, or no `-m` | the user's list: #1, and the next one in order when it fails (rate limits, outages…). Use this unless you need one particular model. |
52
+ | a model from the list: `-m "<provider> <model>"` (`-m "groq openai/gpt-oss-120b"`), or `-m <model>` alone | that model alone: never another one, also when it fails (a rate limit or an outage is tried again, up to `--max-retries`) |
53
+
54
+ **Failures:** a rate limit, a server error, a timeout or a lost connection is waited out and tried
55
+ again (or handed to the next model in the list), up to `--max-retries` (10). An error waiting can't
56
+ fix (a model name the server doesn't know, a wrong key, a request it refuses) is never sent again:
57
+ the run stops with that error and exit code `1`. With the default list it moves on to the next
58
+ model only if the user set its fallback rule to "at any error".
59
+
60
+ `--provider <name>` and `-u <url>` (a server for this run) also mean one model, alone.
61
+
62
+ `heart` is installed by `npm start` in the HeartCode folder. `heartcode` is the same command.
63
+ Needs Node.js 20 or newer. No providers? See "Setting up a model" below.
64
+
65
+ ## Run a task
66
+
67
+ ```sh
68
+ heart -p --output-format json -C /path/to/project "fix the failing test in math.test.js"
69
+ ```
70
+
71
+ - `-C <folder>`: the project to work in (default: the current folder). A folder, not a file.
72
+ - The prompt is the last argument, or stdin: `echo "explain src/agent.js" | heart -p`.
73
+ - Exit code: `0` it finished, `1` it failed (no model, the model kept failing, the server refused
74
+ the request (wrong model name, bad key), `--max-turns` hit). The error is in `result` (JSON) or on stderr.
75
+
76
+ `--output-format json` prints one object on stdout:
77
+
78
+ ```json
79
+ { "type": "result", "subtype": "success", "is_error": false, "result": "Fixed it: …",
80
+ "num_turns": 6, "duration_ms": 41200, "total_cost_usd": 0.0031,
81
+ "usage": { "input_tokens": 18000, "output_tokens": 900 }, "session_id": "20261027-…" }
82
+ ```
83
+
84
+ Read `is_error` and `result`. `subtype` is `success`, `error_during_execution` or
85
+ `error_max_turns`. Without `--output-format`, stdout is just the answer text, and an error
86
+ goes to stderr. Logs never go to stdout.
87
+
88
+ ## Continue the same conversation
89
+
90
+ Pass the `session_id` from the last result:
91
+
92
+ ```sh
93
+ heart -p --output-format json --resume 20261027-… "now add a test for the empty case"
94
+ heart -p --continue "…" # or: the last session in this folder
95
+ ```
96
+
97
+ ## Keep it safe
98
+
99
+ Headless never asks anyone. What the permission mode would ask about is declined, and the
100
+ model is told why.
101
+
102
+ | You want | Flags |
103
+ |---|---|
104
+ | Answers only, no changes (review, explain, find) | `--allowedTools Read,Grep,Glob` |
105
+ | Edits, but no shell commands | `--permission-mode commands` |
106
+ | Everything (edits and commands) | `--permission-mode all` (the default unless the user set another) |
107
+ | Changes kept apart from the user's files | `--worktree=<name>` (in a git repo): it works in `.heartcode/worktrees/<name>` on the branch `heartcode/<name>`; kept if anything changed (merge it from there), removed if nothing did |
108
+ | A cap on how long it works | `--max-turns 30` (model steps), `--max-retries 3` (a failing provider) |
109
+ | Nothing written to the user's `~/.heart` (sessions, logs, settings) | `--temp` |
110
+
111
+ Tool names: HeartCode's own (`read_file`, `edit_file`, `write_file`, `run_command`, `search`,
112
+ `list_files`, `web_fetch`, `web_search`…) or Claude Code's (`Read`, `Edit`, `Write`, `Bash`,
113
+ `Grep`, `Glob`, `WebFetch`, `WebSearch`). `--disallowedTools Bash` takes one away.
114
+
115
+ ## Choose the model
116
+
117
+ ```sh
118
+ heart -p -m default "…" # the user's list, with its fallbacks (the same as no -m)
119
+ heart -p -m "groq openai/gpt-oss-120b" "…" # that provider and model alone, for this run
120
+ heart -p --provider ollama -m qwen3-coder "…" # the same, written apart
121
+ heart -p --effort high "…" # how hard a thinking model thinks
122
+ ```
123
+
124
+ What a model can do (tool calls, context size, thinking, images) comes from models.dev, at once;
125
+ nothing is sent to the model to find out. A model models.dev doesn't know works with plain
126
+ assumptions, and the user can set it up on the settings page. Models that can't call tools still
127
+ work, in a slower guided mode.
128
+
129
+ ## Setting up a model
130
+
131
+ Ask the user which provider and model to use, and for the key. Never invent one. Then:
132
+
133
+ ```sh
134
+ # for one run only (nothing saved); the key from the environment, not the command line
135
+ HEART_BASE_URL=https://api.groq.com/openai/v1 HEART_API_KEY=… HEART_MODEL=openai/gpt-oss-120b heart -p "…"
136
+
137
+ # saved for every run (writes ~/.heart/config.json)
138
+ HEART_API_KEY=… heart -u https://api.groq.com/openai/v1 -m openai/gpt-oss-120b --save -p "hello"
139
+ ```
140
+
141
+ A model server on this machine (Ollama on :11434, LM Studio on :1234, llama.cpp on :8080) is
142
+ found on its own. The user can also set everything up in a browser: `heart settings`.
143
+
144
+ ## More context for the task
145
+
146
+ | Flag | |
147
+ |---|---|
148
+ | `--append-system-prompt "<text>"` | added to HeartCode's system prompt for this run |
149
+ | `--mcp-config <file or JSON>` | more MCP servers (repeatable); `--strict-mcp-config`: only those |
150
+ | `--skills-dir <folder>` | more skills (repeatable) |
151
+ | `--no-memory` | don't read or write HeartCode's memory notes |
152
+
153
+ A project's own `HEARTCODE.md` (or `AGENTS.md`, `CLAUDE.md`) is read like instructions.
154
+
155
+ ## Long tasks: a stream of events
156
+
157
+ ```sh
158
+ heart -p --input-format stream-json --output-format stream-json
159
+ ```
160
+
161
+ Stdin: one message per line, `{"type":"user","message":{"role":"user","content":"…"}}`.
162
+ Stop the running one with `{"type":"control_request","request_id":"1","request":{"subtype":"interrupt"}}`.
163
+ Stdout: one JSON event per line, `system`/`init`, then `assistant` (text, thinking, `tool_use`),
164
+ `user` (`tool_result`), and a `result` after each message. It runs until stdin closes.
165
+
166
+ ## Don't
167
+
168
+ - Don't run `heart` without `-p` from an agent: it's a full-screen app and waits for a person.
169
+ - Don't put API keys on the command line where other processes can see them; use `HEART_API_KEY`.
170
+ - Don't run `heart settings` unless the user asked: it starts a web page and runs until stopped.
package/bin/heart.js ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ // `heart` (and `heartcode`): what `npm start` installs. Then HeartCode itself (heartcode.js).
3
+ // heart this folder
4
+ // heart . this folder
5
+ // heart ~/code/my-app that folder (a folder: a file is an error)
6
+ // heart ~/code/my-app "fix the failing test" -m … then anything heartcode takes (heart --help)
7
+ // heart --temp … a throwaway ~/.heart: only your config.json (providers, keys), the
8
+ // models' test results and prices copied in; deleted when HeartCode
9
+ // quits; your own is never written to
10
+ // Logs, when they're switched on (the settings page: Logging): each run's log in
11
+ // ~/.heart/logs/runs/<day-month-year-hour-min-sec>/, a file per tab ("log": true in config.json);
12
+ // each request to a model in ~/.heart/logs/requests/<same>/, a file per request ("logRequests").
13
+ // Kept for logDays days (30 unless changed). HEART_LOG=/path/file.log logs this run in that one file
14
+ // anyway; HEART_LOG=off: no run log.
15
+ import fs from 'node:fs'
16
+ import os from 'node:os'
17
+ import path from 'node:path'
18
+
19
+ const fail = (msg) => {
20
+ console.error(msg)
21
+ process.exit(1)
22
+ }
23
+ if (Number(process.versions.node.split('.')[0]) < 20) fail(`HeartCode needs Node.js 20 or newer (you have ${process.version}): https://nodejs.org`)
24
+
25
+ const args = process.argv.slice(2)
26
+ const realHome = process.env.HEART_HOME || path.join(os.homedir(), '.heart')
27
+ const stamp = (() => {
28
+ const d = new Date()
29
+ return `${d.getDate()}-${d.getMonth() + 1}-${d.getFullYear()}-${d.getHours()}-${d.getMinutes()}-${d.getSeconds()}`.replace(/\b(\d)\b/g, '0$1')
30
+ })()
31
+
32
+ // --temp: HeartCode's folder is a new one in the system's temp folder
33
+ const temp = args.includes('--temp')
34
+ if (temp) {
35
+ args.splice(args.indexOf('--temp'), 1)
36
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'heart-temp-'))
37
+ // your providers and keys; what HeartCode learned about each model and prices (or it tests them again)
38
+ for (const f of ['config.json', 'models', 'prices.json']) if (fs.existsSync(path.join(realHome, f))) fs.cpSync(path.join(realHome, f), path.join(dir, f), { recursive: true })
39
+ process.env.HEART_HOME = dir
40
+ process.on('exit', () => fs.rmSync(dir, { recursive: true, force: true }))
41
+ }
42
+
43
+ // a folder first: HeartCode opens it (subcommands and options go straight through)
44
+ const SUBCOMMANDS = ['probe', 'models', 'providers', 'config', 'settings']
45
+ const first = args[0]
46
+ if (first && !first.startsWith('-') && !SUBCOMMANDS.includes(first)) {
47
+ const where = path.resolve(first.replace(/^~(?=\/|$)/, os.homedir()))
48
+ const stat = fs.statSync(where, { throwIfNoEntry: false })
49
+ if (stat?.isDirectory()) args.splice(0, 1, '-C', where)
50
+ else if (stat) fail(`${first} is a file: heart opens a folder (heart path/to/folder)`)
51
+ else if (/^(\.|~|\/)|[\\/]/.test(first)) fail(`no such folder: ${first}`)
52
+ // else: words, a task to run (heart "fix the failing test")
53
+ }
54
+
55
+ // the run's log, if switched on (not for -p/--print: its callers set HEART_LOG themselves if they
56
+ // want one; nor for --help and --version); requests are logged by HeartCode itself (src/log.js)
57
+ const logOn = (() => {
58
+ try {
59
+ return JSON.parse(fs.readFileSync(path.join(process.env.HEART_HOME || realHome, 'config.json'), 'utf8')).log === true
60
+ } catch {
61
+ return false
62
+ }
63
+ })()
64
+ const logs = temp ? path.join(os.tmpdir(), 'heart-temp-logs') : path.join(realHome, 'logs')
65
+ process.env.HEART_LOGS ??= logs
66
+ let runStamp = stamp
67
+ for (let n = 2; ['runs', 'requests'].some((d) => fs.existsSync(path.join(logs, d, runStamp))); n++) runStamp = `${stamp}-${n}` // two runs in the same second
68
+ process.env.HEART_LOG_STAMP ??= runStamp
69
+ // The run's folder is set aside either way (made with its first line): the Logging page's switch
70
+ // turns the log on or off while HeartCode runs (src/log.js, HEART_LOG_RUN).
71
+ if (!process.env.HEART_LOG && !process.env.HEART_LOG_DIR && !args.some((a) => ['-p', '--print', '-h', '--help', '-v', '--version'].includes(a))) {
72
+ process.env.HEART_LOG_DIR = path.join(logs, 'runs', runStamp) // old ones: deleted after logDays (src/log.js)
73
+ process.env.HEART_LOG_RUN = logOn ? 'on' : 'off' // how it starts
74
+ }
75
+
76
+ process.argv = [process.argv[0], new URL('./heartcode.js', import.meta.url).pathname, ...args]
77
+ await import('./heartcode.js')