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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ todo/
fuzz-*.log
.claude/settings.local.json
.deepsec/
plans/
23 changes: 23 additions & 0 deletions examples/cjs-consumer/pnpm-lock.yaml

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

23 changes: 15 additions & 8 deletions examples/custom-command/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
buildLinkSummaryPrompt,
pickSummaryLengthForCharacters,
} from "@steipete/summarize-core/prompts";
import { defineCommand } from "just-bash";
import { decodeBytesToUtf8, defineCommand } from "just-bash";

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

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

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

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

return {
stdout: `Lines: ${lines}\nWords: ${words}\nChars: ${chars}\n`,
Expand All @@ -166,8 +169,12 @@ export const wordcountCommand = defineCommand("wordcount", async (args, ctx) =>
* Usage: reverse or pipe text to it
*/
export const reverseCommand = defineCommand("reverse", async (_args, ctx) => {
const lines = ctx.stdin.split("\n");
const reversed = lines.map((line) => line.split("").reverse().join(""));
// Decode bytes to text so reversing happens by codepoint (multibyte
// characters stay intact). `Array.from` splits on Unicode codepoints
// rather than UTF-16 code units, so emoji / surrogate pairs survive.
const text = decodeBytesToUtf8(ctx.stdin);
const lines = text.split("\n");
const reversed = lines.map((line) => Array.from(line).reverse().join(""));
return {
stdout: reversed.join("\n"),
stderr: "",
Expand Down
24 changes: 24 additions & 0 deletions examples/executor-tools/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# executor-tools-example

## 1.0.3

### Patch Changes

- Updated dependencies [[`01a4721`](https://github.com/vercel-labs/just-bash/commit/01a4721324350adea4b035b311f0b60ccdbb65ff)]:
- just-bash@3.0.1
- @just-bash/executor@1.0.2

## 1.0.2

### Patch Changes

- Updated dependencies [[`fd98df8`](https://github.com/vercel-labs/just-bash/commit/fd98df8048d658454ed0769c020594754bf6e43d)]:
- @just-bash/executor@1.0.1

## 1.0.1

### Patch Changes

- 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)]:
- just-bash@3.0.0
- @just-bash/executor@1.0.0
56 changes: 56 additions & 0 deletions examples/executor-tools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Executor Tools Examples

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.

## Run

```bash
cd examples/executor-tools
pnpm install

# Run all examples
pnpm start

# Run a specific example
npx tsx inline-tools.ts
npx tsx multi-turn-discovery.ts
npx tsx multi-api-agent.ts # default country: JP
npx tsx multi-api-agent.ts BR # override

# Or via main.ts
npx tsx main.ts 1 # inline tools
npx tsx main.ts 2 # SDK discovery
npx tsx main.ts 3 # multi-API agent loop
```

## Examples

### Example 1: Inline Tools (`inline-tools.ts`)

Defines tools directly in the `Bash` constructor — no SDK required.

1. **GraphQL tools** — Countries API queries exposed as `tools.countries.*`
2. **Utility tools** — `tools.util.timestamp()`, `tools.util.random()`
3. **Cross-tool scripts** — one js-exec script calling tools from multiple namespaces
4. **Tools + filesystem** — fetch data via tools, write to virtual fs, read with bash commands
5. **Error handling** — tool errors propagate as catchable exceptions

### Example 2: Multi-Turn Tool Discovery (`multi-turn-discovery.ts`)

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.

1. **Discover** — Agent reads `/.executor/config.json` to see registered sources
2. **Use** — Agent calls a discovered tool (`tools.countries.country({ code: "JP" })`)
3. **Filter** — Agent queries a list endpoint with filters (`tools.countries.countries()`)
4. **Chain** — Agent chains multiple tools: continents → countries per continent
5. **Persist** — Agent writes all 250 countries as CSV to the virtual filesystem

### Example 3: Multi-API Agent Loop (`multi-api-agent.ts`)

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.

1. **Parallel lookup** — One js-exec script fetches country, weather, and Wikipedia data in sequence and stashes JSON results in the virtual filesystem
2. **Bash composition** — A pure-bash heredoc reads the saved JSON via `jq` and writes a markdown report
3. **CLI surface** — The same tools are also auto-exposed as bash commands (`country lookup code=BR | jq -r .name`)

Pass a country code to override the default: `npx tsx multi-api-agent.ts US`.
141 changes: 141 additions & 0 deletions examples/executor-tools/inline-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/**
* Example 1: Inline Tools
*
* Demonstrates defining tools and calling them from sandboxed js-exec scripts.
* No @executor-js/sdk plugins required for inline tools — only the SDK itself
* is needed via @just-bash/executor's peer deps.
*
* Uses:
* - countries.trevorblades.com (GraphQL) — country data
*
* Run with: npx tsx inline-tools.ts
*/

import { createExecutor } from "@just-bash/executor";
import { Bash } from "just-bash";

const executor = await createExecutor({
tools: {
// GraphQL tool — queries countries.trevorblades.com
"countries.list": {
description: "List countries, optionally filtered by continent code",
execute: async (args?: { continent?: string }) => {
const query = args?.continent
? `query($code: String!) { countries(filter: { continent: { eq: $code } }) { code name capital emoji } }`
: `{ countries { code name capital emoji } }`;
const variables = args?.continent
? { code: args.continent }
: undefined;
const res = await fetch("https://countries.trevorblades.com/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, variables }),
});
const json = (await res.json()) as { data: { countries: unknown[] } };
return json.data.countries;
},
},

"countries.get": {
description: "Get a single country by ISO code",
execute: async (args: { code: string }) => {
const query = `query($code: ID!) { country(code: $code) { name capital currency emoji languages { name } continent { name } } }`;
const res = await fetch("https://countries.trevorblades.com/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query, variables: { code: args.code } }),
});
const json = (await res.json()) as { data: { country: unknown } };
return json.data.country;
},
},

// Simple sync tools
"util.timestamp": {
description: "Current Unix timestamp",
execute: () => ({ ts: Math.floor(Date.now() / 1000) }),
},
"util.random": {
description: "Random number between min and max",
execute: (args: { min?: number; max?: number }) => ({
value: Math.floor(
Math.random() * ((args.max ?? 100) - (args.min ?? 0)) +
(args.min ?? 0),
),
}),
},
},
});

const bash = new Bash({
executionLimits: { maxJsTimeoutMs: 60000 },
customCommands: executor.commands,
javascript: { invokeTool: executor.invokeTool },
});

// 1. List European countries
console.log("1. European countries:");
let r = await bash.exec(`js-exec -c '
const countries = await tools.countries.list({ continent: "EU" });
console.log(countries.length + " countries in Europe");
for (const c of countries.slice(0, 5)) {
console.log(" " + c.emoji + " " + c.name + " — " + c.capital);
}
console.log(" ...");
'`);
console.log(r.stdout);

// 2. Country detail
console.log("2. Country detail:");
r = await bash.exec(`js-exec -c '
const c = await tools.countries.get({ code: "JP" });
console.log(c.emoji + " " + c.name);
console.log(" Capital: " + c.capital);
console.log(" Currency: " + c.currency);
console.log(" Continent: " + c.continent.name);
console.log(" Languages: " + c.languages.map(l => l.name).join(", "));
'`);
console.log(r.stdout);

// 3. Mix tools from different sources
console.log("3. Cross-tool script:");
r = await bash.exec(`js-exec -c '
const ts = await tools.util.timestamp();
const rand = await tools.util.random({ min: 0, max: 249 });
const all = await tools.countries.list();
const pick = all[rand.value];

console.log("Report at " + ts.ts);
console.log("Random country #" + rand.value + ": " + pick.emoji + " " + pick.name);
'`);
console.log(r.stdout);

// 4. Tools + virtual filesystem
console.log("4. Fetch → write to fs → read with bash:");
r = await bash.exec(`js-exec -c '
const fs = require("fs");
const countries = await tools.countries.list({ continent: "SA" });
const csv = "code,name,capital\\n" +
countries.map(c => c.code + "," + c.name + "," + c.capital).join("\\n");
fs.writeFileSync("/tmp/south-america.csv", csv);
console.log("Wrote " + countries.length + " rows to /tmp/south-america.csv");
'`);
console.log(r.stdout);

r = await bash.exec("cat /tmp/south-america.csv | head -5");
console.log(" " + r.stdout.split("\n").join("\n "));

// 5. Error handling
console.log("5. Error handling:");
r = await bash.exec(`js-exec -c '
try {
await tools.countries.get({ code: "NOPE" });
} catch (e) {
console.error("Caught: " + e.message);
}
console.log("Script continued after error");
'`);
console.log(r.stdout);
if (r.stderr) console.log(" stderr: " + r.stderr);

console.log("Done!");
37 changes: 37 additions & 0 deletions examples/executor-tools/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Executor Tools Examples
*
* Runs all examples sequentially. You can also run each individually:
* npx tsx inline-tools.ts
* npx tsx multi-turn-discovery.ts
* npx tsx multi-api-agent.ts
*/

const example = process.argv[2];

if (!example || example === "all") {
console.log("╔══════════════════════════════════════════╗");
console.log("║ Executor Tools — All Examples ║");
console.log("╚══════════════════════════════════════════╝\n");

console.log("─── Example 1: Inline Tools ───────────────────────\n");
await import("./inline-tools.js");

console.log("\n─── Example 2: SDK Discovery ──────────────────────\n");
await import("./multi-turn-discovery.js");

console.log("\n─── Example 3: Multi-API Agent Loop ───────────────\n");
await import("./multi-api-agent.js");
} else if (example === "1" || example === "inline") {
await import("./inline-tools.js");
} else if (example === "2" || example === "discovery") {
await import("./multi-turn-discovery.js");
} else if (example === "3" || example === "multi-api") {
await import("./multi-api-agent.js");
} else {
console.error(`Unknown example: ${example}`);
console.error(
"Usage: npx tsx main.ts [all|1|2|3|inline|discovery|multi-api]",
);
process.exit(1);
}
Loading
Loading