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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .trajectories/completed/2026-08/traj_2ng1fbz1wsxb.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"id": "traj_2ng1fbz1wsxb",
"version": 1,
"task": {
"title": "Address final Relayfile self-serve onboarding review feedback and merge",
"source": {
"system": "plain",
"id": "PR-438"
}
},
"status": "completed",
"startedAt": "2026-08-23T14:42:40.801Z",
"completedAt": "2026-08-23T14:45:33.103Z",
"agents": [
{
"name": "default",
"role": "lead",
"joinedAt": "2026-08-23T14:42:40.884Z"
}
],
"chapters": [
{
"id": "chap_2fufyvo2tx68",
"title": "Work",
"agentName": "default",
"startedAt": "2026-08-23T14:42:40.884Z",
"endedAt": "2026-08-23T14:45:33.103Z",
"events": [
{
"ts": 1787496160884,
"type": "decision",
"content": "Propagate the CLI login timeout into the bundled Cloud SDK and swallow asynchronous browser-launch errors: Propagate the CLI login timeout into the bundled Cloud SDK and swallow asynchronous browser-launch errors",
"raw": {
"question": "Propagate the CLI login timeout into the bundled Cloud SDK and swallow asynchronous browser-launch errors",
"chosen": "Propagate the CLI login timeout into the bundled Cloud SDK and swallow asynchronous browser-launch errors",
"alternatives": [],
"reasoning": "The wrapper deadline alone could not extend the SDK's fixed five-minute callback timer, and Node child-process spawn failures bypass try/catch unless an error listener is attached."
},
"significance": "high"
},
{
"ts": 1787496333041,
"type": "reflection",
"content": "Final review findings are addressed with one behavioral regression test; CLI tests, contract checks, Go tests, Go vet, syntax checks, and affected TypeScript package typechecks pass.",
"raw": {
"confidence": 0.95
},
"significance": "high",
"tags": [
"confidence:0.95"
]
}
]
}
],
"retrospective": {
"summary": "Propagated the configured login timeout through Relayfile's Cloud SDK boundary, handled asynchronous browser launcher failures, and added behavioral coverage for both.",
"approach": "Standard approach",
"confidence": 0.95
},
"commits": [],
"filesChanged": [],
"projectId": "/private/tmp/relayfile-438-fix.79dx8Z",
"tags": [],
"_trace": {
"startRef": "809e137cb65e6479618549eea88e5d67a96aff0e",
"endRef": "809e137cb65e6479618549eea88e5d67a96aff0e"
}
}
33 changes: 33 additions & 0 deletions .trajectories/completed/2026-08/traj_2ng1fbz1wsxb.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Trajectory: Address final Relayfile self-serve onboarding review feedback and merge

> **Status:** ✅ Completed
> **Task:** PR-438
> **Confidence:** 95%
> **Started:** August 23, 2026 at 04:42 PM
> **Completed:** August 23, 2026 at 04:45 PM

---

## Summary

Propagated the configured login timeout through Relayfile's Cloud SDK boundary, handled asynchronous browser launcher failures, and added behavioral coverage for both.

**Approach:** Standard approach

---

## Key Decisions

### Propagate the CLI login timeout into the bundled Cloud SDK and swallow asynchronous browser-launch errors
- **Chose:** Propagate the CLI login timeout into the bundled Cloud SDK and swallow asynchronous browser-launch errors
- **Reasoning:** The wrapper deadline alone could not extend the SDK's fixed five-minute callback timer, and Node child-process spawn failures bypass try/catch unless an error listener is attached.

---

## Chapters

### 1. Work
*Agent: default*

- Propagate the CLI login timeout into the bundled Cloud SDK and swallow asynchronous browser-launch errors: Propagate the CLI login timeout into the bundled Cloud SDK and swallow asynchronous browser-launch errors
- Final review findings are addressed with one behavioral regression test; CLI tests, contract checks, Go tests, Go vet, syntax checks, and affected TypeScript package typechecks pass.
9 changes: 8 additions & 1 deletion .trajectories/index.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": 1,
"lastUpdated": "2026-08-15T21:42:38.397Z",
"lastUpdated": "2026-08-23T14:45:33.239Z",
"trajectories": {
"traj_4pvrlmqfnzng": {
"title": "Review PR #278 in AgentWorkforce/relayfile",
Expand Down Expand Up @@ -190,6 +190,13 @@
"startedAt": "2026-08-15T21:38:24.295Z",
"completedAt": "2026-08-15T21:42:38.104Z",
"path": ".trajectories/completed/2026-08/traj_1pmb0dufncg0.json"
},
"traj_2ng1fbz1wsxb": {
"title": "Address final Relayfile self-serve onboarding review feedback and merge",
"status": "completed",
"startedAt": "2026-08-23T14:42:40.801Z",
"completedAt": "2026-08-23T14:45:33.103Z",
"path": ".trajectories/completed/2026-08/traj_2ng1fbz1wsxb.json"
}
}
}
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ That's the entire interface. No new SDK to learn, no MCP schemas eating your con

## Quick paths

- **Hosted integrations:** `npx relayfile setup --provider notion --workspace research-room --local-dir ./relayfile-mount`
- **Hosted integrations:** `npx relayfile@latest` — sign in, connect GitHub, and mount the current project with no prior account or CLI setup.
- **Local OSS:** run the Docker stack below, then mount `ws_demo` as a normal directory.
- **Sandbox SDK:** use `RelayfileSetup.ensureMountedWorkspace()` when your runtime already has a cloud access token.
- **Programmatic agents:** use [`@relayfile/agents`](packages/agents/README.md) for Vercel AI SDK, OpenAI Agents SDK, and LangChain, or wrap `RelayFileClient.readFile()` / `writeFile()` directly in any custom harness.
Expand Down Expand Up @@ -320,18 +320,23 @@ Hosted Agent Relay runs these pieces for you. Fully self-hosted provider-backed

If you want Notion, Slack, Linear, GitHub, or other provider-backed files without running any infrastructure, use hosted Agent Relay. Agent Relay Cloud runs the workspace, relayfile API, scoped auth, Nango OAuth, provider sync workers, and writeback workers for you.

Use the [`setting-up-relayfile` skill](https://github.com/AgentWorkforce/skills/blob/main/skills/setting-up-relayfile/SKILL.md) when an agent should set up hosted files:
From the project where your agent will work, run:

```bash
relayfile setup \
npx relayfile@latest
```

That one command opens Google sign-in, creates the account and workspace, opens GitHub OAuth, and mounts files at `./relayfile-mount`. No invite code, API token, or separate `agent-relay` installation is required. The command stays open to keep the mount synchronized and prints the exact path and starter prompt for a second terminal.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

For another provider or a custom workspace name, use the explicit setup form:

```bash
npx relayfile@latest setup \
--provider notion \
--workspace my-agent \
--local-dir ./relayfile-mount \
--no-open
--local-dir ./relayfile-mount
```

That command connects to `agentrelay.com`, creates or joins a cloud workspace, completes provider auth, waits for sync, and mounts the resulting files for the agent. The local directory is just the agent's file interface; the integration stack is hosted.

Use the OSS repo when you want to run the file server yourself. Use hosted Agent Relay when you want the whole integration path managed:

| Need | Local OSS | Hosted Agent Relay |
Expand Down
104 changes: 100 additions & 4 deletions cmd/relayfile-cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,13 @@ const (
defaultMountMode = "poll"
defaultMountInterval = 30 * time.Second
defaultEventSilenceThreshold = 24 * time.Hour
setupIntentPrintedEnv = "RELAYFILE_NPM_SETUP_INTENT_PRINTED"
minMountPollInterval = 5 * time.Second
defaultMountTimeout = 15 * time.Second
)

const setupIntent = "Relayfile setup. This signs you in, connects an integration, and prepares a local VFS mount."

var relayfileVersion = relayfileDefaultVersion

var relayIntegrationBindingsMu sync.Mutex
Expand Down Expand Up @@ -601,7 +604,12 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) error {
return nil
}
if len(args) == 0 {
return runSetup(nil, stdin, stdout)
return runSetupWithOptions(
quickStartSetupArgs(),
stdin,
stdout,
setupRunOptions{preserveExistingLocalDir: true},
)
}

switch args[0] {
Expand Down Expand Up @@ -665,6 +673,23 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) error {
}
}

func quickStartSetupArgs() []string {
workingDir, _ := os.Getwd()
return quickStartSetupArgsForDir(workingDir, time.Now())
}

func quickStartSetupArgsForDir(workingDir string, now time.Time) []string {
workspaceName := "relayfile-" + now.UTC().Format("20060102-150405")
if base := strings.TrimSpace(filepath.Base(workingDir)); base != "" && base != "." && base != string(filepath.Separator) {
workspaceName = base
Comment thread
khaliqgant marked this conversation as resolved.
}
return []string{
"--provider", "github",
"--workspace", workspaceName,
"--local-dir", "./relayfile-mount",
}
}

func wantsVersion(args []string) bool {
return len(args) == 1 && (args[0] == "--version" || args[0] == "version")
}
Expand Down Expand Up @@ -874,7 +899,7 @@ func printUsage(w io.Writer) {
fmt.Fprintln(w, `relayfile is the RelayFile CLI.

Usage:
relayfile
relayfile (hosted GitHub quickstart for the current project)
relayfile setup [--provider PROVIDER] [--backend BACKEND] [--workspace NAME] [--local-dir DIR]
relayfile login [--no-open] [--provision-messaging-only] [--api-key] [--server URL] [--token TOKEN]
relayfile logout
Expand Down Expand Up @@ -961,7 +986,15 @@ Subcommands:
observer Open the hosted file observer for a workspace`)
}

type setupRunOptions struct {
preserveExistingLocalDir bool
}

func runSetup(args []string, stdin io.Reader, stdout io.Writer) error {
return runSetupWithOptions(args, stdin, stdout, setupRunOptions{})
}

func runSetupWithOptions(args []string, stdin io.Reader, stdout io.Writer, options setupRunOptions) error {
fs := flag.NewFlagSet("setup", flag.ContinueOnError)
fs.SetOutput(io.Discard)
cloudAPIURL := fs.String("cloud-api-url", envOrDefault("RELAYFILE_CLOUD_API_URL", defaultCloudAPIURL), "Relayfile Cloud API URL")
Expand Down Expand Up @@ -999,7 +1032,7 @@ func runSetup(args []string, stdin io.Reader, stdout io.Writer) error {
cloudAPI = defaultCloudAPIURL
}

fmt.Fprintln(stdout, "Relayfile setup. This signs you in, connects an integration, and prepares a local VFS mount.")
printSetupIntent(stdout)

tokenSet, err := ensureCloudCredentials(cloudAPI, strings.TrimSpace(*cloudToken), *loginTimeout, !*noOpen, stdout)
if err != nil {
Expand Down Expand Up @@ -1048,6 +1081,7 @@ func runSetup(args []string, stdin io.Reader, stdout io.Writer) error {
localDir = "./relayfile-mount"
}
}
name, localDir = resolveSetupTarget(name, localDir, options.preserveExistingLocalDir)
absLocalDir, err := filepath.Abs(localDir)
if err != nil {
return err
Expand Down Expand Up @@ -1128,10 +1162,72 @@ func runSetup(args []string, stdin io.Reader, stdout io.Writer) error {
return nil
}

fmt.Fprintf(stdout, "Starting VFS mount at %s\n", localDir)
fmt.Fprintf(stdout, "Starting VFS mount at %s\n", absLocalDir)
fmt.Fprintln(stdout, "Keep this terminal open while Relayfile syncs. In another terminal, start your agent and give it this prompt:")
if selectedProvider != "" && selectedProvider != "none" && selectedProvider != "skip" {
fmt.Fprintf(stdout, " Use %s as the source of truth. Read LAYOUT.md first, then show me what needs attention.\n", setupAgentPromptPath(absLocalDir, selectedProvider))
} else {
fmt.Fprintf(stdout, " Use %s as our shared workspace. Read LAYOUT.md first.\n", absLocalDir)
}
return runMount(mountArgs)
}

func printSetupIntent(stdout io.Writer) {
if strings.TrimSpace(os.Getenv(setupIntentPrintedEnv)) == "1" {
return
}
fmt.Fprintln(stdout, setupIntent)
}

func resolveSetupTarget(workspaceName, requestedLocalDir string, preserveExisting bool) (string, string) {
if !preserveExisting {
return workspaceName, requestedLocalDir
}

requestedIdentity := setupTargetPathIdentity(requestedLocalDir)
existing, ok := workspaceRecordByName(workspaceName)
if !ok {
return workspaceName, requestedLocalDir
}
if existingIdentity := setupTargetPathIdentity(existing.LocalDir); existingIdentity != "" && existingIdentity == requestedIdentity {
return workspaceName, existing.LocalDir
}

// Quickstart names normally stay human-readable (the project basename).
// If another local project already claimed that name, add a stable path
// discriminator so we never mount or connect the other project's workspace.
digest := sha256.Sum256([]byte(filepath.Clean(requestedIdentity)))
digestHex := hex.EncodeToString(digest[:])
for width := 8; width <= len(digestHex); width += 4 {
candidate := workspaceName + "-" + digestHex[:width]
candidateRecord, exists := workspaceRecordByName(candidate)
if !exists {
return candidate, requestedLocalDir
}
if candidateIdentity := setupTargetPathIdentity(candidateRecord.LocalDir); candidateIdentity != "" && candidateIdentity == requestedIdentity {
return candidate, candidateRecord.LocalDir
}
}

return workspaceName + "-" + digestHex, requestedLocalDir
}

func setupTargetPathIdentity(value string) string {
absolute := absolutePathIfSet(value)
if absolute == "" {
return ""
}
canonical, err := canonicalMountStartLockRoot(absolute)
if err != nil {
return filepath.Clean(absolute)
}
return canonical
}

func setupAgentPromptPath(absLocalDir, provider string) string {
return filepath.Join(absLocalDir, mountscope.ProviderRoot(provider))
}

func ensureCloudCredentials(cloudAPIURL, explicitToken string, timeout time.Duration, shouldOpenBrowser bool, stdout io.Writer) (cloudCredentials, error) {
explicitToken = strings.TrimSpace(explicitToken)
cloudAPIURL = strings.TrimRight(strings.TrimSpace(cloudAPIURL), "/")
Expand Down
Loading
Loading