From fc8e8767bda2f5bac75f8f2739bba8c0ec6107f6 Mon Sep 17 00:00:00 2001 From: A Ibrahim Date: Wed, 22 Apr 2026 19:00:54 +0200 Subject: [PATCH 1/6] fix: add agent-shell bin entry so npx resolves correctly npx looks for a bin matching the unscoped package name (agent-shell), not tigris-agent-shell. Added agent-shell as primary bin entry. Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 1 + src/cli.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 25756ca..bcf095a 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "bin": { + "agent-shell": "dist/cli.js", "tigris-agent-shell": "dist/cli.js" }, "exports": { diff --git a/src/cli.ts b/src/cli.ts index 6c96870..deff4dd 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -37,7 +37,7 @@ function printHelp() { process.stdout.write(`Tigris Agent Shell — A virtual bash environment with a persistent filesystem backed by Tigris object storage Usage: - tigris-agent-shell [options] + npx @tigrisdata/agent-shell [options] Options: --key Access key ID From 1e201ebc9150f571bff1fb14c44377d94f509bec Mon Sep 17 00:00:00 2001 From: A Ibrahim Date: Wed, 22 Apr 2026 19:08:58 +0200 Subject: [PATCH 2/6] fix: improve help output with categorized commands Group commands into Session, Storage, and Shell categories. Add Tigris commands (presign, snapshot, fork, forks) to help. Link to just-bash supported commands. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/repl/session.ts | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/repl/session.ts b/src/repl/session.ts index 045ea44..0b8a2bc 100644 --- a/src/repl/session.ts +++ b/src/repl/session.ts @@ -411,19 +411,25 @@ export class ReplSession { } private handleHelp(io: ReplIO): void { - io.write("Commands:\n"); - io.write(" login Login (OAuth)\n"); + io.write("Session:\n"); + io.write(" login Login with Tigris (OAuth)\n"); io.write(" configure --key --secret [--bucket ] [--endpoint ]\n"); - io.write(" mount Mount a bucket\n"); - io.write(" mount List mounts\n"); - io.write(" umount Unmount a path\n"); - io.write(" df List mounts\n"); - io.write(" flush [path] Flush to Tigris\n"); - io.write(" whoami Show current session\n"); - io.write(" logout Clear session\n"); - io.write(" clear Clear screen\n"); - io.write(" help Show this help\n"); - io.write("\nAll other commands are executed as bash.\n"); + io.write(" whoami Show current session\n"); + io.write(" logout Clear session\n"); + io.write("\nStorage:\n"); + io.write(" mount Mount a bucket\n"); + io.write(" umount Unmount a path\n"); + io.write(" df List mounts\n"); + io.write(" flush [path] Flush changes to Tigris\n"); + io.write(" presign [--expires N] [--put] Generate a presigned URL\n"); + io.write(" snapshot [--name N] [--list] Create or list snapshots\n"); + io.write(" fork [--snapshot V] Fork a bucket\n"); + io.write(" forks List forks\n"); + io.write("\nShell:\n"); + io.write(" clear Clear screen\n"); + io.write(" help Show this help\n"); + io.write("\nAll other input is executed as bash.\n"); + io.write("Supported commands: https://github.com/vercel-labs/just-bash#supported-commands\n"); } /** Whether a shell is configured and ready. */ From 89853f8d2bc8d3eda3fbeccadd78dfae1c6c5a19 Mon Sep 17 00:00:00 2001 From: A Ibrahim Date: Wed, 22 Apr 2026 20:42:50 +0200 Subject: [PATCH 3/6] fix: mount all buckets at /, improve df and help output - Mount all org buckets at / instead of just the first - Start cwd at / so ls shows all buckets - Categorize help into Session, Storage, and Shell sections - Add Tigris commands (presign, snapshot, fork, forks) to help - Dynamic column width in df for long bucket names Co-Authored-By: Claude Opus 4.6 (1M context) --- src/repl/session.ts | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/src/repl/session.ts b/src/repl/session.ts index 0b8a2bc..10b2cb0 100644 --- a/src/repl/session.ts +++ b/src/repl/session.ts @@ -115,7 +115,7 @@ export class ReplSession { } } - /** Shared: list buckets, auto-mount first, commit session. */ + /** Shared: list buckets, mount all, commit session. */ private async listAndMountBuckets( newConfig: TigrisConfig, authMethod: "access-key" | "oauth", @@ -139,26 +139,15 @@ export class ReplSession { return; } - io.write("Available buckets:\n"); - for (const name of bucketNames) { - io.write(` ${name}\n`); - } - - const first = bucketNames[0]; - if (first) { - // Create shell with the first bucket so commands get the right config - newConfig.bucket = first; - const mountPoint = `/${first}`; - const newShell = new TigrisShell(newConfig, { cwd: mountPoint }); - this.commitSession(newConfig, newShell, authMethod); - this.cwd = mountPoint; - io.write(`\nMounted ${first} at ${mountPoint}\n`); - } + const newShell = new TigrisShell(newConfig, { cwd: "/" }); + this.commitSession(newConfig, newShell, authMethod); + this.cwd = "/"; - if (bucketNames.length > 1) { - io.write("\nTo mount additional buckets:\n"); - io.write(" mount \n"); + // Mount all buckets at / + for (const name of bucketNames) { + newShell.mount(name, `/${name}`); } + io.write(`Mounted ${bucketNames.length} bucket(s) at /. Run 'df' to list them.\n`); } /** Commit a new session — replace config, shell, reset cwd. */ @@ -350,9 +339,10 @@ export class ReplSession { return; } - io.write("Bucket Mounted on\n"); + const col = Math.max("Bucket".length, ...mounts.map((m) => m.bucket.length)) + 2; + io.write(`${"Bucket".padEnd(col)}Mounted on\n`); for (const m of mounts) { - io.write(`${m.bucket.padEnd(26)}${m.mountPoint}\n`); + io.write(`${m.bucket.padEnd(col)}${m.mountPoint}\n`); } } From 0e4b2199ac636195eef2ff79a8fc2f22bda98365 Mon Sep 17 00:00:00 2001 From: A Ibrahim Date: Wed, 22 Apr 2026 20:47:40 +0200 Subject: [PATCH 4/6] docs: update interactive shell examples for mount-all behavior Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 628cfdc..b1de380 100644 --- a/README.md +++ b/README.md @@ -49,19 +49,16 @@ Authenticate with access keys: ``` $ configure --key tid_... --secret tsec_... -Available buckets: - my-bucket - shared-data +Mounted 2 bucket(s) at /. Run 'df' to list them. -Mounted my-bucket at /workspace - -$ echo "hello" > greeting.txt -$ cat greeting.txt +/ $ ls +my-bucket shared-data +/ $ cd my-bucket +/my-bucket $ echo "hello" > greeting.txt +/my-bucket $ cat greeting.txt hello -$ ls -greeting.txt -$ flush -Flushed 1 mount(s) +/my-bucket $ flush +Flushed 2 mount(s) ``` Or login with your Tigris account: @@ -74,7 +71,7 @@ Open this URL in your browser: Waiting for authorization... done! Logged in as you@example.com -Mounted my-bucket at /workspace +Mounted 2 bucket(s) at /. Run 'df' to list them. ``` You can also pass credentials as flags: From d4607d23b1db3d7724cd0edbff9744becbd93dd4 Mon Sep 17 00:00:00 2001 From: A Ibrahim Date: Thu, 23 Apr 2026 09:05:38 +0200 Subject: [PATCH 5/6] fix: resolve presign bucket from cwd, normalize double-slash paths - presign resolves bucket from current mount based on cwd - Shell passes mount resolver to presign via closure - createTigrisCommands public API unchanged - Normalize // in cwd from just-bash path resolution - Error message when not in a mounted bucket Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/index.ts | 1 + src/commands/presign.ts | 52 +++++++++++++++++++++++++++++++++++------ src/repl/session.ts | 2 +- src/shell.ts | 35 ++++++++++++++++++++++----- 4 files changed, 76 insertions(+), 14 deletions(-) diff --git a/src/commands/index.ts b/src/commands/index.ts index 4241f30..8963386 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -17,5 +17,6 @@ export function createTigrisCommands(config: TigrisConfig): Command[] { } export { createForkCommand, createForksListCommand } from "./fork.js"; +export type { PresignOptions } from "./presign.js"; export { createPresignCommand } from "./presign.js"; export { createSnapshotCommand } from "./snapshot.js"; diff --git a/src/commands/presign.ts b/src/commands/presign.ts index 32fc2ac..37b89ec 100644 --- a/src/commands/presign.ts +++ b/src/commands/presign.ts @@ -27,10 +27,35 @@ function parsePresignArgs(args: string[]): { return { expiresIn, operation }; } -export function createPresignCommand(config: TigrisConfig) { - return defineCommand("presign", async (args) => { - const path = args[0]; - if (!path) { +/** Resolve an absolute path to { bucket, key } using mount lookup. */ +function resolvePath( + absolutePath: string, + configBucket: string | undefined, + resolveBucket?: (path: string) => { bucket: string; key: string } | null, +): { bucket: string; key: string } | null { + // Static bucket from config — strip leading slash for key + if (configBucket) { + const key = absolutePath.startsWith("/") ? absolutePath.slice(1) : absolutePath; + return { bucket: configBucket, key }; + } + + // Dynamic resolution from mounts + if (resolveBucket) { + return resolveBucket(absolutePath); + } + + return null; +} + +export interface PresignOptions { + /** Resolve an absolute path to bucket + key from the mount table. */ + resolveBucket?: (path: string) => { bucket: string; key: string } | null; +} + +export function createPresignCommand(config: TigrisConfig, options?: PresignOptions) { + return defineCommand("presign", async (args, ctx) => { + const rawPath = args[0]; + if (!rawPath) { return { stdout: "", stderr: "presign: missing path argument\nUsage: presign [--expires N] [--put]\n", @@ -46,12 +71,25 @@ export function createPresignCommand(config: TigrisConfig) { }; } + // Resolve relative paths against cwd + const absolutePath = rawPath.startsWith("/") + ? rawPath + : `${ctx.cwd.replace(/\/$/, "")}/${rawPath}`; + + const resolved = resolvePath(absolutePath, config.bucket, options?.resolveBucket); + if (!resolved) { + return { + stdout: "", + stderr: "presign: cannot determine bucket. cd into a mounted bucket first.\n", + exitCode: 1, + }; + } + const { expiresIn, operation } = parsePresignArgs(args.slice(1)); - const key = path.startsWith("/") ? path.slice(1) : path; - const result = await getPresignedUrl(key, { + const result = await getPresignedUrl(resolved.key, { operation, expiresIn, - config, + config: { ...config, bucket: resolved.bucket }, }); if ("error" in result) { diff --git a/src/repl/session.ts b/src/repl/session.ts index 10b2cb0..d641806 100644 --- a/src/repl/session.ts +++ b/src/repl/session.ts @@ -81,7 +81,7 @@ export class ReplSession { }); if (result.env?.PWD) { - this.cwd = result.env.PWD; + this.cwd = result.env.PWD.replace(/\/\/+/g, "/"); } if (result.stdout) io.write(result.stdout); diff --git a/src/shell.ts b/src/shell.ts index 6d328a7..ec34549 100644 --- a/src/shell.ts +++ b/src/shell.ts @@ -1,6 +1,8 @@ import type { BashExecResult } from "just-bash"; import { Bash, InMemoryFs, MountableFs } from "just-bash"; -import { createTigrisCommands } from "./commands/index.js"; +import { createForkCommand, createForksListCommand } from "./commands/fork.js"; +import { createPresignCommand } from "./commands/presign.js"; +import { createSnapshotCommand } from "./commands/snapshot.js"; import { TigrisAdapter } from "./fs/tigris-adapter.js"; import type { ShellOptions, TigrisConfig } from "./types.js"; import { validateConfig } from "./types.js"; @@ -39,15 +41,18 @@ export class TigrisShell { this.mount(config.bucket, cwd); } - const commandConfig = config.bucket - ? { ...config, bucket: config.bucket } - : { ...config, bucket: "" }; - this.bash = new Bash({ fs: this.mountableFs, cwd, ...(shellOptions?.env !== undefined && { env: shellOptions.env }), - customCommands: createTigrisCommands(commandConfig), + customCommands: [ + createPresignCommand(config, { + resolveBucket: (path) => this.resolveBucketForPath(path), + }), + createSnapshotCommand(config), + createForkCommand(config), + createForksListCommand(config), + ], }); } @@ -110,6 +115,24 @@ export class TigrisShell { } } + /** Resolve an absolute path to its bucket and object key. */ + private resolveBucketForPath(absolutePath: string): { bucket: string; key: string } | null { + // Normalize double slashes (e.g. //bucket from cd ../bucket) + const normalized = absolutePath.replace(/\/\/+/g, "/"); + // Find the longest matching mount point + let best: MountEntry | null = null; + for (const m of this.mounts) { + if (normalized === m.mountPoint || normalized.startsWith(`${m.mountPoint}/`)) { + if (!best || m.mountPoint.length > best.mountPoint.length) { + best = m; + } + } + } + if (!best) return null; + const key = normalized.slice(best.mountPoint.length + 1); // strip mount + "/" + return { bucket: best.bucket, key }; + } + /** Access the underlying just-bash instance. */ get engine(): Bash { return this.bash; From bdcf62ebb9083e127630be333fa941417a9b25dc Mon Sep 17 00:00:00 2001 From: A Ibrahim Date: Thu, 23 Apr 2026 09:22:14 +0200 Subject: [PATCH 6/6] fix: presign key resolution in single-bucket mode In single-bucket mode, use rawPath directly as the object key instead of resolving against cwd which prepends the mount prefix. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/presign.ts | 38 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 26 deletions(-) diff --git a/src/commands/presign.ts b/src/commands/presign.ts index 37b89ec..e4e5672 100644 --- a/src/commands/presign.ts +++ b/src/commands/presign.ts @@ -27,26 +27,6 @@ function parsePresignArgs(args: string[]): { return { expiresIn, operation }; } -/** Resolve an absolute path to { bucket, key } using mount lookup. */ -function resolvePath( - absolutePath: string, - configBucket: string | undefined, - resolveBucket?: (path: string) => { bucket: string; key: string } | null, -): { bucket: string; key: string } | null { - // Static bucket from config — strip leading slash for key - if (configBucket) { - const key = absolutePath.startsWith("/") ? absolutePath.slice(1) : absolutePath; - return { bucket: configBucket, key }; - } - - // Dynamic resolution from mounts - if (resolveBucket) { - return resolveBucket(absolutePath); - } - - return null; -} - export interface PresignOptions { /** Resolve an absolute path to bucket + key from the mount table. */ resolveBucket?: (path: string) => { bucket: string; key: string } | null; @@ -71,12 +51,18 @@ export function createPresignCommand(config: TigrisConfig, options?: PresignOpti }; } - // Resolve relative paths against cwd - const absolutePath = rawPath.startsWith("/") - ? rawPath - : `${ctx.cwd.replace(/\/$/, "")}/${rawPath}`; - - const resolved = resolvePath(absolutePath, config.bucket, options?.resolveBucket); + let resolved: { bucket: string; key: string } | null; + if (config.bucket) { + // Single-bucket mode — rawPath is relative to the bucket root + const key = rawPath.startsWith("/") ? rawPath.slice(1) : rawPath; + resolved = { bucket: config.bucket, key }; + } else { + // Multi-bucket — resolve against cwd to find the mount + const absolutePath = rawPath.startsWith("/") + ? rawPath + : `${ctx.cwd.replace(/\/$/, "")}/${rawPath}`; + resolved = options?.resolveBucket?.(absolutePath) ?? null; + } if (!resolved) { return { stdout: "",