weifuwu 0.19.10 → 0.22.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.
@@ -3,6 +3,4 @@ import { app } from './app.ts'
3
3
 
4
4
  loadEnv()
5
5
  const port = Number(process.env.PORT) || 3000
6
- const server = serve(app.handler(), { port, websocket: app.websocketHandler() })
7
- await server.ready
8
- console.log(`Listening on http://localhost:${server.port}`)
6
+ serve(app.handler(), { port, websocket: app.websocketHandler() })
package/cli.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { mkdir, writeFile, copyFile, readFile, cp } from 'node:fs/promises'
3
- import { existsSync } from 'node:fs'
3
+ import { existsSync, readdirSync } from 'node:fs'
4
4
  import { execSync } from 'node:child_process'
5
5
  import { homedir } from 'node:os'
6
6
  import { join, dirname, resolve } from 'node:path'
@@ -11,6 +11,12 @@ const __dirname = dirname(__filename)
11
11
 
12
12
  const pkgRoot = existsSync(join(__dirname, 'package.json')) ? __dirname : resolve(__dirname, '..')
13
13
 
14
+ async function readPkg(): Promise<any> {
15
+ return JSON.parse(await readFile(join(pkgRoot, 'package.json'), 'utf-8'))
16
+ }
17
+
18
+ // ── Commands ────────────────────────────────────────────────────────────
19
+
14
20
  async function cmdSkill() {
15
21
  const targetDir = join(homedir(), '.agents', 'skills', 'weifuwu')
16
22
  await mkdir(targetDir, { recursive: true })
@@ -19,24 +25,25 @@ async function cmdSkill() {
19
25
  }
20
26
 
21
27
  async function cmdVersion() {
22
- const pkg = JSON.parse(await readFile(join(pkgRoot, 'package.json'), 'utf-8'))
28
+ const pkg = await readPkg()
23
29
  console.log(pkg.version)
24
30
  }
25
31
 
26
- async function cmdInit(name: string) {
32
+ async function cmdInit(name: string, opts: { minimal?: boolean; skipInstall?: boolean }) {
27
33
  const targetDir = resolve(process.cwd(), name)
28
- const pkg = JSON.parse(await readFile(join(pkgRoot, 'package.json'), 'utf-8'))
34
+ const pkg = await readPkg()
29
35
  const v = pkg.version
30
- const depVer = (depName: string) => `^${pkg.devDependencies[depName].replace(/^\^/, '')}`
36
+ const depVer = (depName: string) =>
37
+ `^${(pkg.devDependencies?.[depName] || '0.0.0').replace(/^\^/, '')}`
31
38
 
32
39
  await mkdir(targetDir, { recursive: true })
33
40
 
34
- // Copy code templates
41
+ // Copy templates
35
42
  const templateDir = join(pkgRoot, 'cli', 'template')
36
43
  await cp(templateDir, targetDir, { recursive: true })
37
44
 
38
- // Rewrite local imports → package imports for the copied project
39
- for (const file of ['app.ts', 'index.ts', 'ui/app/page.tsx']) {
45
+ // Rewrite local imports → package imports
46
+ for (const file of ['app.ts', 'index.ts']) {
40
47
  const fp = join(targetDir, file)
41
48
  let content = await readFile(fp, 'utf-8')
42
49
  content = content
@@ -45,23 +52,64 @@ async function cmdInit(name: string) {
45
52
  await writeFile(fp, content)
46
53
  }
47
54
 
48
- // Write config files
55
+ // Rewrite UI imports
56
+ const uiPage = join(targetDir, 'ui', 'app', 'page.tsx')
57
+ if (existsSync(uiPage)) {
58
+ let content = await readFile(uiPage, 'utf-8')
59
+ content = content
60
+ .replace(/from '\.\.\/\.\.\/\.\.\/\.\.\/react\.ts'/g, "from 'weifuwu/react'")
61
+ await writeFile(uiPage, content)
62
+ }
63
+
64
+ // Minimal mode: strip SSR/i18n/theme, keep only HTTP core
65
+ if (opts.minimal) {
66
+ try { await rmrf(join(targetDir, 'ui')) } catch {}
67
+ try { await rmrf(join(targetDir, 'locales')) } catch {}
68
+
69
+ // Write minimal app.ts
70
+ await writeFile(join(targetDir, 'app.ts'), [
71
+ `import { Router } from 'weifuwu'`,
72
+ ``,
73
+ `export const app = new Router()`,
74
+ ``,
75
+ `app.get('/', () => new Response('Hello from ${name}!'))`,
76
+ `app.get('/api/ping', () => Response.json({ pong: true, time: new Date().toISOString() }))`,
77
+ ``,
78
+ ].join('\n'))
79
+
80
+ // Write minimal index.ts
81
+ await writeFile(join(targetDir, 'index.ts'), [
82
+ `import { serve } from 'weifuwu'`,
83
+ `import { app } from './app.ts'`,
84
+ ``,
85
+ `const port = Number(process.env.PORT) || 3000`,
86
+ `serve(app.handler(), { port })`,
87
+ ``,
88
+ ].join('\n'))
89
+ }
90
+
91
+ // Write package.json
92
+ const deps: Record<string, string> = { weifuwu: `^${v}` }
93
+ const devDeps: Record<string, string> = {}
94
+ if (!opts.minimal) {
95
+ devDeps['@types/react'] = depVer('@types/react')
96
+ devDeps['@types/react-dom'] = depVer('@types/react-dom')
97
+ }
98
+ devDeps['@types/node'] = depVer('@types/node')
99
+
49
100
  await writeFile(join(targetDir, 'package.json'), JSON.stringify({
50
101
  name,
51
102
  type: 'module',
52
103
  scripts: {
53
- dev: 'NODE_ENV=development node index.ts',
104
+ dev: 'NODE_ENV=development node --watch index.ts',
54
105
  start: 'node index.ts',
55
106
  },
56
- dependencies: {
57
- weifuwu: `^${v}`,
58
- },
59
- devDependencies: {
60
- '@types/node': depVer('@types/node'),
61
- '@types/react': depVer('@types/react'),
62
- },
107
+ dependencies: deps,
108
+ devDependencies: devDeps,
63
109
  }, null, 2) + '\n')
64
110
 
111
+ // Write tsconfig.json
112
+ const include = opts.minimal ? ['*.ts'] : ['*.ts', 'ui/**/*.ts', 'ui/**/*.tsx']
65
113
  await writeFile(join(targetDir, 'tsconfig.json'), JSON.stringify({
66
114
  compilerOptions: {
67
115
  target: 'ESNext',
@@ -73,52 +121,182 @@ async function cmdInit(name: string) {
73
121
  noEmit: true,
74
122
  allowImportingTsExtensions: true,
75
123
  },
76
- include: ['*.ts', 'ui/**/*.ts', 'ui/**/*.tsx'],
124
+ include,
77
125
  }, null, 2) + '\n')
78
126
 
79
127
  await writeFile(join(targetDir, '.gitignore'), 'node_modules\ndist\n.env\n.sessions\n.weifuwu\n')
80
128
  await writeFile(join(targetDir, '.env'), 'PORT=3000\n')
81
-
82
129
  await writeFile(join(targetDir, 'AGENTS.md'), [
83
130
  `# ${name}`,
84
131
  '',
85
- 'This is a [weifuwu](https://weifuwu.io) HTTP application — pure Node.js, no build step.',
86
- '',
87
- '## Before you start',
88
- '',
89
- 'Read `node_modules/weifuwu/README.md` first.',
132
+ `This is a [weifuwu](https://weifuwu.io) application — pure Node.js, no build step.`,
90
133
  '',
91
134
  '## Commands',
92
135
  '',
93
- '- `npm run dev` — start dev server with `--watch`',
136
+ '- `npm run dev` — start dev server with hot reload',
94
137
  '- `npm start` — start production server',
95
138
  '- `npm install` — install dependencies',
96
- '- `npx tsc --noEmit` — type-check without emitting',
139
+ '- `npx tsc --noEmit` — type-check',
97
140
  '',
98
141
  '## API Reference',
99
142
  '',
100
- 'See `node_modules/weifuwu/README.md` for the full weifuwu API documentation.',
143
+ 'See `node_modules/weifuwu/README.md` for the full documentation.',
101
144
  '',
102
145
  ].join('\n'))
103
146
 
104
- console.log('\nInstalling dependencies...')
105
- execSync('npm install', { cwd: targetDir, stdio: 'inherit' })
106
- console.log(`\n✅ Created ${name}/ cd ${name} && npm start`)
147
+ if (!opts.skipInstall) {
148
+ console.log('\nInstalling dependencies...')
149
+ execSync('npm install', { cwd: targetDir, stdio: 'inherit' })
150
+ }
151
+ console.log(`\n✅ Created ${name}/ — cd ${name} && ${opts.skipInstall ? 'npm install && ' : ''}npm run dev`)
152
+ }
153
+
154
+ async function cmdDev() {
155
+ const entry = existsSync('index.ts') ? 'index.ts'
156
+ : existsSync('app.ts') ? 'app.ts'
157
+ : null
158
+
159
+ if (!entry) {
160
+ console.error('No index.ts or app.ts found in current directory.')
161
+ console.error('Run `npx weifuwu init <name>` to create a new project.')
162
+ process.exit(1)
163
+ }
164
+
165
+ console.log(`Starting dev server (${entry})...`)
166
+ execSync(`NODE_ENV=development node --watch ${entry}`, { stdio: 'inherit' })
107
167
  }
108
168
 
169
+ async function cmdGenerate(type: string, name: string) {
170
+ if (type !== 'module') {
171
+ console.error(`Unknown generate type: ${type}. Usage: npx weifuwu generate module <name>`)
172
+ process.exit(1)
173
+ }
174
+
175
+ const dir = join(process.cwd(), name)
176
+ if (existsSync(dir)) {
177
+ console.error(`Directory ${name} already exists.`)
178
+ process.exit(1)
179
+ }
180
+
181
+ await mkdir(dir, { recursive: true })
182
+ await writeFile(join(dir, 'index.ts'), [
183
+ `import type { Middleware } from 'weifuwu'`,
184
+ ``,
185
+ `export interface ${capitalize(name)}Options {`,
186
+ ` // Add your options here`,
187
+ `}`,
188
+ ``,
189
+ `export function ${name}(opts?: ${capitalize(name)}Options): Middleware {`,
190
+ ` return async (req, ctx, next) => {`,
191
+ ` // Your middleware logic here`,
192
+ ` return next(req, ctx)`,
193
+ ` }`,
194
+ `}`,
195
+ ``,
196
+ ].join('\n'))
197
+
198
+ await mkdir(join(dir, '..', 'test'), { recursive: true })
199
+ await writeFile(join(dir, '..', 'test', `${name}.test.ts`), [
200
+ `import { describe, it } from 'node:test'`,
201
+ `import assert from 'node:assert/strict'`,
202
+ `import { ${name} } from '../${name}/index.ts'`,
203
+ `import { Router } from 'weifuwu'`,
204
+ ``,
205
+ `describe('${name}', () => {`,
206
+ ` it('works as middleware', async () => {`,
207
+ ` const app = new Router()`,
208
+ ` app.use(${name}())`,
209
+ ` app.get('/', () => new Response('ok'))`,
210
+ ``,
211
+ ` const res = await app.handler()(`,
212
+ ` new Request('http://localhost/'),`,
213
+ ` { params: {}, query: {} } as any,`,
214
+ ` )`,
215
+ ` assert.equal(res.status, 200)`,
216
+ ` })`,
217
+ `})`,
218
+ ``,
219
+ ].join('\n'))
220
+
221
+ console.log(`✅ Created module ${name}/ with index.ts and test/${name}.test.ts`)
222
+ }
223
+
224
+ function capitalize(s: string): string {
225
+ return s.charAt(0).toUpperCase() + s.slice(1)
226
+ }
227
+
228
+ // Minimal rm -rf
229
+ async function rmrf(dir: string) {
230
+ try {
231
+ const entries = readdirSync(dir, { withFileTypes: true })
232
+ for (const entry of entries) {
233
+ const p = join(dir, entry.name)
234
+ if (entry.isDirectory()) {
235
+ await rmrf(p)
236
+ } else {
237
+ const { unlink } = await import('node:fs/promises')
238
+ await unlink(p)
239
+ }
240
+ }
241
+ const { rmdir } = await import('node:fs/promises')
242
+ await rmdir(dir)
243
+ } catch { /* ignore */ }
244
+ }
245
+
246
+ import { parseArgs } from 'node:util'
247
+
248
+ // ── CLI dispatcher ──────────────────────────────────────────────────────
249
+
109
250
  const cmd = process.argv[2]
110
251
 
252
+ const HELP = `
253
+ weifuwu — Web-standard HTTP framework for Node.js
254
+
255
+ Usage:
256
+ npx weifuwu init <name> Create a new project (SSR + i18n + theme)
257
+ npx weifuwu init <name> --minimal Create a minimal HTTP project
258
+ npx weifuwu init <name> --skip-install Create project, skip npm install
259
+ npx weifuwu dev Start dev server in current directory
260
+ npx weifuwu generate module <name> Scaffold a new module
261
+ npx weifuwu version Print version
262
+ `
263
+
111
264
  if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
112
265
  cmdVersion().catch(console.error)
113
266
  } else if (cmd === 'skill') {
114
267
  cmdSkill().catch(console.error)
115
268
  } else if (cmd === 'init') {
116
- const name = process.argv[3]
269
+ const { values, positionals } = parseArgs({
270
+ args: process.argv.slice(3),
271
+ options: {
272
+ minimal: { type: 'boolean', short: 'm' },
273
+ 'skip-install': { type: 'boolean' },
274
+ },
275
+ strict: false,
276
+ allowPositionals: true,
277
+ })
278
+ const name = positionals[0]
117
279
  if (!name) {
118
- console.error('Usage: npx weifuwu init <name>')
280
+ console.error('Usage: npx weifuwu init <name> [--minimal] [--skip-install]')
281
+ process.exit(1)
282
+ }
283
+ cmdInit(name, { minimal: !!values.minimal, skipInstall: !!values['skip-install'] }).catch(console.error)
284
+ } else if (cmd === 'dev') {
285
+ cmdDev()
286
+ } else if (cmd === 'generate' || cmd === 'g') {
287
+ const { positionals } = parseArgs({
288
+ args: process.argv.slice(3),
289
+ options: {},
290
+ strict: false,
291
+ allowPositionals: true,
292
+ })
293
+ const type = positionals[0]
294
+ const name = positionals[1]
295
+ if (!type || !name) {
296
+ console.error('Usage: npx weifuwu generate module <name>')
119
297
  process.exit(1)
120
298
  }
121
- cmdInit(name).catch(console.error)
299
+ cmdGenerate(type, name).catch(console.error)
122
300
  } else {
123
- console.log('\nUsage:\n npx weifuwu version Print version\n npx weifuwu init <name> Create a new weifuwu project\n npx weifuwu skill Install weifuwu skill to ~/.agents/skills/\n')
301
+ console.log(HELP)
124
302
  }
@@ -3,6 +3,7 @@ import type { BoundTable } from '../postgres/schema/index.ts';
3
3
  import type { RunParams } from './types.ts';
4
4
  interface RestDeps {
5
5
  agents: BoundTable<any>;
6
+ runs: BoundTable<any>;
6
7
  knowledge: BoundTable<any>;
7
8
  runner: {
8
9
  run: (agentId: number, params: RunParams) => Promise<any>;
@@ -5,6 +5,7 @@ import type { RunParams, RunResult, KnowledgeDoc } from './types.ts';
5
5
  interface RunnerDeps {
6
6
  sql: Sql<{}>;
7
7
  agents: BoundTable<any>;
8
+ runs: BoundTable<any>;
8
9
  knowledge: BoundTable<any>;
9
10
  getModel: () => LanguageModel;
10
11
  getEmbeddingModel: () => EmbeddingModel;
@@ -0,0 +1,74 @@
1
+ import type { Context, Middleware } from './types.ts';
2
+ import type { Redis } from './vendor.ts';
3
+ export interface CachedResponse {
4
+ status: number;
5
+ statusText: string;
6
+ headers: Record<string, string>;
7
+ body: string;
8
+ createdAt: number;
9
+ tags: string[];
10
+ }
11
+ export interface CacheStore {
12
+ get(key: string): Promise<CachedResponse | null>;
13
+ set(key: string, entry: CachedResponse, ttl: number): Promise<void>;
14
+ delete(key: string): Promise<void>;
15
+ invalidate(tag: string): Promise<void>;
16
+ flush(): Promise<void>;
17
+ }
18
+ export interface CacheOptions {
19
+ /** TTL in milliseconds. Default: 300_000 (5 min). */
20
+ ttl?: number;
21
+ /** Cache store. 'memory' (default) or 'redis'. */
22
+ store?: 'memory' | 'redis' | CacheStore;
23
+ /** Redis client (required when store: 'redis'). */
24
+ redis?: Redis;
25
+ /** Custom cache key function. Default: SHA256 of method + URL. */
26
+ key?: (req: Request) => string;
27
+ /** Tag function for grouped invalidation. Called after handler runs (ctx available). */
28
+ tag?: (req: Request, ctx: Context) => string | string[] | undefined;
29
+ /** Whether to cache responses with Set-Cookie. Default: false. */
30
+ cacheCookies?: boolean;
31
+ /** Status codes to cache. Default: [200]. */
32
+ cacheStatus?: number[];
33
+ /** Maximum number of bytes per cached body. Default: 1MB. Larger bodies are skipped. */
34
+ maxBodySize?: number;
35
+ }
36
+ export interface CacheMiddleware extends Middleware {
37
+ /** Invalidate all entries with a given tag. */
38
+ invalidate(tag: string): Promise<void>;
39
+ /** Flush all cached entries. */
40
+ flush(): Promise<void>;
41
+ /** Cleanup. */
42
+ close(): void;
43
+ /** Store reference (for testing). */
44
+ store: CacheStore;
45
+ }
46
+ export declare class MemoryCache implements CacheStore {
47
+ private store;
48
+ private tagIndex;
49
+ private interval;
50
+ constructor(cleanupMs?: number);
51
+ get(key: string): Promise<CachedResponse | null>;
52
+ set(key: string, data: CachedResponse, ttl: number): Promise<void>;
53
+ delete(key: string): Promise<void>;
54
+ invalidate(tag: string): Promise<void>;
55
+ flush(): Promise<void>;
56
+ private cleanup;
57
+ close(): void;
58
+ /** Testing only. */
59
+ get size(): number;
60
+ }
61
+ export declare class RedisCache implements CacheStore {
62
+ private redis;
63
+ private prefix;
64
+ private tagPrefix;
65
+ constructor(redis: Redis, prefix?: string);
66
+ private key;
67
+ private tagKey;
68
+ get(key: string): Promise<CachedResponse | null>;
69
+ set(key: string, entry: CachedResponse, ttl: number): Promise<void>;
70
+ delete(key: string): Promise<void>;
71
+ invalidate(tag: string): Promise<void>;
72
+ flush(): Promise<void>;
73
+ }
74
+ export declare function cache(options?: CacheOptions): CacheMiddleware;