@bondedhq/runner 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +272 -2
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +120 -0
- package/dist/code-hash.d.ts +54 -0
- package/dist/code-hash.js +110 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/init/apply.d.ts +22 -0
- package/dist/init/apply.js +73 -0
- package/dist/init/check.d.ts +79 -0
- package/dist/init/check.js +158 -0
- package/dist/init/command.d.ts +34 -0
- package/dist/init/command.js +314 -0
- package/dist/init/detect.d.ts +34 -0
- package/dist/init/detect.js +184 -0
- package/dist/init/diff.d.ts +6 -0
- package/dist/init/diff.js +98 -0
- package/dist/init/plan.d.ts +44 -0
- package/dist/init/plan.js +652 -0
- package/dist/runner.d.ts +78 -0
- package/dist/runner.js +319 -0
- package/dist/service.d.ts +24 -0
- package/dist/service.js +169 -0
- package/dist/tail.d.ts +21 -0
- package/dist/tail.js +50 -0
- package/examples/agent-url-agent.mjs +140 -0
- package/examples/mcp-agent.mjs +60 -0
- package/package.json +60 -3
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** The agent frameworks `init` knows. */
|
|
2
|
+
export declare const FRAMEWORKS: readonly ["eliza", "agentkit", "langchain", "ai-sdk", "mcp", "python", "unknown"];
|
|
3
|
+
export type Framework = (typeof FRAMEWORKS)[number];
|
|
4
|
+
export declare const FRAMEWORK_NAMES: Record<Framework, string>;
|
|
5
|
+
/** Web frameworks `init --url` can add an Agent URL route to. */
|
|
6
|
+
export type WebFramework = "next-app" | "next-pages" | "express" | "hono" | "node-http";
|
|
7
|
+
export type PackageManager = "npm" | "pnpm" | "yarn" | "bun";
|
|
8
|
+
/** What `init` found in the agent's folder. Paths are relative to `root`, with `/`. */
|
|
9
|
+
export interface Project {
|
|
10
|
+
root: string;
|
|
11
|
+
framework: Framework;
|
|
12
|
+
/** Why it decided on the framework, in words. */
|
|
13
|
+
evidence: string;
|
|
14
|
+
packageManager: PackageManager;
|
|
15
|
+
/** TypeScript (a tsconfig.json or a typescript dependency). */
|
|
16
|
+
typescript: boolean;
|
|
17
|
+
/** package.json `"type": "module"`: plain .js files are ES modules. */
|
|
18
|
+
esm: boolean;
|
|
19
|
+
/** How relative imports are written in TypeScript: with `.js` (NodeNext) or without. */
|
|
20
|
+
importExtension: "" | ".js";
|
|
21
|
+
/** Where source files live: `src` when there is one, else the root. */
|
|
22
|
+
sourceDir: string;
|
|
23
|
+
/** Source files (TS/JS), skipping node_modules, build output and tests. */
|
|
24
|
+
sources: string[];
|
|
25
|
+
dependencies: Record<string, string>;
|
|
26
|
+
scripts: Record<string, string>;
|
|
27
|
+
web: WebFramework;
|
|
28
|
+
/** Next.js App Router folder (`app` or `src/app`), when there is one. */
|
|
29
|
+
nextAppDir?: string;
|
|
30
|
+
nextMajor?: number;
|
|
31
|
+
hasSdk: boolean;
|
|
32
|
+
}
|
|
33
|
+
export declare function detectProject(root: string, override?: Framework): Project;
|
|
34
|
+
//# sourceMappingURL=detect.d.ts.map
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
3
|
+
/** The agent frameworks `init` knows. */
|
|
4
|
+
export const FRAMEWORKS = [
|
|
5
|
+
"eliza",
|
|
6
|
+
"agentkit",
|
|
7
|
+
"langchain",
|
|
8
|
+
"ai-sdk",
|
|
9
|
+
"mcp",
|
|
10
|
+
"python",
|
|
11
|
+
"unknown",
|
|
12
|
+
];
|
|
13
|
+
export const FRAMEWORK_NAMES = {
|
|
14
|
+
eliza: "ElizaOS",
|
|
15
|
+
agentkit: "Coinbase AgentKit",
|
|
16
|
+
langchain: "LangChain / LangGraph",
|
|
17
|
+
"ai-sdk": "Vercel AI SDK",
|
|
18
|
+
mcp: "an MCP client",
|
|
19
|
+
python: "Python",
|
|
20
|
+
unknown: "an unrecognised framework",
|
|
21
|
+
};
|
|
22
|
+
const SKIP = new Set([
|
|
23
|
+
"node_modules",
|
|
24
|
+
"dist",
|
|
25
|
+
"build",
|
|
26
|
+
"out",
|
|
27
|
+
".next",
|
|
28
|
+
".git",
|
|
29
|
+
".turbo",
|
|
30
|
+
"coverage",
|
|
31
|
+
"test",
|
|
32
|
+
"tests",
|
|
33
|
+
"__tests__",
|
|
34
|
+
]);
|
|
35
|
+
const SOURCE = /\.(ts|tsx|mts|js|mjs|jsx)$/;
|
|
36
|
+
const MAX_SOURCES = 400;
|
|
37
|
+
export function detectProject(root, override) {
|
|
38
|
+
const pkgPath = join(root, "package.json");
|
|
39
|
+
const pkg = existsSync(pkgPath)
|
|
40
|
+
? JSON.parse(readFileSync(pkgPath, "utf8"))
|
|
41
|
+
: undefined;
|
|
42
|
+
const dependencies = { ...pkg?.devDependencies, ...pkg?.dependencies };
|
|
43
|
+
const has = (name) => name in dependencies;
|
|
44
|
+
const tsconfig = readTsconfig(root);
|
|
45
|
+
const typescript = tsconfig !== undefined || has("typescript");
|
|
46
|
+
const sourceDir = existsSync(join(root, "src")) && statSync(join(root, "src")).isDirectory() ? "src" : "";
|
|
47
|
+
const sources = listSources(root);
|
|
48
|
+
const found = override
|
|
49
|
+
? { framework: override, evidence: "chosen with --framework" }
|
|
50
|
+
: detectFramework(root, pkg !== undefined, has, sources);
|
|
51
|
+
const resolution = String(tsconfig?.compilerOptions?.moduleResolution ?? tsconfig?.compilerOptions?.module ?? "").toLowerCase();
|
|
52
|
+
const nextAppDir = ["app", "src/app"].find((d) => existsSync(join(root, d)));
|
|
53
|
+
const nextMajor = has("next")
|
|
54
|
+
? Number(/(\d+)/.exec(dependencies.next ?? "")?.[1]) || undefined
|
|
55
|
+
: undefined;
|
|
56
|
+
return {
|
|
57
|
+
root,
|
|
58
|
+
...found,
|
|
59
|
+
packageManager: packageManager(root, pkg?.packageManager),
|
|
60
|
+
typescript,
|
|
61
|
+
esm: pkg?.type === "module",
|
|
62
|
+
importExtension: resolution.startsWith("node16") || resolution.startsWith("nodenext") ? ".js" : "",
|
|
63
|
+
sourceDir,
|
|
64
|
+
sources,
|
|
65
|
+
dependencies,
|
|
66
|
+
scripts: pkg?.scripts ?? {},
|
|
67
|
+
web: has("next")
|
|
68
|
+
? nextAppDir
|
|
69
|
+
? "next-app"
|
|
70
|
+
: "next-pages"
|
|
71
|
+
: has("hono")
|
|
72
|
+
? "hono"
|
|
73
|
+
: has("express")
|
|
74
|
+
? "express"
|
|
75
|
+
: "node-http",
|
|
76
|
+
...(nextAppDir && has("next") ? { nextAppDir } : {}),
|
|
77
|
+
...(nextMajor ? { nextMajor } : {}),
|
|
78
|
+
hasSdk: has("@bondedhq/sdk"),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function detectFramework(root, hasPackageJson, has, sources) {
|
|
82
|
+
if (has("@coinbase/agentkit"))
|
|
83
|
+
return { framework: "agentkit", evidence: "package.json depends on @coinbase/agentkit" };
|
|
84
|
+
if (has("@elizaos/core"))
|
|
85
|
+
return { framework: "eliza", evidence: "package.json depends on @elizaos/core" };
|
|
86
|
+
const character = characterFile(root);
|
|
87
|
+
if (character)
|
|
88
|
+
return { framework: "eliza", evidence: `${character} looks like an ElizaOS character` };
|
|
89
|
+
for (const name of ["@langchain/langgraph", "@langchain/core", "langchain"])
|
|
90
|
+
if (has(name))
|
|
91
|
+
return { framework: "langchain", evidence: `package.json depends on ${name}` };
|
|
92
|
+
if (has("ai"))
|
|
93
|
+
return { framework: "ai-sdk", evidence: "package.json depends on ai (the Vercel AI SDK)" };
|
|
94
|
+
if (has("@modelcontextprotocol/sdk"))
|
|
95
|
+
return { framework: "mcp", evidence: "package.json depends on @modelcontextprotocol/sdk" };
|
|
96
|
+
if (!hasPackageJson &&
|
|
97
|
+
["pyproject.toml", "requirements.txt", "setup.py"].some((f) => existsSync(join(root, f))))
|
|
98
|
+
return {
|
|
99
|
+
framework: "python",
|
|
100
|
+
evidence: "a Python project (pyproject.toml or requirements.txt)",
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
framework: "unknown",
|
|
104
|
+
evidence: hasPackageJson
|
|
105
|
+
? `no known agent framework in package.json${sources.length ? "" : ", and no source files"}`
|
|
106
|
+
: "no package.json",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** An ElizaOS character file: JSON with a name and plugins or bio. */
|
|
110
|
+
function characterFile(root) {
|
|
111
|
+
const candidates = [];
|
|
112
|
+
for (const dir of ["", "characters"]) {
|
|
113
|
+
const abs = join(root, dir);
|
|
114
|
+
if (!existsSync(abs) || !statSync(abs).isDirectory())
|
|
115
|
+
continue;
|
|
116
|
+
for (const f of readdirSync(abs))
|
|
117
|
+
if (f.endsWith(".character.json") || (dir === "characters" && f.endsWith(".json")))
|
|
118
|
+
candidates.push(dir ? `${dir}/${f}` : f);
|
|
119
|
+
}
|
|
120
|
+
return candidates.find((f) => {
|
|
121
|
+
try {
|
|
122
|
+
const json = JSON.parse(readFileSync(join(root, f), "utf8"));
|
|
123
|
+
return typeof json.name === "string" && ("plugins" in json || "bio" in json);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** The package manager, from `packageManager` or the nearest lockfile (here or a parent folder). */
|
|
131
|
+
function packageManager(root, field) {
|
|
132
|
+
const named = /^(npm|pnpm|yarn|bun)@/.exec(field ?? "")?.[1];
|
|
133
|
+
if (named)
|
|
134
|
+
return named;
|
|
135
|
+
for (let dir = root;; dir = dirname(dir)) {
|
|
136
|
+
if (existsSync(join(dir, "pnpm-lock.yaml")))
|
|
137
|
+
return "pnpm";
|
|
138
|
+
if (existsSync(join(dir, "yarn.lock")))
|
|
139
|
+
return "yarn";
|
|
140
|
+
if (existsSync(join(dir, "bun.lockb")) || existsSync(join(dir, "bun.lock")))
|
|
141
|
+
return "bun";
|
|
142
|
+
if (existsSync(join(dir, "package-lock.json")))
|
|
143
|
+
return "npm";
|
|
144
|
+
if (dirname(dir) === dir || existsSync(join(dir, ".git")))
|
|
145
|
+
return "npm";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** tsconfig.json, read leniently (comments and trailing commas allowed). */
|
|
149
|
+
function readTsconfig(root) {
|
|
150
|
+
const path = join(root, "tsconfig.json");
|
|
151
|
+
if (!existsSync(path))
|
|
152
|
+
return undefined;
|
|
153
|
+
const text = readFileSync(path, "utf8")
|
|
154
|
+
.replace(/("(?:[^"\\]|\\.)*")|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g, (_, s) => s ?? "")
|
|
155
|
+
.replace(/,(\s*[}\]])/g, "$1");
|
|
156
|
+
try {
|
|
157
|
+
return JSON.parse(text);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return {};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function listSources(root) {
|
|
164
|
+
const out = [];
|
|
165
|
+
const walk = (dir) => {
|
|
166
|
+
if (out.length >= MAX_SOURCES)
|
|
167
|
+
return;
|
|
168
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
169
|
+
if (entry.name.startsWith(".") || SKIP.has(entry.name))
|
|
170
|
+
continue;
|
|
171
|
+
const abs = join(dir, entry.name);
|
|
172
|
+
if (entry.isDirectory())
|
|
173
|
+
walk(abs);
|
|
174
|
+
else if (entry.isFile() &&
|
|
175
|
+
SOURCE.test(entry.name) &&
|
|
176
|
+
!/\.(test|spec|d)\.[a-z]+$/.test(entry.name) &&
|
|
177
|
+
!/\.config\.[a-z]+$/.test(entry.name))
|
|
178
|
+
out.push(relative(root, abs).split(sep).join("/"));
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
walk(root);
|
|
182
|
+
return out.sort();
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=detect.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A unified diff of one file (`git diff` style, 3 lines of context). `before` is undefined for a
|
|
3
|
+
* new file. Line-based longest common subsequence: `init`'s files are small.
|
|
4
|
+
*/
|
|
5
|
+
export declare function unifiedDiff(path: string, before: string | undefined, after: string): string;
|
|
6
|
+
//# sourceMappingURL=diff.d.ts.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A unified diff of one file (`git diff` style, 3 lines of context). `before` is undefined for a
|
|
3
|
+
* new file. Line-based longest common subsequence: `init`'s files are small.
|
|
4
|
+
*/
|
|
5
|
+
export function unifiedDiff(path, before, after) {
|
|
6
|
+
const a = before === undefined ? [] : splitLines(before);
|
|
7
|
+
const b = splitLines(after);
|
|
8
|
+
const ops = diffLines(a, b);
|
|
9
|
+
const header = before === undefined ? `--- /dev/null\n+++ b/${path}\n` : `--- a/${path}\n+++ b/${path}\n`;
|
|
10
|
+
const hunks = [];
|
|
11
|
+
const CONTEXT = 3;
|
|
12
|
+
let i = 0;
|
|
13
|
+
while (i < ops.length) {
|
|
14
|
+
// The next change.
|
|
15
|
+
while (i < ops.length && ops[i].kind === " ")
|
|
16
|
+
i++;
|
|
17
|
+
if (i >= ops.length)
|
|
18
|
+
break;
|
|
19
|
+
const start = Math.max(0, i - CONTEXT);
|
|
20
|
+
let end = i;
|
|
21
|
+
// Extend while changes are within 2 * CONTEXT of each other.
|
|
22
|
+
for (let j = i; j < ops.length; j++) {
|
|
23
|
+
if (ops[j].kind !== " ")
|
|
24
|
+
end = j;
|
|
25
|
+
else if (j - end > 2 * CONTEXT)
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
const stop = Math.min(ops.length, end + CONTEXT + 1);
|
|
29
|
+
const slice = ops.slice(start, stop);
|
|
30
|
+
const aStart = ops.slice(0, start).filter((o) => o.kind !== "+").length;
|
|
31
|
+
const bStart = ops.slice(0, start).filter((o) => o.kind !== "-").length;
|
|
32
|
+
const aLen = slice.filter((o) => o.kind !== "+").length;
|
|
33
|
+
const bLen = slice.filter((o) => o.kind !== "-").length;
|
|
34
|
+
hunks.push(`@@ -${aLen ? aStart + 1 : aStart},${aLen} +${bLen ? bStart + 1 : bStart},${bLen} @@\n` +
|
|
35
|
+
slice.map((o) => `${o.kind}${o.line}\n`).join(""));
|
|
36
|
+
i = stop;
|
|
37
|
+
}
|
|
38
|
+
return hunks.length ? header + hunks.join("") : "";
|
|
39
|
+
}
|
|
40
|
+
function splitLines(text) {
|
|
41
|
+
const lines = text.split("\n");
|
|
42
|
+
if (lines.at(-1) === "")
|
|
43
|
+
lines.pop();
|
|
44
|
+
return lines;
|
|
45
|
+
}
|
|
46
|
+
function diffLines(a, b) {
|
|
47
|
+
// Common prefix and suffix first, so the table covers only the changed middle.
|
|
48
|
+
let pre = 0;
|
|
49
|
+
while (pre < a.length && pre < b.length && a[pre] === b[pre])
|
|
50
|
+
pre++;
|
|
51
|
+
let suf = 0;
|
|
52
|
+
while (suf < a.length - pre &&
|
|
53
|
+
suf < b.length - pre &&
|
|
54
|
+
a[a.length - 1 - suf] === b[b.length - 1 - suf])
|
|
55
|
+
suf++;
|
|
56
|
+
const am = a.slice(pre, a.length - suf);
|
|
57
|
+
const bm = b.slice(pre, b.length - suf);
|
|
58
|
+
const middle = [];
|
|
59
|
+
if (am.length * bm.length > 4_000_000) {
|
|
60
|
+
// Too big to align: show it as replaced.
|
|
61
|
+
middle.push(...am.map((line) => ({ kind: "-", line })));
|
|
62
|
+
middle.push(...bm.map((line) => ({ kind: "+", line })));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const n = am.length;
|
|
66
|
+
const m = bm.length;
|
|
67
|
+
const table = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
|
|
68
|
+
for (let x = n - 1; x >= 0; x--)
|
|
69
|
+
for (let y = m - 1; y >= 0; y--)
|
|
70
|
+
table[x][y] =
|
|
71
|
+
am[x] === bm[y]
|
|
72
|
+
? table[x + 1][y + 1] + 1
|
|
73
|
+
: Math.max(table[x + 1][y], table[x][y + 1]);
|
|
74
|
+
let x = 0;
|
|
75
|
+
let y = 0;
|
|
76
|
+
while (x < n || y < m) {
|
|
77
|
+
if (x < n && y < m && am[x] === bm[y]) {
|
|
78
|
+
middle.push({ kind: " ", line: am[x] });
|
|
79
|
+
x++;
|
|
80
|
+
y++;
|
|
81
|
+
}
|
|
82
|
+
else if (x < n && (y >= m || table[x + 1][y] >= table[x][y + 1])) {
|
|
83
|
+
middle.push({ kind: "-", line: am[x] });
|
|
84
|
+
x++;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
middle.push({ kind: "+", line: bm[y] });
|
|
88
|
+
y++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return [
|
|
93
|
+
...a.slice(0, pre).map((line) => ({ kind: " ", line })),
|
|
94
|
+
...middle,
|
|
95
|
+
...a.slice(a.length - suf).map((line) => ({ kind: " ", line })),
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=diff.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type Framework, type Project } from "./detect.js";
|
|
2
|
+
/** One file `init` would write: new (no `before`) or changed. Paths relative to the project. */
|
|
3
|
+
export interface FileChange {
|
|
4
|
+
path: string;
|
|
5
|
+
before?: string;
|
|
6
|
+
after: string;
|
|
7
|
+
/** What it is for, in a sentence. */
|
|
8
|
+
why: string;
|
|
9
|
+
}
|
|
10
|
+
/** Everything `init` proposes for a project. Nothing is written until it is applied. */
|
|
11
|
+
export interface InitPlan {
|
|
12
|
+
framework: Framework;
|
|
13
|
+
changes: FileChange[];
|
|
14
|
+
/** What `init` could not change safely: do it by hand (each a sentence or a code line). */
|
|
15
|
+
manual: string[];
|
|
16
|
+
/** Things to know (not to do now). */
|
|
17
|
+
notes: string[];
|
|
18
|
+
/** Variables the new code reads, for `.env` (names only). */
|
|
19
|
+
env: string[];
|
|
20
|
+
/** The packages to add, when they aren't there yet. */
|
|
21
|
+
install: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface PlanOptions {
|
|
24
|
+
/** Also add an Agent URL route for the project's web framework. */
|
|
25
|
+
url?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Rewrite files init wrote before even when they were edited since (the diff shows what goes).
|
|
28
|
+
* Off by default: an edited file is kept.
|
|
29
|
+
*/
|
|
30
|
+
overwriteGenerated?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The first line of every file `init` writes, so a second run knows the file is init's. It is
|
|
34
|
+
* rewritten only while it still holds exactly what init would write (or with --overwrite-generated).
|
|
35
|
+
*/
|
|
36
|
+
export declare const MARKER = "// Added by `bonded-runner init`";
|
|
37
|
+
/** The production vault's variables (`bondedVaultFromEnv` in @bondedhq/sdk). */
|
|
38
|
+
export declare const VAULT_ENV: string[];
|
|
39
|
+
export declare function planInit(project: Project, options?: PlanOptions): InitPlan;
|
|
40
|
+
/** Adds an import after the file's existing imports (or at the top), unless it is there already. */
|
|
41
|
+
export declare function addImport(text: string, statement: string, specifier: string): string;
|
|
42
|
+
/** What the plan does, framework by framework, in a line: for the summary. */
|
|
43
|
+
export declare function frameworkLine(project: Project): string;
|
|
44
|
+
//# sourceMappingURL=plan.d.ts.map
|