Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Publish

# Publishing is driven by a version tag, so the released artifact is always
# traceable to a commit. `npm version` creates the tag; pushing it ships.
on:
push:
tags: ['v*']

jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
# Required for npm provenance — proves on the registry that this tarball
# was built by this workflow from this commit.
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: 'https://registry.npmjs.org'

- run: npm ci --ignore-scripts

# Never publish something that would not have passed CI.
- run: npm run verify

# Refuse to publish a tag whose version does not match package.json,
# rather than silently shipping the wrong number.
- name: Check tag matches package version
run: |
tag="${GITHUB_REF_NAME#v}"
pkg=$(node -p "require('./package.json').version")
if [ "$tag" != "$pkg" ]; then
echo "Tag v$tag does not match package.json version $pkg" >&2
exit 1
fi

- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Mao Nakamoto

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
59 changes: 43 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
# @fleet/ai-forms
# ai-forms

Headless AI form filling and conversational refinement. One implementation, shared by every app in the fleet.
[![npm](https://img.shields.io/npm/v/ai-forms.svg)](https://www.npmjs.com/package/ai-forms)
[![CI](https://github.com/maonakamoto/ai-forms/actions/workflows/ci.yml/badge.svg)](https://github.com/maonakamoto/ai-forms/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

Forms are the worst part of most software. This package makes every form in every project answer to plain language — fill it from a description, then keep talking to it ("shorter", "move the date to next Friday", "actually call it something else") until it is right.
Headless AI form filling and conversational refinement.

Forms are the worst part of most software. This package makes a form answer to plain language — fill it from a description, then keep talking to it ("shorter", "move the date to next Friday", "actually call it something else") until it is right.

```
npm install github:maonakamoto/ai-forms#v0.1.0
npm install ai-forms
```

No registry auth. Versioned by git tag. `npm ci` builds it from source on install.
No provider SDK, no markup, no styles. You pass in a function that calls whatever model you already use; the package handles prompting, parsing, sanitising, and the merge rules that decide who wins when the model and the user disagree.

Works with any model (OpenAI, Anthropic, Groq, Gemini, local — anything you can wrap in `(prompt) => Promise<string>`), any framework on the server, and React on the client. The React hook is optional; the core is framework-free.

---

## The standard
## What "done" looks like

An app meets the standard when all five hold:
Five properties, all of which this package holds:

1. **Every form can be filled from prose.** The user types what they want in one box; the form fills in.
2. **Every filled form can be changed by talking to it.** Follow-up instructions apply to what is already there. This is the part almost everyone gets wrong — see "Why refinement silently fails" below.
Expand Down Expand Up @@ -45,7 +51,7 @@ The same mistake has a sibling: a single minimum input length. A fill descriptio

```ts
// src/config/ai-forms.ts
import { defineFields, type FormTarget } from '@fleet/ai-forms';
import { defineFields, type FormTarget } from 'ai-forms';

export const GOAL_FORM: FormTarget = {
key: 'goal',
Expand All @@ -68,22 +74,43 @@ export const AI_FORMS = [GOAL_FORM /* , ... */];

```ts
// src/app/api/ai/form-assist/route.ts
import { createFormAssistHandler } from '@fleet/ai-forms/server';
import { createFormAssistHandler } from 'ai-forms/server';
import { AI_FORMS } from '@/config/ai-forms';
import { callGroqText } from '@/lib/groq';
import { getCurrentUserId } from '@/lib/session';

export const POST = createFormAssistHandler({
targets: AI_FORMS,
authorize: async () => (await getCurrentUserId())
? { ok: true }
: { ok: false, status: 401, error: 'Sign in to use the assistant.' },
complete: ({ system, prompt, maxTokens, temperature }) =>
callGroqText(prompt, { systemPrompt: system, maxTokens, temperature }),

// Any provider. `complete` just has to return the model's text.
complete: async ({ system, prompt, maxTokens, temperature }) => {
const res = await fetch(`${process.env.LLM_BASE_URL}/chat/completions`, {
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${process.env.LLM_API_KEY}`,
},
body: JSON.stringify({
model: process.env.LLM_MODEL,
max_tokens: maxTokens,
temperature,
messages: [
{ role: 'system', content: system },
{ role: 'user', content: prompt },
],
}),
});
const json = await res.json();
return json.choices[0].message.content;
},
});
```

The package never owns API keys, model choice, budgets, or fallback policy — the app passes its own caller. Field specs live on the server, so a client can never widen the set of fields the model may write.
The package never owns API keys, model choice, budgets, or fallback policy — the app passes its own caller. That is deliberate: you already have retry, spend caps, and a fallback chain, and a form library has no business owning any of them.

Field specs live on the server, so a client can never widen the set of fields the model may write.

### 3. Use the hook in the form

Expand All @@ -110,7 +137,7 @@ Rendering is yours. The package ships no markup and no classes — each app has

## API

### `@fleet/ai-forms`
### `ai-forms`

| Export | Purpose |
| --- | --- |
Expand All @@ -121,11 +148,11 @@ Rendering is yours. The package ships no markup and no classes — each app has
| `parseAssistResponse(text)` | Extract JSON from fenced or prose-wrapped completions |
| `MIN_INSTRUCTION_LENGTH` | Per-intent input floors |

### `@fleet/ai-forms/server`
### `ai-forms/server`

`createFormAssistHandler(config)` → `(Request) => Promise<Response>`. Web-standard, so it drops straight into a Next.js App Router route. `authorize` runs before any model call.

### `@fleet/ai-forms/react`
### `ai-forms/react`

`useAiForm(options)` → values, `setValue`, `ask` / `fill` / `refine`, `busy`, `error`, `transcript`, `changed`, `isAiTouched`, `undo`, `canUndo`, `reset`.

Expand Down
7 changes: 5 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 24 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,36 @@
{
"name": "@fleet/ai-forms",
"name": "ai-forms",
"version": "0.1.0",
"description": "Headless AI form filling and conversational refinement. Provider-agnostic core, React hook, and a server route factory.",
"license": "MIT",
"author": "Mao Nakamoto",
"homepage": "https://github.com/maonakamoto/ai-forms#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/maonakamoto/ai-forms.git"
},
"bugs": {
"url": "https://github.com/maonakamoto/ai-forms/issues"
},
"keywords": [
"ai",
"forms",
"form-fill",
"autofill",
"llm",
"react",
"headless",
"natural-language"
],
"type": "module",
"sideEffects": false,
"engines": {
"node": ">=18"
},
"publishConfig": {
"access": "public",
"provenance": true
},
"files": [
"dist"
],
Expand Down
Loading