@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 CHANGED
@@ -3,13 +3,16 @@
3
3
  > The agent harness that gives your LLM memory, hands, and a nervous system.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/%40princetheprogrammerbtw%2Fhusk.svg)](https://www.npmjs.com/package/@princetheprogrammerbtw/husk)
6
+ [![npm downloads](https://img.shields.io/npm/dm/%40princetheprogrammerbtw%2Fhusk.svg)](https://www.npmjs.com/package/@princetheprogrammerbtw/husk)
6
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
8
  [![Node](https://img.shields.io/node/v/%40princetheprogrammerbtw%2Fhusk.svg)](https://nodejs.org)
8
- [![CI](https://github.com/10xdev4u-alt/husk/actions/workflows/ci.yml/badge.svg)](./.github/workflows/ci.yml)
9
+ [![CI](https://github.com/10xdev4u-alt/husk/actions/workflows/ci.yml/badge.svg)](https://github.com/10xdev4u-alt/husk/actions/workflows/ci.yml)
10
+ [![GitHub stars](https://img.shields.io/github/stars/10xdev4u-alt/husk.svg)](https://github.com/10xdev4u-alt/husk/stargazers)
11
+ [![Bundle size](https://img.shields.io/bundlephobia/minzip/%40princetheprogrammerbtw%2Fhusk.svg)](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, full TypeScript types
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. The agent loop is ~150 lines. Provider adapters are the only files that know about provider-specific wire formats.
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 = values.help === void 0 ? process.argv[3] : void 0;
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
- Options:
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.1.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