Repository navigation
LOU-H: DX — zero-config, CLI scaffold & playground - #9
Merged
Merged
Conversation
Adds src/createAgent.ts exposing createAgent({prompt, provider, tools}) ->
{send}, a thin composition of the existing AgentBuilder + ToolRegistry +
static AgentExecutor.execute(). AgentExecutor.execute() does not require
any repositories, so none are wired up here. tools is keyed by name
(Record<string, ToolDescriptor>) rather than an array, since ToolDescriptor
carries no name of its own and AgentConfig.tools is itself name-keyed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds packages/create-loushy-agent with its own package.json (bin: create-loushy-agent -> bin/cli.js), a #!/usr/bin/env node bin/cli.js that requires dist/index.js's exported main(), and src/index.ts using Node's built-in util.parseArgs (no new dependency) for --help/--name/--provider /--tools/--yes and a clear "Unknown flag: --xyz" message + exit 1 for anything else. Bundled via its own tsup.config.ts (CJS only, matching the root project's tsup style). Verified: --help exits 0 with usage on stdout; an unrecognized flag exits 1 with a message on stderr. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds packages/create-loushy-agent/src/prompts.ts exporting AnswerConfig
{name, provider, tools} and collectAnswers() built on the `prompts`
package (text for name, select for provider, multiselect for tools, with
http/github as starter tool choices). Wired into main(): a --yes flag, or
both --name and --provider given together, skips the interactive prompts
entirely and builds AnswerConfig from flags via the same validateAnswers()
guard collectAnswers() itself uses, so an invalid --provider value throws
naming the bad value and the allowed set.
Tested two ways: a direct unit test using prompts' own `prompts.inject()`
dependency-injection support (no TTY needed), and a spawned-process
integration test driving the real bin/cli.js with --name/--provider/--yes
flags and asserting on stdout.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds packages/create-loushy-agent/src/template.ts: generateProject(dir, answers) writes a generated package.json, tsconfig.json (mirroring the root project's baseline compilerOptions), src/agent.ts (calling createAgent() from LOU-H1 with the chosen provider via resolveProvider() and any wired tools), and a .env.example (LOU-H5, see next commit). The SDK isn't published to the npm registry, so a plain semver dependency spec would not be installable from a project generated outside this worktree. generateProject() instead `npm pack`s the SDK's own current source tree and copies the resulting tarball alongside the generated project, wiring a `file:./<tarball>` dependency - this is exact-version- pinned (the tarball's version is read straight from the SDK's own package.json) and genuinely installable from anywhere. main() gained --dir/--tools shortcuts so generateProject() is directly testable. Verified end-to-end for real: scaffolds into a fresh os.tmpdir() temp directory (never inside this worktree) with --name=test-agent --provider=openai --yes, then actually runs `npm install && npm run build` inside it (exit 0), and diffs the generated project's installed @loushy/build-ai-agent version against the SDK's own root package.json version, confirming an exact match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PROVIDER_ENV_VARS in template.ts (added in the LOU-H4 commit) already reuses LOU-F8's resolveProvider() env table verbatim (OPENAI_API_KEY, ANTHROPIC_API_KEY, OLLAMA_BASE_URL - note: OLLAMA_BASE_URL, not OLLAMA_HOST as the ticket's illustrative snippet assumed; LOU-F8's real table uses OLLAMA_BASE_URL) and generateProject() already writes a .env.example naming the right var. This commit adds the dedicated test: scaffolds three projects (one per provider) into separate temp dirs via the real CLI and greps each .env.example for the exact expected var name. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds src/cli/dev.ts exporting startDevServer(configPath, port = 3737),
built on Node's built-in http module. GET /health -> 200 'ok'; POST /chat
parses the JSON body and calls the loaded agent's real send() (createAgent
from LOU-H1 under the hood, so this goes through the genuine static
AgentExecutor.execute() path), returning the ExecutionResult as JSON. Port
availability is checked up front via the server's 'error' listener -
EADDRINUSE rejects startDevServer() with a message naming the port instead
of crashing uncaught.
Config loading (deferred decision, judgment call): configPath is a small
ad-hoc JSON file ({name, prompt, provider: {type, model}, tools?}) for now.
LOU-H9 (declarative agent spec format) is the designed-for successor;
dev.ts is retrofitted to use loadSpec()+specToAgent() once that lands
(see the LOU-H9 commit) rather than duplicating config-loading logic.
Provider resolution reuses LOU-F8's resolveProvider() for real provider
types (openai/anthropic/ollama/openrouter). A non-real type (e.g. 'mock',
which is intentionally outside resolveProvider()'s env-var whitelist)
falls through to LLMProviderRegistry.create() directly - this is what
lets tests run the dev server end-to-end against MockLLMProvider without
real API credentials.
Added a "loushy" bin entry to the ROOT package.json (bin/loushy.js ->
dist/cli/dev.js's startDevServer), since the ticket's file path
(src/cli/dev.ts under the root project) points at the root package as the
natural home, not packages/create-loushy-agent.
Verified: server started against a sample JSON config using a
LLMProviderRegistry-registered mock provider; POST /chat returns the mock
provider's exact response; GET /health returns 200; starting a second
server on an already-bound port rejects with a "port N is already in use"
error.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds src/cli/dev-ui/index.html: an input#msg, button#send and div#log,
with an inline script that fetch()es POST /chat and appends the reply to
the log div with no page reload. Wired a GET / route in dev.ts serving
this file with Content-Type: text/html.
Static HTML isn't something tsup bundles, so tsup.config.ts gained an
onSuccess hook that copies src/cli/dev-ui/ to dist/cli/dev-ui/ after each
build, matching where dev.ts's path.join(__dirname, 'dev-ui', ...) looks
for it at runtime.
Verified: GET / returns HTML containing id="msg"/id="send"/id="log"; the
page's own inline script targets fetch('/chat'); driving that exact same
POST /chat call (the realistic message round trip the page performs)
returns the mock provider's reply.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dev.ts now watches configPath via Node's built-in fs.watch. The current
agent lives in a mutable top-level holder ({agent}) so /chat always reads
the latest reloaded version without restarting the HTTP server or
dropping the listening port. On a reload failure (e.g. invalid JSON, or a
valid-JSON-but-missing-required-field edit), the error is logged and the
previous working agent is kept in the holder rather than crashing.
Verified: started the dev server, sent one message and recorded the mock
response, edited the config file's prompt (and swapped the registered
mock provider's response) on disk, waited 400ms for the fs.watch event,
sent a second message and confirmed it reflects the new config while
`handle.server` and the port are unchanged (no restart) - then wrote an
invalid edit (missing 'prompt') and confirmed the server keeps serving the
last-good config instead of crashing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds src/spec/: schema.ts (AgentSpec {name, prompt, provider: {type,
model}, tools?} plus a zod runtime schema mirroring it), loadSpec.ts
(loads .yaml/.yml via the new `yaml` dependency, or .json via JSON.parse,
by extension; validates via the zod schema and throws an Error naming
each invalid/missing field, e.g. "'prompt': Required"), and
specToAgent.ts (maps a validated spec to a live agent via createAgent()
from LOU-H1, resolving `tools` names against the SDK's real built-in
tools - http/current-date/day-name resolve directly; 'github'/'jira'
throw a clear, actionable error instead of silently registering a broken
tool, since GitHubTools/JiraTools are credentialed factories with no spec
field to source token/owner/repo or baseUrl/email/apiToken from).
Retrofit (LOU-H6 integration): dev.ts now loads configs via
loadSpec()+specToAgent() instead of its original ad-hoc JSON loader - the
natural integration point the epic calls out. The old shape
({name, prompt, provider, tools}) is a valid AgentSpec as-is, so every
LOU-H6/H7/H8 test config and test still passes unmodified. Provider
resolution (real types through LOU-F8's resolveProvider(), 'mock' and any
other LLMProviderRegistry-registered type via the registry directly) moved
from dev.ts into specToAgent.ts as resolveSpecProvider(), now the single
canonical implementation.
Tested: an authored .yaml spec and an equivalent createAgent()-built agent
(same prompt, same scripted MockLLMProvider) produce identical text,
finishReason and toolCalls for the same input; a spec missing 'prompt'
throws naming that field; an unrecognized tool name and a credentialed
tool (github/jira) with no config each throw a distinct, clear error.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds 5 example agents under examples/ - support-bot, research-assistant, workflow-router, doc-qa, slack-notifier - each with index.ts (built on createAgent() from LOU-H1), a minimal package.json, and a README.md. Every index.ts defaults to a free MockLLMProvider (from src/providers/mock.ts) so it runs with zero setup and zero API cost, and only switches to a real provider (via resolveProvider(), LOU-F8) when the relevant env var is already set (OPENAI_API_KEY for support-bot/ workflow-router/slack-notifier, ANTHROPIC_API_KEY for research-assistant, OLLAMA_BASE_URL for doc-qa) - spreading coverage across all three real provider types rather than defaulting every example to the same one. research-assistant also wires up the built-in http tool. Added root package.json scripts (example:support-bot, etc., matching the existing example:tracing:* / tsx convention from LOU-E6) and verified each one runs to completion with exit 0 against its default mock provider. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds examples/README.md with one `## [dirname](./dirname)` section per example directory - the 5 new LOU-H10 examples (doc-qa, research- assistant, slack-notifier, support-bot, workflow-router) plus the pre-existing tracing/ (LOU-E6) - each with a one-line description. Deviation: OpenRouterProvider.examples.ts (LOU-B6) is a FILE directly under examples/, not a directory (the ticket's phrasing assumed it had been "relocated" into its own directory; it wasn't). It's called out in a short note at the bottom of the README instead of a `## [dirname]` heading, since the verification is specifically a directory-listing diff and a heading for a non-directory would break that invariant. Verified via examples/README.test.ts: extracts every `## [x](./x)` heading's path and diffs it against the actual directory listing under examples/, asserting an exact match (no stale, no missing). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AgentExecutor.execute() gained validateExecuteOptions() - since
AgentExecutor is a static, instance-free API, execute() is the one entry
point where a missing required option can be caught up front, before it
fails deep inside runAgentLoop() with a generic "Cannot read properties of
undefined" TypeError (e.g. `provider.generate(...)` when `provider` was
never checked). Guards `provider`, `agent`, and `input` (all three are
non-optional on ExecuteOptions but were previously undereferenced until
used), each throwing a message naming the missing field plus a corrective
one-line snippet.
AgentBuilder's existing validateAgentConfig() (src/agent-types/validators.ts)
already guarded name/agentType with clear messages ("Agent name is
required" / "Agent type is required") - this commit adds a corrective
one-line snippet to each, without changing the existing message text
(both LOU-H12's new test and the pre-existing AgentBuilder.test.ts
assertions use substring matching, so nothing else needed updating).
Searched both files for other required-field dereferences lacking a
guard; none found beyond the above (tools/flows/settings/etc. are all
genuinely optional with safe defaults already in place).
Tested: omitting provider/agent/input from AgentExecutor.execute() each
throws naming that field with "Example: ..." corrective text; building an
agent with AgentBuilder.create().build() (no name, no type) throws both
"Agent name is required. Example: ..." and "Agent type is required.
Example: ...".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…s work outside the test suite
QA found that `node bin/loushy.js dev <config-with-mock-provider>` failed
with "Provider 'mock' not found" - LLMProviderRegistry only ever learned
about 'mock' inside test files (beforeAll/beforeEach), never in any
production code path.
- src/providers/index.ts: register 'mock' -> MockLLMProvider alongside the
real providers, using the same LLMProviderRegistry.register() call
signature already used for openai/ollama/openrouter/anthropic.
- src/providers/mock.ts: also self-register 'mock' at module load. This is
the registration that actually matters at runtime: mock.ts has zero
external dependencies, so it's always safe to bundle eagerly, unlike
providers/index.ts's barrel, which also statically imports
OpenAIProvider/OllamaProvider/OpenRouterProvider - each of which imports
its optional peer-dependency SDK ('@ai-sdk/openai', 'ollama-ai-provider',
...) at module scope. Forcing the whole barrel to survive bundling (tried
first) crashed `loushy dev` with "Cannot find module '@ai-sdk/openai'"
for users who never installed it, defeating the zero-API-key DX goal.
Re-registering 'mock' is safe either way: LLMProviderRegistry.register()
is a plain Map.set(), so it never throws on re-registration.
- src/spec/specToAgent.ts: side-effect import '../providers/mock' (not the
whole barrel) so CLI bundles like dist/cli/dev.js - whose dependency
graph otherwise only ever touches provider *types*, never providers as
values - actually run this registration.
- package.json: scope "sideEffects" to src/providers/mock.ts so esbuild
(tsup) doesn't tree-shake the registration call out of dist builds; all
other modules stay tree-shakeable as before.
Verified: `node bin/loushy.js dev <mock-config>` now boots for real and
correctly serves /health and /chat. npx vitest run / npx tsc --noEmit pass
with the same pre-existing baseline in both the root package (641
passed/2 skipped, 3 known pre-existing provider-suite failures) and
packages/create-loushy-agent (8/8). LOU-H10 starter templates
(examples/support-bot etc.) still run correctly via tsx.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
server.listen(port) with no host argument bound to 0.0.0.0 (all interfaces), making the local dev tool reachable over the network by default. Default startDevServer's host param to 127.0.0.1 and wire a matching --host CLI flag (following bin/loushy.js's existing manual flag-parsing pattern) for developers who want LAN access, e.g. testing from a phone. Adds a test asserting the server binds to 127.0.0.1 by default. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
readBody() accumulated request body chunks indefinitely with no size limit; combined with the dev server being network-reachable, an oversized POST could exhaust memory on a long-running dev server. Cap accumulation at 1MB (common Node.js body-size-limit convention): once exceeded, stop growing the buffered body (still draining the stream to avoid stalling the connection) and respond 413 Payload Too Large. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
execFileSync's shell: true already shell-quotes each array argument on Windows, so manually wrapping dir in literal quotes double-quoted the path. Pass dir unwrapped and let shell: true handle quoting. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This was referenced Oct 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements all 12 tickets from LOU-H (Phase 2-3), collapsing Loushy's DX from three hand-wired objects to a single CLI command:
createAgent({prompt, provider, tools?})one-liner, additive to the existingAgentBuilder/AgentExecutorpath.create-loushy-agentCLI package — interactive prompts (name/provider/tools), non-interactive flag shortcuts, real project scaffolding (package.json/tsconfig.json/agent entry file, Loushy pinned via a packed tarball since the SDK isn't published), and provider-specific.env.examplegeneration.loushy dev— a local HTTP server wrappingAgentExecutorwith/healthand/chat, a minimal no-reload chat UI, and hot-reload on config-file changes without restarting the server or dropping connections.AgentSpecformat (zod-validated) as an alternative to the TypeScript builder chain —loushy devnow loads configs through this format.examples/directory (including pre-existing ones from LOU-B6/LOU-E6).createAgent()/AgentExecutor.execute()/AgentBuildernow throw actionable errors naming the exact missing field and a corrective code snippet instead of generic "Cannot read property of undefined" failures.A real gap found and fixed
QA didn't stop at reading the source — it ran the actual
loushy devbinary against a mock-provider config and hitProvider 'mock' not found, even though every unit test passed (because each test file registered its own mock provider locally). Root cause went deeper than expected:package.json'ssideEffects: falseplus nothing in theloushy devcode path importing the providers module as a value (only as TypeScript types) meant tsup/esbuild tree-shook the registration entirely out of the shipped CLI bundle — and naively forcing the whole barrel to survive bundling would have pulled inOpenAIProvider/OllamaProvider, which statically import their optional peer SDKs and crash for anyone who hasn't installed them, defeating the zero-API-key goal.Fixed by making
src/providers/mock.ts(which has zero external dependencies) self-register at module load, scopingsideEffectsto just that file, and adding a targeted side-effect import inspecToAgent.ts— so the mock provider survives bundling without dragging in the real, dependency-heavy providers. Verified with a real CLI invocation:node bin/loushy.js dev <mock-config> --port 3941now genuinely serves/healthand returns realAgentExecutoroutput from/chat.Validation results
npx vitest run→ 641 passing, 2 skipped. 3 known pre-existing provider-suite failures (missing optional peer deps) — unrelated to this epic.npx tsc --noEmit: clean.packages/create-loushy-agent: 8/8 tests passing (including a real ~15snpm install && npm run buildend-to-end integration test on a freshly scaffolded project in a disposable temp directory),tsc --noEmitclean.--providerflags produces the correct.env.examplevar name for each (includingOLLAMA_BASE_URL, corrected from the ticket's literalOLLAMA_HOSTassumption to match LOU-F8's real convention).🤖 Generated with Claude Code