@princetheprogrammerbtw/husk 0.1.1 → 0.3.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/README.md +76 -11
- package/dist/cli/index.js +164 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +372 -1
- package/dist/index.js +438 -1
- package/dist/index.js.map +1 -1
- package/dist/otel/index.d.ts +49 -0
- package/dist/otel/index.js +75 -0
- package/dist/otel/index.js.map +1 -0
- package/dist/tracer-y41CTrNG.d.ts +64 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
> The agent harness that gives your LLM memory, hands, and a nervous system.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@princetheprogrammerbtw/husk)
|
|
6
|
+
[](https://www.npmjs.com/package/@princetheprogrammerbtw/husk)
|
|
6
7
|
[](https://opensource.org/licenses/MIT)
|
|
7
8
|
[](https://nodejs.org)
|
|
8
|
-
[](
|
|
9
|
+
[](https://github.com/10xdev4u-alt/husk/actions/workflows/ci.yml)
|
|
10
|
+
[](https://github.com/10xdev4u-alt/husk/stargazers)
|
|
11
|
+
[](https://bundlephobia.com/package/@princetheprogrammerbtw/husk)
|
|
9
12
|
|
|
10
13
|
## What is Husk?
|
|
11
14
|
|
|
12
|
-
Most LLM calls are a brain in a jar — they can think, but can't act, remember, verify their own work, or show you what they did. **Husk** is the body, hands, memory, and nervous system you wrap around any LLM (Claude, GPT, Gemini, local models) to turn it into a real agent.
|
|
15
|
+
Most LLM calls are a **brain in a jar** — they can think, but can't act, remember, verify their own work, or show you what they did. **Husk** is the body, hands, memory, and nervous system you wrap around any LLM (Claude, GPT, Gemini, local models) to turn it into a real agent.
|
|
13
16
|
|
|
14
17
|
```ts
|
|
15
18
|
import { Agent, AnthropicProvider, Read, Write, Edit, Bash, Grep, FileStore } from '@princetheprogrammerbtw/husk';
|
|
@@ -31,16 +34,27 @@ const result = await agent.run('Review src/core/agent.ts');
|
|
|
31
34
|
console.log(result.output);
|
|
32
35
|
```
|
|
33
36
|
|
|
37
|
+
## Why Husk?
|
|
38
|
+
|
|
39
|
+
| You're used to… | Husk gives you… |
|
|
40
|
+
|---|---|
|
|
41
|
+
| One-shot LLM calls with no memory | Persistent file-backed or in-memory memory across calls |
|
|
42
|
+
| Hand-rolled tool-calling loops | A small, typed event stream you can subscribe to |
|
|
43
|
+
| Tied to one provider's SDK | Provider-agnostic core; swap Anthropic ↔ OpenAI in one line |
|
|
44
|
+
| Reinventing agent loops in every project | Drop-in `Agent` class with stop conditions, parallel tool execution, and error recovery |
|
|
45
|
+
| No observability into what the model actually did | Typed events for every iteration, tool call, and provider response |
|
|
46
|
+
|
|
34
47
|
## Features
|
|
35
48
|
|
|
36
49
|
- 🧠 **Provider-agnostic** — Anthropic, OpenAI, more coming. Bring your own model.
|
|
37
|
-
- 🛠️ **5 built-in tools** — `Read`, `Write`, `Edit`, `Bash` (with safety denylist), `Grep` (ripgrep with grep fallback)
|
|
50
|
+
- 🛠️ **5 built-in tools** — `Read`, `Write`, `Edit`, `Bash` (with safety denylist for `rm -rf /`, fork bombs, etc.), `Grep` (ripgrep with grep fallback)
|
|
38
51
|
- 💾 **Memory** — `InMemoryStore` for sessions, `FileStore` for persistence
|
|
39
52
|
- 👀 **Observability** — typed event emitter, drop in any logger or tracer
|
|
40
53
|
- 🧭 **Steering** — system prompts, numbered rules, few-shot examples
|
|
41
54
|
- 🤝 **Sub-agents** — compose agents inside agents (see [multi-agent example](./examples/03-multi-agent))
|
|
42
|
-
- 📦 **Batteries included** — 35KB ESM bundle,
|
|
55
|
+
- 📦 **Batteries included** — 35KB ESM bundle, 26KB d.ts, zero runtime deps except the provider SDKs
|
|
43
56
|
- 🖥️ **CLI** — `husk run "<prompt>"` for one-shot invocations
|
|
57
|
+
- 🔒 **Type-safe** — strict TypeScript, no `any`, full type definitions shipped
|
|
44
58
|
|
|
45
59
|
## Install
|
|
46
60
|
|
|
@@ -50,6 +64,8 @@ npm install @princetheprogrammerbtw/husk
|
|
|
50
64
|
pnpm add @princetheprogrammerbtw/husk
|
|
51
65
|
# or
|
|
52
66
|
bun add @princetheprogrammerbtw/husk
|
|
67
|
+
# or
|
|
68
|
+
yarn add @princetheprogrammerbtw/husk
|
|
53
69
|
```
|
|
54
70
|
|
|
55
71
|
You'll also need an API key for the provider you choose:
|
|
@@ -61,7 +77,7 @@ export OPENAI_API_KEY=sk-... # for GPT
|
|
|
61
77
|
|
|
62
78
|
## Quickstart
|
|
63
79
|
|
|
64
|
-
The smallest possible agent:
|
|
80
|
+
The smallest possible agent — model, prompt, done:
|
|
65
81
|
|
|
66
82
|
```ts
|
|
67
83
|
import { Agent, AnthropicProvider } from '@princetheprogrammerbtw/husk';
|
|
@@ -74,6 +90,41 @@ const result = await agent.run('What is the capital of France? Answer in one sen
|
|
|
74
90
|
console.log(result.output); // "Paris"
|
|
75
91
|
```
|
|
76
92
|
|
|
93
|
+
A more realistic agent — with tools, memory, and steering:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import {
|
|
97
|
+
Agent, AnthropicProvider, Read, Write, Edit, Bash, Grep,
|
|
98
|
+
FileStore, InMemoryStore,
|
|
99
|
+
} from '@princetheprogrammerbtw/husk';
|
|
100
|
+
|
|
101
|
+
const agent = new Agent({
|
|
102
|
+
model: new AnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY }),
|
|
103
|
+
tools: [Read, Write, Edit, Bash, Grep],
|
|
104
|
+
memory: new FileStore({ path: './.husk/memory' }),
|
|
105
|
+
steering: {
|
|
106
|
+
systemPrompt: 'You are a careful code reviewer.',
|
|
107
|
+
rules: [
|
|
108
|
+
'Read the file in full before commenting.',
|
|
109
|
+
'Cite specific line numbers for every finding.',
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const result = await agent.run('Review src/core/agent.ts');
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Swapping to OpenAI is a one-line change:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { OpenAIProvider } from '@princetheprogrammerbtw/husk';
|
|
121
|
+
|
|
122
|
+
const agent = new Agent({
|
|
123
|
+
model: new OpenAIProvider({ apiKey: process.env.OPENAI_API_KEY }),
|
|
124
|
+
// ...same config otherwise
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
77
128
|
## CLI
|
|
78
129
|
|
|
79
130
|
```bash
|
|
@@ -84,9 +135,11 @@ husk run "Summarize README.md" --provider openai --model gpt-5
|
|
|
84
135
|
husk run --help
|
|
85
136
|
```
|
|
86
137
|
|
|
138
|
+
The CLI wraps the same `Agent` class — flags map directly to `AgentConfig` fields.
|
|
139
|
+
|
|
87
140
|
## Examples
|
|
88
141
|
|
|
89
|
-
Three worked examples in the `examples/` directory:
|
|
142
|
+
Three worked examples in the [`examples/`](./examples) directory:
|
|
90
143
|
|
|
91
144
|
- **[01-hello-agent](./examples/01-hello-agent)** — minimal agent, no tools
|
|
92
145
|
- **[02-code-reviewer](./examples/02-code-reviewer)** — full tool set + steering for code review
|
|
@@ -96,9 +149,10 @@ Run any example with `bun run examples/0X-name/index.ts`.
|
|
|
96
149
|
|
|
97
150
|
## Documentation
|
|
98
151
|
|
|
99
|
-
- **[Learning Journal](./LEARNING.md)** — design decisions, trade-offs, and lessons learned
|
|
100
|
-
- **[Changelog](./CHANGELOG.md)** — release history
|
|
101
|
-
- **[Contributing](./CONTRIBUTING.md)** — how to contribute
|
|
152
|
+
- 📓 **[Learning Journal](./LEARNING.md)** — design decisions, trade-offs, and lessons learned while building
|
|
153
|
+
- 📋 **[Changelog](./CHANGELOG.md)** — release history
|
|
154
|
+
- 🤝 **[Contributing](./CONTRIBUTING.md)** — how to contribute
|
|
155
|
+
- 🏗️ **[Architecture](#architecture)** — the module layout, below
|
|
102
156
|
|
|
103
157
|
## Architecture
|
|
104
158
|
|
|
@@ -111,15 +165,26 @@ src/
|
|
|
111
165
|
└── index.ts # public API surface
|
|
112
166
|
```
|
|
113
167
|
|
|
114
|
-
Every piece composes through a typed event stream
|
|
168
|
+
Every piece composes through a **typed event stream**. The agent loop is ~150 lines. Provider adapters are the only files that know about provider-specific wire formats. Tools are plain objects implementing a 4-field interface — register by passing an array to the Agent.
|
|
115
169
|
|
|
116
170
|
## Roadmap
|
|
117
171
|
|
|
118
172
|
- **v0.1.0** ✅ Core loop, Anthropic + OpenAI, 5 built-in tools, memory, observability, CLI
|
|
173
|
+
- **v0.1.1** ✅ CLI shebang fix, version bump
|
|
119
174
|
- **v0.2.0** Eval runner, OTel export, Ollama adapter
|
|
120
175
|
- **v0.3.0** Vector memory, hosted dashboard
|
|
121
176
|
- **v1.0.0** Stable API, marketplace, enterprise features
|
|
122
177
|
|
|
178
|
+
## Contributing
|
|
179
|
+
|
|
180
|
+
PRs welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for the dev setup, scripts, and commit conventions.
|
|
181
|
+
|
|
182
|
+
The project follows Conventional Commits. Every commit body explains *why*, not what — the diff already shows what.
|
|
183
|
+
|
|
184
|
+
## Show your support
|
|
185
|
+
|
|
186
|
+
If Husk saves you time, ⭐️ the [GitHub repo](https://github.com/10xdev4u-alt/husk) — it helps others find the project. Issues, PRs, and feedback all welcome.
|
|
187
|
+
|
|
123
188
|
## License
|
|
124
189
|
|
|
125
|
-
MIT © 2026 princetheprogrammerbtw
|
|
190
|
+
MIT © 2026 [princetheprogrammerbtw](https://github.com/10xdev4u-alt)
|
package/dist/cli/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, statSync, promises } from 'fs';
|
|
3
|
+
import { readdir } from 'fs/promises';
|
|
4
|
+
import { resolve, extname, dirname, join } from 'path';
|
|
5
|
+
import { pathToFileURL } from 'url';
|
|
2
6
|
import { promisify, parseArgs } from 'util';
|
|
3
|
-
import { promises } from 'fs';
|
|
4
|
-
import { resolve, dirname, join } from 'path';
|
|
5
7
|
import Anthropic from '@anthropic-ai/sdk';
|
|
6
8
|
import OpenAI from 'openai';
|
|
7
9
|
import { exec } from 'child_process';
|
|
@@ -1042,6 +1044,71 @@ function truncateOutput(output, limit) {
|
|
|
1042
1044
|
... (${lines.length - limit} more matches truncated)`;
|
|
1043
1045
|
}
|
|
1044
1046
|
|
|
1047
|
+
// src/evals/runner.ts
|
|
1048
|
+
async function runSuite(suite, factory, options = {}) {
|
|
1049
|
+
const start = Date.now();
|
|
1050
|
+
const results = [];
|
|
1051
|
+
let passed = 0;
|
|
1052
|
+
for (const c of suite.cases) {
|
|
1053
|
+
options.onCaseStart?.(c.name);
|
|
1054
|
+
const caseResult = await runCase(c, factory);
|
|
1055
|
+
results.push(caseResult);
|
|
1056
|
+
if (caseResult.passed) passed += 1;
|
|
1057
|
+
options.onCaseEnd?.(caseResult);
|
|
1058
|
+
if (options.failFast && !caseResult.passed) {
|
|
1059
|
+
break;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return {
|
|
1063
|
+
suiteName: suite.name,
|
|
1064
|
+
results,
|
|
1065
|
+
passed,
|
|
1066
|
+
total: suite.cases.length,
|
|
1067
|
+
durationMs: Date.now() - start
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
async function runCase(c, factory) {
|
|
1071
|
+
const start = Date.now();
|
|
1072
|
+
const agent = await factory();
|
|
1073
|
+
let agentResult;
|
|
1074
|
+
try {
|
|
1075
|
+
agentResult = await agent.run(c.input);
|
|
1076
|
+
} catch (err) {
|
|
1077
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1078
|
+
const errorAssertionResult = {
|
|
1079
|
+
pass: false,
|
|
1080
|
+
name: "agent.run",
|
|
1081
|
+
message: `agent.run threw: ${message}`
|
|
1082
|
+
};
|
|
1083
|
+
return {
|
|
1084
|
+
caseName: c.name,
|
|
1085
|
+
passed: false,
|
|
1086
|
+
assertionResults: [errorAssertionResult],
|
|
1087
|
+
agentResult: {
|
|
1088
|
+
output: "",
|
|
1089
|
+
messages: [],
|
|
1090
|
+
iterations: 0,
|
|
1091
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
1092
|
+
durationMs: Date.now() - start
|
|
1093
|
+
},
|
|
1094
|
+
durationMs: Date.now() - start
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
const assertionResults = [];
|
|
1098
|
+
for (const a of c.assertions) {
|
|
1099
|
+
const r = await a(agentResult);
|
|
1100
|
+
assertionResults.push(r);
|
|
1101
|
+
}
|
|
1102
|
+
const allPassed = assertionResults.every((r) => r.pass);
|
|
1103
|
+
return {
|
|
1104
|
+
caseName: c.name,
|
|
1105
|
+
passed: allPassed,
|
|
1106
|
+
assertionResults,
|
|
1107
|
+
agentResult,
|
|
1108
|
+
durationMs: Date.now() - start
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1045
1112
|
// src/cli/index.ts
|
|
1046
1113
|
var TOOL_REGISTRY = { read: Read, write: Write, edit: Edit, bash: Bash, grep: Grep };
|
|
1047
1114
|
async function main() {
|
|
@@ -1054,6 +1121,10 @@ async function main() {
|
|
|
1054
1121
|
await runCommand();
|
|
1055
1122
|
return;
|
|
1056
1123
|
}
|
|
1124
|
+
if (subcommand === "eval") {
|
|
1125
|
+
await evalCommand();
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1057
1128
|
if (subcommand === "version" || subcommand === "--version" || subcommand === "-v") {
|
|
1058
1129
|
console.log(`husk ${VERSION}`);
|
|
1059
1130
|
return;
|
|
@@ -1079,7 +1150,7 @@ async function runCommand() {
|
|
|
1079
1150
|
printHelp();
|
|
1080
1151
|
return;
|
|
1081
1152
|
}
|
|
1082
|
-
const prompt =
|
|
1153
|
+
const prompt = process.argv[3];
|
|
1083
1154
|
if (!prompt) {
|
|
1084
1155
|
console.error("Error: husk run requires a prompt argument.");
|
|
1085
1156
|
console.error('Usage: husk run "your prompt here"');
|
|
@@ -1111,13 +1182,95 @@ async function runCommand() {
|
|
|
1111
1182
|
console.log(result.output);
|
|
1112
1183
|
process.exit(0);
|
|
1113
1184
|
}
|
|
1185
|
+
async function evalCommand() {
|
|
1186
|
+
const target = process.argv[3];
|
|
1187
|
+
if (!target) {
|
|
1188
|
+
console.error("Error: husk eval requires a path argument.");
|
|
1189
|
+
console.error("Usage: husk eval <file-or-dir>");
|
|
1190
|
+
process.exit(2);
|
|
1191
|
+
}
|
|
1192
|
+
const resolved = resolve(target);
|
|
1193
|
+
if (!existsSync(resolved)) {
|
|
1194
|
+
console.error(`Error: path not found: ${resolved}`);
|
|
1195
|
+
process.exit(2);
|
|
1196
|
+
}
|
|
1197
|
+
const stat = statSync(resolved);
|
|
1198
|
+
const files = [];
|
|
1199
|
+
if (stat.isDirectory()) {
|
|
1200
|
+
const entries = await readdir(resolved, { withFileTypes: true });
|
|
1201
|
+
for (const e of entries) {
|
|
1202
|
+
if (!e.isFile()) continue;
|
|
1203
|
+
const ext = extname(e.name);
|
|
1204
|
+
if (ext === ".ts" || ext === ".js" || ext === ".mjs") {
|
|
1205
|
+
files.push(resolve(resolved, e.name));
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
} else {
|
|
1209
|
+
files.push(resolved);
|
|
1210
|
+
}
|
|
1211
|
+
if (files.length === 0) {
|
|
1212
|
+
console.error(`Error: no .ts/.js/.mjs files found in ${resolved}`);
|
|
1213
|
+
process.exit(2);
|
|
1214
|
+
}
|
|
1215
|
+
let totalPassed = 0;
|
|
1216
|
+
let totalCases = 0;
|
|
1217
|
+
let anyFailed = false;
|
|
1218
|
+
for (const file of files) {
|
|
1219
|
+
console.log(`
|
|
1220
|
+
=== ${file} ===`);
|
|
1221
|
+
try {
|
|
1222
|
+
const mod = await import(pathToFileURL(file).href);
|
|
1223
|
+
const suites = [];
|
|
1224
|
+
for (const value of Object.values(mod)) {
|
|
1225
|
+
if (value && typeof value === "object" && "name" in value && "cases" in value && Array.isArray(value.cases)) {
|
|
1226
|
+
suites.push(value);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
if (suites.length === 0) {
|
|
1230
|
+
console.error(` No EvalSuite found in ${file}`);
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
for (const suite of suites) {
|
|
1234
|
+
const factory = () => Promise.resolve(makeDefaultAgent());
|
|
1235
|
+
const result = await runSuite(suite, factory);
|
|
1236
|
+
totalPassed += result.passed;
|
|
1237
|
+
totalCases += result.total;
|
|
1238
|
+
for (const r of result.results) {
|
|
1239
|
+
const icon = r.passed ? "\u2713" : "\u2717";
|
|
1240
|
+
console.log(` ${icon} ${r.caseName}`);
|
|
1241
|
+
if (!r.passed) {
|
|
1242
|
+
anyFailed = true;
|
|
1243
|
+
for (const a of r.assertionResults) {
|
|
1244
|
+
console.log(` \u2717 ${a.name}: ${a.message ?? "failed"}`);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
console.log(` ${result.passed}/${result.total} passed in ${result.durationMs}ms`);
|
|
1249
|
+
}
|
|
1250
|
+
} catch (err) {
|
|
1251
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1252
|
+
console.error(` Error loading ${file}: ${message}`);
|
|
1253
|
+
anyFailed = true;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
console.log(`
|
|
1257
|
+
=== Total: ${totalPassed}/${totalCases} cases passed ===`);
|
|
1258
|
+
process.exit(anyFailed ? 1 : 0);
|
|
1259
|
+
}
|
|
1260
|
+
function makeDefaultAgent() {
|
|
1261
|
+
const providerName = process.env.HUSK_PROVIDER ?? "anthropic";
|
|
1262
|
+
const modelId = process.env.HUSK_MODEL ?? "claude-opus-4-6";
|
|
1263
|
+
const provider = providerName === "openai" ? new OpenAIProvider({ model: modelId, apiKey: process.env.OPENAI_API_KEY }) : new AnthropicProvider({ model: modelId, apiKey: process.env.ANTHROPIC_API_KEY });
|
|
1264
|
+
return new Agent({ model: provider });
|
|
1265
|
+
}
|
|
1114
1266
|
function printHelp() {
|
|
1115
|
-
console.log(`husk \u2014 run an agent from the command line
|
|
1267
|
+
console.log(`husk \u2014 run an agent or eval suite from the command line
|
|
1116
1268
|
|
|
1117
1269
|
Usage:
|
|
1118
1270
|
husk run "<prompt>" [options]
|
|
1271
|
+
husk eval <file-or-dir>
|
|
1119
1272
|
|
|
1120
|
-
|
|
1273
|
+
Run options:
|
|
1121
1274
|
--model <id> Model id (default: claude-opus-4-6)
|
|
1122
1275
|
--provider <name> 'anthropic' (default) or 'openai'
|
|
1123
1276
|
--tools <list> Comma-separated tool names: read,write,edit,bash,grep
|
|
@@ -1127,6 +1280,10 @@ Options:
|
|
|
1127
1280
|
-h, --help Show this help
|
|
1128
1281
|
-v, --version Show version
|
|
1129
1282
|
|
|
1283
|
+
Eval options:
|
|
1284
|
+
<file> A .ts/.js/.mjs file exporting one or more EvalSuite
|
|
1285
|
+
<dir> A directory; all *.ts/*.js/*.mjs files are loaded
|
|
1286
|
+
|
|
1130
1287
|
Environment:
|
|
1131
1288
|
ANTHROPIC_API_KEY Required for Anthropic provider
|
|
1132
1289
|
OPENAI_API_KEY Required for OpenAI provider
|
|
@@ -1137,9 +1294,10 @@ Examples:
|
|
|
1137
1294
|
husk run "What is the capital of France?"
|
|
1138
1295
|
husk run "Refactor src/foo.ts" --tools read,edit,write
|
|
1139
1296
|
husk run "Summarize README.md" --provider openai --model gpt-5
|
|
1297
|
+
husk eval ./evals/geography.ts
|
|
1140
1298
|
`);
|
|
1141
1299
|
}
|
|
1142
|
-
var VERSION = "0.
|
|
1300
|
+
var VERSION = "0.3.0-dev.0";
|
|
1143
1301
|
await main();
|
|
1144
1302
|
//# sourceMappingURL=index.js.map
|
|
1145
1303
|
//# sourceMappingURL=index.js.map
|