Skip to content

Commit 353b08d

Browse files
authored
Merge pull request #12 from flowglad/sync-@just-bash/executor@1.0.2
sync: upstream @just-bash/executor@1.0.2
2 parents 718e5fd + ef82be4 commit 353b08d

1,398 files changed

Lines changed: 22353 additions & 8738 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ todo/
1818
fuzz-*.log
1919
.claude/settings.local.json
2020
.deepsec/
21+
plans/

examples/cjs-consumer/pnpm-lock.yaml

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/custom-command/commands.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
buildLinkSummaryPrompt,
1414
pickSummaryLengthForCharacters,
1515
} from "@steipete/summarize-core/prompts";
16-
import { defineCommand } from "just-bash";
16+
import { decodeBytesToUtf8, defineCommand } from "just-bash";
1717

1818
/**
1919
* Generate a random UUID
@@ -50,10 +50,11 @@ export const uuidCommand = defineCommand("uuid", async (args) => {
5050
* Usage: json-format [file] or pipe JSON to it
5151
*/
5252
export const jsonFormatCommand = defineCommand("json-format", async (args, ctx) => {
53-
let input = ctx.stdin;
53+
// Decode bytes to text — JSON.parse requires real Unicode.
54+
let input = decodeBytesToUtf8(ctx.stdin);
5455

5556
// Read from file if provided
56-
if (args[0] && !ctx.stdin) {
57+
if (args[0] && !input) {
5758
try {
5859
input = await ctx.fs.readFile(ctx.fs.resolvePath(ctx.cwd, args[0]));
5960
} catch {
@@ -134,10 +135,12 @@ export const loremCommand = defineCommand("lorem", async (args) => {
134135
* Similar to wc but with labeled output
135136
*/
136137
export const wordcountCommand = defineCommand("wordcount", async (args, ctx) => {
137-
let input = ctx.stdin;
138+
// ctx.stdin is a `ByteString` (the pipeline carries raw bytes); decode
139+
// for the line/word/char math, which only makes sense on Unicode text.
140+
let input = decodeBytesToUtf8(ctx.stdin);
138141

139142
// Read from file if provided
140-
if (args[0] && !ctx.stdin) {
143+
if (args[0] && !input) {
141144
try {
142145
input = await ctx.fs.readFile(ctx.fs.resolvePath(ctx.cwd, args[0]));
143146
} catch {
@@ -151,7 +154,7 @@ export const wordcountCommand = defineCommand("wordcount", async (args, ctx) =>
151154

152155
const lines = input.split("\n").length - (input.endsWith("\n") ? 1 : 0);
153156
const words = input.trim().split(/\s+/).filter(Boolean).length;
154-
const chars = input.length;
157+
const chars = Array.from(input).length;
155158

156159
return {
157160
stdout: `Lines: ${lines}\nWords: ${words}\nChars: ${chars}\n`,
@@ -166,8 +169,12 @@ export const wordcountCommand = defineCommand("wordcount", async (args, ctx) =>
166169
* Usage: reverse or pipe text to it
167170
*/
168171
export const reverseCommand = defineCommand("reverse", async (_args, ctx) => {
169-
const lines = ctx.stdin.split("\n");
170-
const reversed = lines.map((line) => line.split("").reverse().join(""));
172+
// Decode bytes to text so reversing happens by codepoint (multibyte
173+
// characters stay intact). `Array.from` splits on Unicode codepoints
174+
// rather than UTF-16 code units, so emoji / surrogate pairs survive.
175+
const text = decodeBytesToUtf8(ctx.stdin);
176+
const lines = text.split("\n");
177+
const reversed = lines.map((line) => Array.from(line).reverse().join(""));
171178
return {
172179
stdout: reversed.join("\n"),
173180
stderr: "",
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# executor-tools-example
2+
3+
## 1.0.3
4+
5+
### Patch Changes
6+
7+
- Updated dependencies [[`01a4721`](https://github.com/vercel-labs/just-bash/commit/01a4721324350adea4b035b311f0b60ccdbb65ff)]:
8+
- just-bash@3.0.1
9+
- @just-bash/executor@1.0.2
10+
11+
## 1.0.2
12+
13+
### Patch Changes
14+
15+
- Updated dependencies [[`fd98df8`](https://github.com/vercel-labs/just-bash/commit/fd98df8048d658454ed0769c020594754bf6e43d)]:
16+
- @just-bash/executor@1.0.1
17+
18+
## 1.0.1
19+
20+
### Patch Changes
21+
22+
- Updated dependencies [[`7cca738`](https://github.com/vercel-labs/just-bash/commit/7cca73831987e3331160f426b7a66d7217b8cf79), [`b3bd85e`](https://github.com/vercel-labs/just-bash/commit/b3bd85ed816445e6d148290163a1900f49ebea82), [`7cca738`](https://github.com/vercel-labs/just-bash/commit/7cca73831987e3331160f426b7a66d7217b8cf79)]:
23+
- just-bash@3.0.0
24+
- @just-bash/executor@1.0.0

examples/executor-tools/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Executor Tools Examples
2+
3+
Demonstrates executor tool invocation in just-bash. Sandboxed JavaScript code running in `js-exec` calls tools that fetch from real public APIs — no API keys needed.
4+
5+
## Run
6+
7+
```bash
8+
cd examples/executor-tools
9+
pnpm install
10+
11+
# Run all examples
12+
pnpm start
13+
14+
# Run a specific example
15+
npx tsx inline-tools.ts
16+
npx tsx multi-turn-discovery.ts
17+
npx tsx multi-api-agent.ts # default country: JP
18+
npx tsx multi-api-agent.ts BR # override
19+
20+
# Or via main.ts
21+
npx tsx main.ts 1 # inline tools
22+
npx tsx main.ts 2 # SDK discovery
23+
npx tsx main.ts 3 # multi-API agent loop
24+
```
25+
26+
## Examples
27+
28+
### Example 1: Inline Tools (`inline-tools.ts`)
29+
30+
Defines tools directly in the `Bash` constructor — no SDK required.
31+
32+
1. **GraphQL tools** — Countries API queries exposed as `tools.countries.*`
33+
2. **Utility tools**`tools.util.timestamp()`, `tools.util.random()`
34+
3. **Cross-tool scripts** — one js-exec script calling tools from multiple namespaces
35+
4. **Tools + filesystem** — fetch data via tools, write to virtual fs, read with bash commands
36+
5. **Error handling** — tool errors propagate as catchable exceptions
37+
38+
### Example 2: Multi-Turn Tool Discovery (`multi-turn-discovery.ts`)
39+
40+
Uses `experimental_executor.setup` with the real `@executor/sdk` to auto-discover tools from a live GraphQL schema — no inline tool definitions. The SDK introspects the countries API and registers one tool per query type.
41+
42+
1. **Discover** — Agent reads `/.executor/config.json` to see registered sources
43+
2. **Use** — Agent calls a discovered tool (`tools.countries.country({ code: "JP" })`)
44+
3. **Filter** — Agent queries a list endpoint with filters (`tools.countries.countries()`)
45+
4. **Chain** — Agent chains multiple tools: continents → countries per continent
46+
5. **Persist** — Agent writes all 250 countries as CSV to the virtual filesystem
47+
48+
### Example 3: Multi-API Agent Loop (`multi-api-agent.ts`)
49+
50+
Three real public APIs (REST Countries, Open-Meteo, Wikipedia) are wrapped as inline executor tools and orchestrated across multiple turns to produce a "country snapshot" markdown report. Demonstrates the multi-source pattern from the upstream `@executor-js` examples — using inline tools instead of SDK-discovered ones, so it runs anywhere with no plugin dependencies.
51+
52+
1. **Parallel lookup** — One js-exec script fetches country, weather, and Wikipedia data in sequence and stashes JSON results in the virtual filesystem
53+
2. **Bash composition** — A pure-bash heredoc reads the saved JSON via `jq` and writes a markdown report
54+
3. **CLI surface** — The same tools are also auto-exposed as bash commands (`country lookup code=BR | jq -r .name`)
55+
56+
Pass a country code to override the default: `npx tsx multi-api-agent.ts US`.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/**
2+
* Example 1: Inline Tools
3+
*
4+
* Demonstrates defining tools and calling them from sandboxed js-exec scripts.
5+
* No @executor-js/sdk plugins required for inline tools — only the SDK itself
6+
* is needed via @just-bash/executor's peer deps.
7+
*
8+
* Uses:
9+
* - countries.trevorblades.com (GraphQL) — country data
10+
*
11+
* Run with: npx tsx inline-tools.ts
12+
*/
13+
14+
import { createExecutor } from "@just-bash/executor";
15+
import { Bash } from "just-bash";
16+
17+
const executor = await createExecutor({
18+
tools: {
19+
// GraphQL tool — queries countries.trevorblades.com
20+
"countries.list": {
21+
description: "List countries, optionally filtered by continent code",
22+
execute: async (args?: { continent?: string }) => {
23+
const query = args?.continent
24+
? `query($code: String!) { countries(filter: { continent: { eq: $code } }) { code name capital emoji } }`
25+
: `{ countries { code name capital emoji } }`;
26+
const variables = args?.continent
27+
? { code: args.continent }
28+
: undefined;
29+
const res = await fetch("https://countries.trevorblades.com/graphql", {
30+
method: "POST",
31+
headers: { "Content-Type": "application/json" },
32+
body: JSON.stringify({ query, variables }),
33+
});
34+
const json = (await res.json()) as { data: { countries: unknown[] } };
35+
return json.data.countries;
36+
},
37+
},
38+
39+
"countries.get": {
40+
description: "Get a single country by ISO code",
41+
execute: async (args: { code: string }) => {
42+
const query = `query($code: ID!) { country(code: $code) { name capital currency emoji languages { name } continent { name } } }`;
43+
const res = await fetch("https://countries.trevorblades.com/graphql", {
44+
method: "POST",
45+
headers: { "Content-Type": "application/json" },
46+
body: JSON.stringify({ query, variables: { code: args.code } }),
47+
});
48+
const json = (await res.json()) as { data: { country: unknown } };
49+
return json.data.country;
50+
},
51+
},
52+
53+
// Simple sync tools
54+
"util.timestamp": {
55+
description: "Current Unix timestamp",
56+
execute: () => ({ ts: Math.floor(Date.now() / 1000) }),
57+
},
58+
"util.random": {
59+
description: "Random number between min and max",
60+
execute: (args: { min?: number; max?: number }) => ({
61+
value: Math.floor(
62+
Math.random() * ((args.max ?? 100) - (args.min ?? 0)) +
63+
(args.min ?? 0),
64+
),
65+
}),
66+
},
67+
},
68+
});
69+
70+
const bash = new Bash({
71+
executionLimits: { maxJsTimeoutMs: 60000 },
72+
customCommands: executor.commands,
73+
javascript: { invokeTool: executor.invokeTool },
74+
});
75+
76+
// 1. List European countries
77+
console.log("1. European countries:");
78+
let r = await bash.exec(`js-exec -c '
79+
const countries = await tools.countries.list({ continent: "EU" });
80+
console.log(countries.length + " countries in Europe");
81+
for (const c of countries.slice(0, 5)) {
82+
console.log(" " + c.emoji + " " + c.name + " — " + c.capital);
83+
}
84+
console.log(" ...");
85+
'`);
86+
console.log(r.stdout);
87+
88+
// 2. Country detail
89+
console.log("2. Country detail:");
90+
r = await bash.exec(`js-exec -c '
91+
const c = await tools.countries.get({ code: "JP" });
92+
console.log(c.emoji + " " + c.name);
93+
console.log(" Capital: " + c.capital);
94+
console.log(" Currency: " + c.currency);
95+
console.log(" Continent: " + c.continent.name);
96+
console.log(" Languages: " + c.languages.map(l => l.name).join(", "));
97+
'`);
98+
console.log(r.stdout);
99+
100+
// 3. Mix tools from different sources
101+
console.log("3. Cross-tool script:");
102+
r = await bash.exec(`js-exec -c '
103+
const ts = await tools.util.timestamp();
104+
const rand = await tools.util.random({ min: 0, max: 249 });
105+
const all = await tools.countries.list();
106+
const pick = all[rand.value];
107+
108+
console.log("Report at " + ts.ts);
109+
console.log("Random country #" + rand.value + ": " + pick.emoji + " " + pick.name);
110+
'`);
111+
console.log(r.stdout);
112+
113+
// 4. Tools + virtual filesystem
114+
console.log("4. Fetch → write to fs → read with bash:");
115+
r = await bash.exec(`js-exec -c '
116+
const fs = require("fs");
117+
const countries = await tools.countries.list({ continent: "SA" });
118+
const csv = "code,name,capital\\n" +
119+
countries.map(c => c.code + "," + c.name + "," + c.capital).join("\\n");
120+
fs.writeFileSync("/tmp/south-america.csv", csv);
121+
console.log("Wrote " + countries.length + " rows to /tmp/south-america.csv");
122+
'`);
123+
console.log(r.stdout);
124+
125+
r = await bash.exec("cat /tmp/south-america.csv | head -5");
126+
console.log(" " + r.stdout.split("\n").join("\n "));
127+
128+
// 5. Error handling
129+
console.log("5. Error handling:");
130+
r = await bash.exec(`js-exec -c '
131+
try {
132+
await tools.countries.get({ code: "NOPE" });
133+
} catch (e) {
134+
console.error("Caught: " + e.message);
135+
}
136+
console.log("Script continued after error");
137+
'`);
138+
console.log(r.stdout);
139+
if (r.stderr) console.log(" stderr: " + r.stderr);
140+
141+
console.log("Done!");

examples/executor-tools/main.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Executor Tools Examples
3+
*
4+
* Runs all examples sequentially. You can also run each individually:
5+
* npx tsx inline-tools.ts
6+
* npx tsx multi-turn-discovery.ts
7+
* npx tsx multi-api-agent.ts
8+
*/
9+
10+
const example = process.argv[2];
11+
12+
if (!example || example === "all") {
13+
console.log("╔══════════════════════════════════════════╗");
14+
console.log("║ Executor Tools — All Examples ║");
15+
console.log("╚══════════════════════════════════════════╝\n");
16+
17+
console.log("─── Example 1: Inline Tools ───────────────────────\n");
18+
await import("./inline-tools.js");
19+
20+
console.log("\n─── Example 2: SDK Discovery ──────────────────────\n");
21+
await import("./multi-turn-discovery.js");
22+
23+
console.log("\n─── Example 3: Multi-API Agent Loop ───────────────\n");
24+
await import("./multi-api-agent.js");
25+
} else if (example === "1" || example === "inline") {
26+
await import("./inline-tools.js");
27+
} else if (example === "2" || example === "discovery") {
28+
await import("./multi-turn-discovery.js");
29+
} else if (example === "3" || example === "multi-api") {
30+
await import("./multi-api-agent.js");
31+
} else {
32+
console.error(`Unknown example: ${example}`);
33+
console.error(
34+
"Usage: npx tsx main.ts [all|1|2|3|inline|discovery|multi-api]",
35+
);
36+
process.exit(1);
37+
}

0 commit comments

Comments
 (0)