weifuwu 0.12.0 → 0.13.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 +95 -1427
- package/cli.ts +88 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +75 -0
- package/dist/env.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +86 -57
- package/docs/agent.md +44 -0
- package/docs/ai.md +93 -0
- package/docs/extra.md +67 -0
- package/docs/graphql.md +61 -0
- package/docs/messager.md +48 -0
- package/docs/middleware.md +131 -0
- package/docs/opencode.md +252 -0
- package/docs/postgres.md +162 -0
- package/docs/router.md +80 -0
- package/docs/tenant.md +174 -0
- package/docs/tsx.md +199 -0
- package/docs/user.md +167 -0
- package/package.json +7 -2
package/cli.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, writeFile, copyFile, readdir } from 'node:fs/promises'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { join, dirname, resolve } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
8
|
+
const __dirname = dirname(__filename)
|
|
9
|
+
|
|
10
|
+
const pkgRoot = resolve(__dirname, '..')
|
|
11
|
+
|
|
12
|
+
async function cmdSkill() {
|
|
13
|
+
const targetDir = join(homedir(), '.agents', 'skills', 'weifuwu')
|
|
14
|
+
const docsTarget = join(targetDir, 'docs')
|
|
15
|
+
|
|
16
|
+
await mkdir(docsTarget, { recursive: true })
|
|
17
|
+
await copyFile(join(pkgRoot, 'README.md'), join(targetDir, 'SKILL.md'))
|
|
18
|
+
|
|
19
|
+
const docDir = join(pkgRoot, 'docs')
|
|
20
|
+
const entries = await readdir(docDir)
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
if (entry.endsWith('.md')) {
|
|
23
|
+
await copyFile(join(docDir, entry), join(docsTarget, entry))
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log('✅ Installed weifuwu skill to ~/.agents/skills/weifuwu/')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function cmdInit(name: string) {
|
|
31
|
+
const targetDir = resolve(process.cwd(), name)
|
|
32
|
+
await mkdir(targetDir, { recursive: true })
|
|
33
|
+
|
|
34
|
+
await writeFile(join(targetDir, 'package.json'), JSON.stringify({
|
|
35
|
+
name,
|
|
36
|
+
type: 'module',
|
|
37
|
+
scripts: {
|
|
38
|
+
dev: 'node --watch app.ts',
|
|
39
|
+
start: 'node app.ts',
|
|
40
|
+
},
|
|
41
|
+
dependencies: {
|
|
42
|
+
weifuwu: 'latest',
|
|
43
|
+
},
|
|
44
|
+
}, null, 2) + '\n')
|
|
45
|
+
|
|
46
|
+
await writeFile(join(targetDir, 'tsconfig.json'), JSON.stringify({
|
|
47
|
+
compilerOptions: {
|
|
48
|
+
target: 'ESNext',
|
|
49
|
+
module: 'NodeNext',
|
|
50
|
+
moduleResolution: 'NodeNext',
|
|
51
|
+
strict: true,
|
|
52
|
+
jsx: 'react-jsx',
|
|
53
|
+
skipLibCheck: true,
|
|
54
|
+
},
|
|
55
|
+
include: ['*.ts'],
|
|
56
|
+
}, null, 2) + '\n')
|
|
57
|
+
|
|
58
|
+
await writeFile(join(targetDir, '.gitignore'), 'node_modules\ndist\n.env\n.sessions\n')
|
|
59
|
+
|
|
60
|
+
await writeFile(join(targetDir, '.env'), 'PORT=3000\n')
|
|
61
|
+
|
|
62
|
+
await writeFile(join(targetDir, 'app.ts'), [
|
|
63
|
+
"import { serve, loadEnv } from 'weifuwu'",
|
|
64
|
+
'',
|
|
65
|
+
"loadEnv()",
|
|
66
|
+
"const port = Number(process.env.PORT) || 3000",
|
|
67
|
+
'',
|
|
68
|
+
"serve((req, ctx) => new Response('Hello, Weifuwu!'), { port })",
|
|
69
|
+
'',
|
|
70
|
+
].join('\n'))
|
|
71
|
+
|
|
72
|
+
console.log(`✅ Created ${name}/ — cd ${name} && npm install && npm run dev`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const cmd = process.argv[2]
|
|
76
|
+
|
|
77
|
+
if (cmd === 'skill') {
|
|
78
|
+
cmdSkill().catch(console.error)
|
|
79
|
+
} else if (cmd === 'init') {
|
|
80
|
+
const name = process.argv[3]
|
|
81
|
+
if (!name) {
|
|
82
|
+
console.error('Usage: npx weifuwu init <name>')
|
|
83
|
+
process.exit(1)
|
|
84
|
+
}
|
|
85
|
+
cmdInit(name).catch(console.error)
|
|
86
|
+
} else {
|
|
87
|
+
console.log('\nUsage:\n npx weifuwu init <name> Create a new weifuwu project\n npx weifuwu skill Install weifuwu skill to ~/.agents/skills/\n')
|
|
88
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// cli.ts
|
|
4
|
+
import { mkdir, writeFile, copyFile, readdir } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { join, dirname, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
var __dirname = dirname(__filename);
|
|
10
|
+
var pkgRoot = resolve(__dirname, "..");
|
|
11
|
+
async function cmdSkill() {
|
|
12
|
+
const targetDir = join(homedir(), ".agents", "skills", "weifuwu");
|
|
13
|
+
const docsTarget = join(targetDir, "docs");
|
|
14
|
+
await mkdir(docsTarget, { recursive: true });
|
|
15
|
+
await copyFile(join(pkgRoot, "README.md"), join(targetDir, "SKILL.md"));
|
|
16
|
+
const docDir = join(pkgRoot, "docs");
|
|
17
|
+
const entries = await readdir(docDir);
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (entry.endsWith(".md")) {
|
|
20
|
+
await copyFile(join(docDir, entry), join(docsTarget, entry));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
console.log("\u2705 Installed weifuwu skill to ~/.agents/skills/weifuwu/");
|
|
24
|
+
}
|
|
25
|
+
async function cmdInit(name) {
|
|
26
|
+
const targetDir = resolve(process.cwd(), name);
|
|
27
|
+
await mkdir(targetDir, { recursive: true });
|
|
28
|
+
await writeFile(join(targetDir, "package.json"), JSON.stringify({
|
|
29
|
+
name,
|
|
30
|
+
type: "module",
|
|
31
|
+
scripts: {
|
|
32
|
+
dev: "node --watch app.ts",
|
|
33
|
+
start: "node app.ts"
|
|
34
|
+
},
|
|
35
|
+
dependencies: {
|
|
36
|
+
weifuwu: "latest"
|
|
37
|
+
}
|
|
38
|
+
}, null, 2) + "\n");
|
|
39
|
+
await writeFile(join(targetDir, "tsconfig.json"), JSON.stringify({
|
|
40
|
+
compilerOptions: {
|
|
41
|
+
target: "ESNext",
|
|
42
|
+
module: "NodeNext",
|
|
43
|
+
moduleResolution: "NodeNext",
|
|
44
|
+
strict: true,
|
|
45
|
+
jsx: "react-jsx",
|
|
46
|
+
skipLibCheck: true
|
|
47
|
+
},
|
|
48
|
+
include: ["*.ts"]
|
|
49
|
+
}, null, 2) + "\n");
|
|
50
|
+
await writeFile(join(targetDir, ".gitignore"), "node_modules\ndist\n.env\n.sessions\n");
|
|
51
|
+
await writeFile(join(targetDir, ".env"), "PORT=3000\n");
|
|
52
|
+
await writeFile(join(targetDir, "app.ts"), [
|
|
53
|
+
"import { serve, loadEnv } from 'weifuwu'",
|
|
54
|
+
"",
|
|
55
|
+
"loadEnv()",
|
|
56
|
+
"const port = Number(process.env.PORT) || 3000",
|
|
57
|
+
"",
|
|
58
|
+
"serve((req, ctx) => new Response('Hello, Weifuwu!'), { port })",
|
|
59
|
+
""
|
|
60
|
+
].join("\n"));
|
|
61
|
+
console.log(`\u2705 Created ${name}/ \u2014 cd ${name} && npm install && npm run dev`);
|
|
62
|
+
}
|
|
63
|
+
var cmd = process.argv[2];
|
|
64
|
+
if (cmd === "skill") {
|
|
65
|
+
cmdSkill().catch(console.error);
|
|
66
|
+
} else if (cmd === "init") {
|
|
67
|
+
const name = process.argv[3];
|
|
68
|
+
if (!name) {
|
|
69
|
+
console.error("Usage: npx weifuwu init <name>");
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
cmdInit(name).catch(console.error);
|
|
73
|
+
} else {
|
|
74
|
+
console.log("\nUsage:\n npx weifuwu init <name> Create a new weifuwu project\n npx weifuwu skill Install weifuwu skill to ~/.agents/skills/\n");
|
|
75
|
+
}
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function loadEnv(path?: string): void;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
// env.ts
|
|
2
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
function loadEnv(path2) {
|
|
5
|
+
const filePath = resolve(process.cwd(), path2 ?? ".env");
|
|
6
|
+
if (!existsSync(filePath)) return;
|
|
7
|
+
const content = readFileSync(filePath, "utf-8");
|
|
8
|
+
for (const line of content.split("\n")) {
|
|
9
|
+
const trimmed = line.trim();
|
|
10
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
11
|
+
const eqIdx = trimmed.indexOf("=");
|
|
12
|
+
if (eqIdx === -1) continue;
|
|
13
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
14
|
+
if (!key) continue;
|
|
15
|
+
if (process.env[key] !== void 0) continue;
|
|
16
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
17
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
18
|
+
value = value.slice(1, -1);
|
|
19
|
+
} else {
|
|
20
|
+
const commentIdx = value.indexOf(" #");
|
|
21
|
+
if (commentIdx !== -1) {
|
|
22
|
+
value = value.slice(0, commentIdx).trimEnd();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
process.env[key] = value;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
1
29
|
// serve.ts
|
|
2
30
|
import http from "node:http";
|
|
3
31
|
async function readBody(req, maxSize) {
|
|
@@ -334,13 +362,13 @@ var Router = class _Router {
|
|
|
334
362
|
const mw = match.middlewares[index++];
|
|
335
363
|
return mw(innerReq, ctx2, dispatch);
|
|
336
364
|
}
|
|
337
|
-
return await new Promise((
|
|
365
|
+
return await new Promise((resolve11) => {
|
|
338
366
|
try {
|
|
339
367
|
upgradeSocket(router.wss, req, socket, head, match.handler, ctx2);
|
|
340
|
-
|
|
368
|
+
resolve11(new Response(null, { status: 101 }));
|
|
341
369
|
} catch {
|
|
342
370
|
socket.destroy();
|
|
343
|
-
|
|
371
|
+
resolve11(new Response("WebSocket upgrade failed", { status: 500 }));
|
|
344
372
|
}
|
|
345
373
|
});
|
|
346
374
|
};
|
|
@@ -530,8 +558,8 @@ function sendHttpResponseOnSocket(socket, response) {
|
|
|
530
558
|
import { createElement } from "react";
|
|
531
559
|
import { renderToReadableStream } from "react-dom/server";
|
|
532
560
|
import * as esbuild from "esbuild";
|
|
533
|
-
import { readdirSync, statSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
534
|
-
import { join, relative, resolve, sep, dirname, basename } from "node:path";
|
|
561
|
+
import { readdirSync, statSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
562
|
+
import { join, relative, resolve as resolve2, sep, dirname, basename } from "node:path";
|
|
535
563
|
import { pathToFileURL } from "node:url";
|
|
536
564
|
import { createHash } from "node:crypto";
|
|
537
565
|
import vm from "node:vm";
|
|
@@ -577,17 +605,17 @@ function resolveAliases() {
|
|
|
577
605
|
if (_alias) return _alias;
|
|
578
606
|
const configFiles = ["tsconfig.json", "jsconfig.json"];
|
|
579
607
|
for (const file of configFiles) {
|
|
580
|
-
const p =
|
|
581
|
-
if (
|
|
608
|
+
const p = resolve2(file);
|
|
609
|
+
if (existsSync2(p)) {
|
|
582
610
|
try {
|
|
583
|
-
const config = JSON.parse(
|
|
611
|
+
const config = JSON.parse(readFileSync2(p, "utf-8"));
|
|
584
612
|
const paths = config.compilerOptions?.paths;
|
|
585
613
|
if (paths) {
|
|
586
614
|
const alias = {};
|
|
587
615
|
for (const [key, values] of Object.entries(paths)) {
|
|
588
616
|
const cleanKey = key.replace("/*", "");
|
|
589
617
|
const val = values[0]?.replace("/*", "");
|
|
590
|
-
if (val) alias[cleanKey] =
|
|
618
|
+
if (val) alias[cleanKey] = resolve2(dirname(p), val);
|
|
591
619
|
}
|
|
592
620
|
_alias = alias;
|
|
593
621
|
return alias;
|
|
@@ -639,9 +667,9 @@ function scanPages(dir) {
|
|
|
639
667
|
const pagePath = join(current, "page.tsx");
|
|
640
668
|
const tsPagePath = join(current, "page.ts");
|
|
641
669
|
let entryPath = "";
|
|
642
|
-
if (
|
|
670
|
+
if (existsSync2(pagePath)) {
|
|
643
671
|
entryPath = pagePath;
|
|
644
|
-
} else if (
|
|
672
|
+
} else if (existsSync2(tsPagePath)) {
|
|
645
673
|
entryPath = tsPagePath;
|
|
646
674
|
}
|
|
647
675
|
if (entryPath) {
|
|
@@ -650,8 +678,8 @@ function scanPages(dir) {
|
|
|
650
678
|
relPath = relPath.replace(/^page\.tsx?$/, "");
|
|
651
679
|
const route = filePathToRoute(relPath);
|
|
652
680
|
const layouts = resolveLayouts(current, dir);
|
|
653
|
-
const loadPath =
|
|
654
|
-
const rPath =
|
|
681
|
+
const loadPath = existsSync2(join(current, "load.ts")) ? join(current, "load.ts") : void 0;
|
|
682
|
+
const rPath = existsSync2(join(current, "route.ts")) ? join(current, "route.ts") : void 0;
|
|
655
683
|
pages.push({
|
|
656
684
|
route,
|
|
657
685
|
entryPath,
|
|
@@ -661,7 +689,7 @@ function scanPages(dir) {
|
|
|
661
689
|
});
|
|
662
690
|
} else {
|
|
663
691
|
const rPath = join(current, "route.ts");
|
|
664
|
-
if (
|
|
692
|
+
if (existsSync2(rPath)) {
|
|
665
693
|
let relPath = relative(dir, rPath).replace(sep, "/");
|
|
666
694
|
relPath = relPath.replace(/\/route\.tsx?$/, "");
|
|
667
695
|
const route = filePathToRoute(relPath);
|
|
@@ -690,7 +718,7 @@ function resolveLayouts(dir, pagesDir) {
|
|
|
690
718
|
let current = dir;
|
|
691
719
|
while (current.startsWith(pagesDir)) {
|
|
692
720
|
const p = join(current, "layout.tsx");
|
|
693
|
-
if (
|
|
721
|
+
if (existsSync2(p)) {
|
|
694
722
|
layouts.push(p);
|
|
695
723
|
}
|
|
696
724
|
const parent = dirname(current);
|
|
@@ -750,8 +778,8 @@ var TsxInstance = class {
|
|
|
750
778
|
clientBuildParams = /* @__PURE__ */ new Map();
|
|
751
779
|
clientRouteLog = /* @__PURE__ */ new Set();
|
|
752
780
|
constructor(options) {
|
|
753
|
-
this.uiDir =
|
|
754
|
-
this.pagesDir =
|
|
781
|
+
this.uiDir = resolve2(options.dir);
|
|
782
|
+
this.pagesDir = existsSync2(join(this.uiDir, "pages")) ? join(this.uiDir, "pages") : this.uiDir;
|
|
755
783
|
this.outDir = join(this.uiDir, ".weifuwu", "ssr");
|
|
756
784
|
this.router = new Router();
|
|
757
785
|
}
|
|
@@ -766,7 +794,7 @@ var TsxInstance = class {
|
|
|
766
794
|
if (p.routePath) allFiles.add(p.routePath);
|
|
767
795
|
}
|
|
768
796
|
const nfPath = join(this.pagesDir, "not-found.tsx");
|
|
769
|
-
const hasNotFound =
|
|
797
|
+
const hasNotFound = existsSync2(nfPath);
|
|
770
798
|
if (hasNotFound) {
|
|
771
799
|
allFiles.add(nfPath);
|
|
772
800
|
const rootLayouts = resolveLayouts(this.pagesDir, this.pagesDir);
|
|
@@ -899,14 +927,14 @@ ${body}`;
|
|
|
899
927
|
} catch {
|
|
900
928
|
return "";
|
|
901
929
|
}
|
|
902
|
-
const inputFile =
|
|
903
|
-
if (!
|
|
930
|
+
const inputFile = resolve2(this.uiDir, "app.css");
|
|
931
|
+
if (!existsSync2(inputFile)) {
|
|
904
932
|
mkdirSync(this.uiDir, { recursive: true });
|
|
905
933
|
writeFileSync(inputFile, '@import "tailwindcss"\n', "utf-8");
|
|
906
934
|
console.log("\u2139 weifuwu/tsx: created " + relative(process.cwd(), inputFile));
|
|
907
935
|
}
|
|
908
936
|
try {
|
|
909
|
-
let src =
|
|
937
|
+
let src = readFileSync2(inputFile, "utf-8");
|
|
910
938
|
const sourceRel = relative(this.uiDir, this.pagesDir) || ".";
|
|
911
939
|
const sourcePath = sourceRel === "." ? "./" : `./${sourceRel}/`;
|
|
912
940
|
src = `@source "${sourcePath}";
|
|
@@ -924,7 +952,7 @@ ${src}`;
|
|
|
924
952
|
headers: { "content-type": "text/css; charset=utf-8" }
|
|
925
953
|
}));
|
|
926
954
|
if (isDev) {
|
|
927
|
-
const inputFile =
|
|
955
|
+
const inputFile = resolve2(this.uiDir, "app.css");
|
|
928
956
|
chokidar.watch(inputFile, { persistent: false }).on("change", async () => {
|
|
929
957
|
this.compiledTailwindCss = "";
|
|
930
958
|
await this.compileTailwind();
|
|
@@ -1073,7 +1101,7 @@ ${body}`;
|
|
|
1073
1101
|
timeout = null;
|
|
1074
1102
|
const files = [...pending];
|
|
1075
1103
|
pending.clear();
|
|
1076
|
-
const exists = files.filter((f) =>
|
|
1104
|
+
const exists = files.filter((f) => existsSync2(f));
|
|
1077
1105
|
const allKnown = exists.every(
|
|
1078
1106
|
(f) => this.pageModules.has(f) || this.layoutModules.has(f) || this.loadModules.has(f) || this.routeModules.has(f)
|
|
1079
1107
|
);
|
|
@@ -1138,7 +1166,7 @@ ${body}`;
|
|
|
1138
1166
|
if (p.routePath) freshFiles.add(p.routePath);
|
|
1139
1167
|
}
|
|
1140
1168
|
const nfPath = join(this.pagesDir, "not-found.tsx");
|
|
1141
|
-
if (
|
|
1169
|
+
if (existsSync2(nfPath)) {
|
|
1142
1170
|
freshFiles.add(nfPath);
|
|
1143
1171
|
const rootLayouts = resolveLayouts(this.pagesDir, this.pagesDir);
|
|
1144
1172
|
for (const lp of rootLayouts) freshFiles.add(lp);
|
|
@@ -1376,9 +1404,9 @@ function auth(options) {
|
|
|
1376
1404
|
// static.ts
|
|
1377
1405
|
import { createHash as createHash2 } from "node:crypto";
|
|
1378
1406
|
import { open, realpath } from "node:fs/promises";
|
|
1379
|
-
import { extname, resolve as
|
|
1407
|
+
import { extname, resolve as resolve3, normalize, sep as sep2 } from "node:path";
|
|
1380
1408
|
function serveStatic(root, options) {
|
|
1381
|
-
const rootDir =
|
|
1409
|
+
const rootDir = resolve3(root);
|
|
1382
1410
|
const opts = options ?? {};
|
|
1383
1411
|
return async (req, ctx) => {
|
|
1384
1412
|
const relativePath = ctx.params["*"] ?? new URL(req.url).pathname.slice(1);
|
|
@@ -1386,7 +1414,7 @@ function serveStatic(root, options) {
|
|
|
1386
1414
|
if (decoded.includes("..") || decoded.includes("\0")) {
|
|
1387
1415
|
return new Response("Forbidden", { status: 403 });
|
|
1388
1416
|
}
|
|
1389
|
-
let filePath = normalize(
|
|
1417
|
+
let filePath = normalize(resolve3(rootDir, decoded));
|
|
1390
1418
|
if (!filePath.startsWith(rootDir + sep2) && filePath !== rootDir) {
|
|
1391
1419
|
return new Response("Forbidden", { status: 403 });
|
|
1392
1420
|
}
|
|
@@ -1402,7 +1430,7 @@ function serveStatic(root, options) {
|
|
|
1402
1430
|
if (stat.isDirectory()) {
|
|
1403
1431
|
await fileHandle.close();
|
|
1404
1432
|
const indexFile = opts.index ?? "index.html";
|
|
1405
|
-
filePath =
|
|
1433
|
+
filePath = resolve3(filePath, indexFile);
|
|
1406
1434
|
if (!filePath.startsWith(rootDir + sep2)) {
|
|
1407
1435
|
return new Response("Forbidden", { status: 403 });
|
|
1408
1436
|
}
|
|
@@ -5106,14 +5134,14 @@ function forkApp(opts) {
|
|
|
5106
5134
|
return { child, port: opts.port };
|
|
5107
5135
|
}
|
|
5108
5136
|
function stopProcess(mp, timeout = 1e4) {
|
|
5109
|
-
return new Promise((
|
|
5137
|
+
return new Promise((resolve11) => {
|
|
5110
5138
|
const timer = setTimeout(() => {
|
|
5111
5139
|
mp.child.kill("SIGKILL");
|
|
5112
|
-
|
|
5140
|
+
resolve11();
|
|
5113
5141
|
}, timeout);
|
|
5114
5142
|
mp.child.on("exit", () => {
|
|
5115
5143
|
clearTimeout(timer);
|
|
5116
|
-
|
|
5144
|
+
resolve11();
|
|
5117
5145
|
});
|
|
5118
5146
|
mp.child.kill("SIGTERM");
|
|
5119
5147
|
});
|
|
@@ -5672,10 +5700,10 @@ function createBashTool(ctx) {
|
|
|
5672
5700
|
return { stdout: "", stderr: "Command denied: potentially dangerous command", exitCode: 1 };
|
|
5673
5701
|
}
|
|
5674
5702
|
const cwd = workdir ? `${ctx.workspace}/${workdir}` : ctx.workspace;
|
|
5675
|
-
return new Promise((
|
|
5703
|
+
return new Promise((resolve11) => {
|
|
5676
5704
|
const child = exec(command, { cwd, timeout: timeout * 1e3, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => {
|
|
5677
5705
|
const truncated = stdout.length > 1e6 || stderr.length > 1e6;
|
|
5678
|
-
|
|
5706
|
+
resolve11({
|
|
5679
5707
|
stdout: stdout.slice(0, 1e6),
|
|
5680
5708
|
stderr: stderr.slice(0, 1e6),
|
|
5681
5709
|
exitCode: error?.code ?? 0,
|
|
@@ -5691,8 +5719,8 @@ function createBashTool(ctx) {
|
|
|
5691
5719
|
// opencode/tools/read.ts
|
|
5692
5720
|
import { tool as tool3 } from "ai";
|
|
5693
5721
|
import { z as z6 } from "zod";
|
|
5694
|
-
import { readFileSync as
|
|
5695
|
-
import { resolve as
|
|
5722
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
5723
|
+
import { resolve as resolve4 } from "node:path";
|
|
5696
5724
|
function createReadTool(ctx) {
|
|
5697
5725
|
return tool3({
|
|
5698
5726
|
description: "Read file contents. Supports offset and limit for reading specific line ranges.",
|
|
@@ -5702,11 +5730,11 @@ function createReadTool(ctx) {
|
|
|
5702
5730
|
limit: z6.number().optional().describe("Number of lines to read")
|
|
5703
5731
|
}),
|
|
5704
5732
|
execute: async ({ path: path2, offset, limit }) => {
|
|
5705
|
-
const resolved =
|
|
5733
|
+
const resolved = resolve4(ctx.workspace, path2);
|
|
5706
5734
|
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
|
|
5707
5735
|
return { error: "Path not allowed", content: null, totalLines: 0 };
|
|
5708
5736
|
}
|
|
5709
|
-
const content =
|
|
5737
|
+
const content = readFileSync3(resolved, "utf-8");
|
|
5710
5738
|
const lines = content.split("\n");
|
|
5711
5739
|
const totalLines = lines.length;
|
|
5712
5740
|
if (offset !== void 0) {
|
|
@@ -5734,7 +5762,7 @@ function createReadTool(ctx) {
|
|
|
5734
5762
|
import { tool as tool4 } from "ai";
|
|
5735
5763
|
import { z as z7 } from "zod";
|
|
5736
5764
|
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
|
|
5737
|
-
import { resolve as
|
|
5765
|
+
import { resolve as resolve5, dirname as dirname2 } from "node:path";
|
|
5738
5766
|
function createWriteTool(ctx) {
|
|
5739
5767
|
return tool4({
|
|
5740
5768
|
description: "Create or overwrite a file. Parent directories are created automatically.",
|
|
@@ -5743,7 +5771,7 @@ function createWriteTool(ctx) {
|
|
|
5743
5771
|
content: z7.string().describe("File content")
|
|
5744
5772
|
}),
|
|
5745
5773
|
execute: async ({ path: path2, content }) => {
|
|
5746
|
-
const resolved =
|
|
5774
|
+
const resolved = resolve5(ctx.workspace, path2);
|
|
5747
5775
|
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
|
|
5748
5776
|
return { error: "Path not allowed" };
|
|
5749
5777
|
}
|
|
@@ -5757,8 +5785,8 @@ function createWriteTool(ctx) {
|
|
|
5757
5785
|
// opencode/tools/edit.ts
|
|
5758
5786
|
import { tool as tool5 } from "ai";
|
|
5759
5787
|
import { z as z8 } from "zod";
|
|
5760
|
-
import { readFileSync as
|
|
5761
|
-
import { resolve as
|
|
5788
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
5789
|
+
import { resolve as resolve6 } from "node:path";
|
|
5762
5790
|
function createEditTool(ctx) {
|
|
5763
5791
|
return tool5({
|
|
5764
5792
|
description: "Perform exact string replacements in a file. If oldString appears multiple times, provide more surrounding context.",
|
|
@@ -5769,11 +5797,11 @@ function createEditTool(ctx) {
|
|
|
5769
5797
|
replaceAll: z8.boolean().default(false).describe("Replace all occurrences")
|
|
5770
5798
|
}),
|
|
5771
5799
|
execute: async ({ path: path2, oldString, newString, replaceAll }) => {
|
|
5772
|
-
const resolved =
|
|
5800
|
+
const resolved = resolve6(ctx.workspace, path2);
|
|
5773
5801
|
if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions)) {
|
|
5774
5802
|
return { error: "Path not allowed" };
|
|
5775
5803
|
}
|
|
5776
|
-
const content =
|
|
5804
|
+
const content = readFileSync4(resolved, "utf-8");
|
|
5777
5805
|
if (replaceAll) {
|
|
5778
5806
|
if (!content.includes(oldString)) {
|
|
5779
5807
|
return { error: "oldString not found in file", replaced: 0 };
|
|
@@ -5802,8 +5830,8 @@ function createEditTool(ctx) {
|
|
|
5802
5830
|
import { tool as tool6 } from "ai";
|
|
5803
5831
|
import { z as z9 } from "zod";
|
|
5804
5832
|
import { execSync as execSync2 } from "node:child_process";
|
|
5805
|
-
import { resolve as
|
|
5806
|
-
import { existsSync as
|
|
5833
|
+
import { resolve as resolve7 } from "node:path";
|
|
5834
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
5807
5835
|
function createGrepTool(ctx) {
|
|
5808
5836
|
return tool6({
|
|
5809
5837
|
description: "Search file contents using regex. Supports file type filtering and context lines.",
|
|
@@ -5814,10 +5842,10 @@ function createGrepTool(ctx) {
|
|
|
5814
5842
|
context: z9.number().default(0).describe("Number of context lines before and after each match")
|
|
5815
5843
|
}),
|
|
5816
5844
|
execute: async ({ pattern, include, path: path2, context }) => {
|
|
5817
|
-
const searchDir = path2 ?
|
|
5845
|
+
const searchDir = path2 ? resolve7(ctx.workspace, path2) : ctx.workspace;
|
|
5818
5846
|
const contextArg = context > 0 ? `-C ${context}` : "";
|
|
5819
5847
|
let cmd;
|
|
5820
|
-
if (
|
|
5848
|
+
if (existsSync3("/usr/bin/rg") || existsSync3("/usr/local/bin/rg")) {
|
|
5821
5849
|
cmd = `rg -n ${contextArg} ${include ? `-g '${include}'` : ""} '${pattern.replace(/'/g, "'\\''")}' '${searchDir}'`;
|
|
5822
5850
|
} else {
|
|
5823
5851
|
cmd = `grep -rn ${contextArg} ${include ? `--include='${include}'` : ""} '${pattern.replace(/'/g, "'\\''")}' '${searchDir}'`;
|
|
@@ -5840,7 +5868,7 @@ function createGrepTool(ctx) {
|
|
|
5840
5868
|
import { tool as tool7 } from "ai";
|
|
5841
5869
|
import { z as z10 } from "zod";
|
|
5842
5870
|
import { execSync as execSync3 } from "node:child_process";
|
|
5843
|
-
import { resolve as
|
|
5871
|
+
import { resolve as resolve8 } from "node:path";
|
|
5844
5872
|
function createGlobTool(ctx) {
|
|
5845
5873
|
return tool7({
|
|
5846
5874
|
description: "Find files matching a glob pattern.",
|
|
@@ -5849,7 +5877,7 @@ function createGlobTool(ctx) {
|
|
|
5849
5877
|
path: z10.string().optional().describe("Subdirectory relative to workspace")
|
|
5850
5878
|
}),
|
|
5851
5879
|
execute: async ({ pattern, path: path2 }) => {
|
|
5852
|
-
const searchDir = path2 ?
|
|
5880
|
+
const searchDir = path2 ? resolve8(ctx.workspace, path2) : ctx.workspace;
|
|
5853
5881
|
try {
|
|
5854
5882
|
const stdout = execSync3(`find '${searchDir}' -name '${pattern.replace(/'/g, "'\\''")}' -not -path '*/node_modules/*' 2>/dev/null | head -200`, {
|
|
5855
5883
|
timeout: 1e4,
|
|
@@ -5903,7 +5931,7 @@ function createQuestionTool(ctx) {
|
|
|
5903
5931
|
options: z12.array(z12.string()).optional().describe("Optional multiple choice options")
|
|
5904
5932
|
}),
|
|
5905
5933
|
execute: async ({ question, options }, { toolCallId }) => {
|
|
5906
|
-
return new Promise((
|
|
5934
|
+
return new Promise((resolve11, reject) => {
|
|
5907
5935
|
const timeout = setTimeout(() => {
|
|
5908
5936
|
ctx.pendingQuestions.delete(toolCallId);
|
|
5909
5937
|
reject(new Error("Question timed out"));
|
|
@@ -5911,7 +5939,7 @@ function createQuestionTool(ctx) {
|
|
|
5911
5939
|
ctx.pendingQuestions.set(toolCallId, {
|
|
5912
5940
|
resolve: (answer) => {
|
|
5913
5941
|
clearTimeout(timeout);
|
|
5914
|
-
|
|
5942
|
+
resolve11(answer);
|
|
5915
5943
|
},
|
|
5916
5944
|
reject: (err) => {
|
|
5917
5945
|
clearTimeout(timeout);
|
|
@@ -6194,7 +6222,7 @@ function createWSHandler2(deps) {
|
|
|
6194
6222
|
import { readFile } from "node:fs/promises";
|
|
6195
6223
|
import { glob } from "node:fs/promises";
|
|
6196
6224
|
import { homedir } from "node:os";
|
|
6197
|
-
import { resolve as
|
|
6225
|
+
import { resolve as resolve9 } from "node:path";
|
|
6198
6226
|
import { parse as parseYaml } from "yaml";
|
|
6199
6227
|
var SEARCH_DIRS = [
|
|
6200
6228
|
(ws) => `${ws}/.opencode/skills`,
|
|
@@ -6232,7 +6260,7 @@ async function scanDir(dir) {
|
|
|
6232
6260
|
try {
|
|
6233
6261
|
const files = [];
|
|
6234
6262
|
for await (const entry of glob("*/SKILL.md", { cwd: dir })) {
|
|
6235
|
-
const skill = await parseSkillFile(
|
|
6263
|
+
const skill = await parseSkillFile(resolve9(dir, entry));
|
|
6236
6264
|
if (skill) files.push(skill);
|
|
6237
6265
|
}
|
|
6238
6266
|
return files;
|
|
@@ -6316,17 +6344,17 @@ function health(options) {
|
|
|
6316
6344
|
|
|
6317
6345
|
// i18n.ts
|
|
6318
6346
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
6319
|
-
import { existsSync as
|
|
6320
|
-
import { join as join4, resolve as
|
|
6347
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
6348
|
+
import { join as join4, resolve as resolve10 } from "node:path";
|
|
6321
6349
|
function i18n(options) {
|
|
6322
|
-
const dir =
|
|
6350
|
+
const dir = resolve10(options.dir);
|
|
6323
6351
|
const defaultLocale = options.defaultLocale ?? "en";
|
|
6324
6352
|
const cache = /* @__PURE__ */ new Map();
|
|
6325
6353
|
async function load(locale) {
|
|
6326
6354
|
const cached = cache.get(locale);
|
|
6327
6355
|
if (cached) return cached;
|
|
6328
6356
|
const filePath = join4(dir, `${locale}.json`);
|
|
6329
|
-
if (!
|
|
6357
|
+
if (!existsSync4(filePath)) return {};
|
|
6330
6358
|
try {
|
|
6331
6359
|
const content = await readFile2(filePath, "utf-8");
|
|
6332
6360
|
const data = JSON.parse(content);
|
|
@@ -6412,6 +6440,7 @@ export {
|
|
|
6412
6440
|
graphql,
|
|
6413
6441
|
health,
|
|
6414
6442
|
i18n,
|
|
6443
|
+
loadEnv,
|
|
6415
6444
|
logger,
|
|
6416
6445
|
mailer,
|
|
6417
6446
|
messager,
|
package/docs/agent.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# AI Agent
|
|
2
|
+
|
|
3
|
+
> [Home](../README.md) → AI Agent
|
|
4
|
+
|
|
5
|
+
## AI Agent
|
|
6
|
+
|
|
7
|
+
Server-side AI agents with OpenAI-compatible API. Built-in chat, tool-use (tool-calling), and knowledge (RAG) types. Works out of the box with Ollama or any OpenAI-compatible provider.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { agent } from 'weifuwu'
|
|
11
|
+
|
|
12
|
+
const agents = agent({ pg })
|
|
13
|
+
|
|
14
|
+
await agents.migrate()
|
|
15
|
+
app.use('/api', agents.router())
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| Type | Description | Execution |
|
|
19
|
+
|------|-------------|-----------|
|
|
20
|
+
| `chat` | Pure conversation | `streamText()` / `generateText()` |
|
|
21
|
+
| `tool-use` | Tool-calling agent | `streamText({ tools })` |
|
|
22
|
+
|
|
23
|
+
### Knowledge (RAG)
|
|
24
|
+
|
|
25
|
+
Add documents to any agent — `searchKnowledge` tool auto-injected:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
await agents.addKnowledge(agentId, 'Title', 'Document content...')
|
|
29
|
+
// The agent automatically calls searchKnowledge when answering
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Streaming
|
|
33
|
+
|
|
34
|
+
```http
|
|
35
|
+
POST /agents/:id/run { input: "hello", stream: true }
|
|
36
|
+
→ event-stream (fullStream SSE: text-delta, tool-call, tool-result, finish)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Programmatic API
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
const result = await agents.run(agentId, { input: 'hello', stream: false })
|
|
43
|
+
// { output: "Hello!", elapsed: 1234 }
|
|
44
|
+
```
|