# graphiti

Progressive GraphQL query builder CLI for Salesforce orgs. Treats a query as a **projection of the schema** — navigate the schema like a filesystem, select leaf fields, set arguments, and compose queries without ever writing raw GraphQL.

## Install

```bash
npm install
npm run build
```

Run via `node dist/cli.js` (built) or `npx tsx src/cli.ts` (dev).

## Prerequisites

- [Salesforce CLI](https://developer.salesforce.com/tools/salesforcecli) installed and authenticated (`sf org login web --alias myorg`)

## Quick Start

```bash
# 1. Connect to an org (downloads GraphQL schema once)
graphiti connect myorg

# 2. Start a new query session
graphiti query new myorg
# → prints Session: s_xxxx and root fields

# 3. Navigate the schema like a directory
graphiti query s_xxxx ls                       # list available fields
graphiti query s_xxxx ls -l                    # long listing (type, args, selections)
graphiti query s_xxxx ls --search Account      # filter by name
graphiti query s_xxxx cd accounts             # enter a field directory
graphiti query s_xxxx cd accounts/edges/node  # multi-level nav
graphiti query s_xxxx pwd                      # print current path
graphiti query s_xxxx cd ..                   # go up one level
graphiti query s_xxxx cd /                    # go back to root

# 4. Select leaf fields (parent path added automatically)
graphiti query s_xxxx select Id
graphiti query s_xxxx select Name --as accountName

# 5. Remove a selection
graphiti query s_xxxx rm Id

# 6. Navigate union/interface fragment directories
graphiti query s_xxxx cd search               # field of union type
graphiti query s_xxxx cd [Account]            # inline fragment directory
graphiti query s_xxxx select name

# 7. Set field arguments
graphiti query s_xxxx cd accounts
graphiti query s_xxxx args set first 10
graphiti query s_xxxx args set where '{"Status":{"eq":"Open"}}'
graphiti query s_xxxx args ls
graphiti query s_xxxx args rm first

# 8. Bind arguments to query variables
graphiti query s_xxxx vars set \$whereInput AccountWhere
graphiti query s_xxxx bind set where \$whereInput

# 9. Set directory-scoped default argument values
graphiti query s_xxxx defaults set first 20

# 10. Preview and execute
graphiti query s_xxxx show
graphiti query s_xxxx validate
graphiti query s_xxxx execute
```

## Interactive Mode

Start an interactive REPL session for faster iteration — same commands, no need to repeat the session ID each time. Bare commands like `select`, `cd`, and `args set` show guided pick-lists.

```bash
graphiti query s_xxxx interactive
```

REPL prompt: `graphiti [s_xxxx] /accounts > `

Inside the REPL:

- Type any subcommand (`ls`, `cd edges`, `select Id`, etc.) without the `graphiti query s_xxxx` prefix.
- `select` with no argument → guided pick-list of available leaf fields.
- `cd` with no argument → guided pick-list of navigable directories.
- `args set` with no argument → guided pick-list of available field arguments.
- `vars set` with just `set` → guided prompts for name, type, and default value.
- `exit` or `Ctrl-C` to quit.

## Commands Reference

### Setup

| Command                                     | Description                                                            |
| ------------------------------------------- | ---------------------------------------------------------------------- |
| `graphiti connect <org>`                    | Connect to a Salesforce org and download its GraphQL schema            |
| `graphiti connect <org> --refresh`          | Re-download the schema and clear all caches (after deploying metadata) |
| `graphiti type <org> <TypeName>`            | Inspect a named GraphQL type                                           |
| `graphiti validate <org> <TypeName> <json>` | Validate a JSON value against an input type                            |

### Session lifecycle

| Command                                               | Description                                        |
| ----------------------------------------------------- | -------------------------------------------------- |
| `graphiti query new <org> [--mutation] [--aggregate]` | Create a new query, mutation, or aggregate session |
| `graphiti query help [topic]`                         | Show command help, optionally scoped to a topic    |

### Navigation

| Command                                       | Description                                                 |
| --------------------------------------------- | ----------------------------------------------------------- |
| `graphiti query <sid> pwd`                    | Print current schema navigation path                        |
| `graphiti query <sid> ls [-l] [--search pat]` | List schema entries at current path                         |
| `graphiti query <sid> cd <path>`              | Navigate to a schema path (supports `..`, `/`, multi-level) |

### Projection

| Command                                           | Description                              |
| ------------------------------------------------- | ---------------------------------------- |
| `graphiti query <sid> select <leaf> [--as alias]` | Select a leaf field (parents auto-added) |
| `graphiti query <sid> rm <field-or-alias>`        | Remove a leaf selection                  |

### Multiple instances & aliases

| Command                                        | Description                                    |
| ---------------------------------------------- | ---------------------------------------------- |
| `graphiti query <sid> alias ls`                | List all projection instances at current path  |
| `graphiti query <sid> alias set <name>`        | Set alias on the focused instance              |
| `graphiti query <sid> alias new <name>`        | Create a new projection instance with an alias |
| `graphiti query <sid> alias use <alias-or-id>` | Switch focus to a different instance           |
| `graphiti query <sid> alias clear`             | Remove alias from the focused instance         |

Use `alias new` to select the same field multiple times with different arguments (e.g., two aliased versions of `accounts` with different `where` clauses).

### Arguments

| Command                                        | Description                                  |
| ---------------------------------------------- | -------------------------------------------- |
| `graphiti query <sid> args ls`                 | List args on the focused projection instance |
| `graphiti query <sid> args set <name> <value>` | Set an argument value                        |
| `graphiti query <sid> args rm <name>`          | Remove an argument                           |

### Variable bindings

| Command                                      | Description                                    |
| -------------------------------------------- | ---------------------------------------------- |
| `graphiti query <sid> bind ls`               | List variable bindings on the focused instance |
| `graphiti query <sid> bind set <arg> <$var>` | Bind an argument to a query variable           |
| `graphiti query <sid> bind rm <arg>`         | Remove a binding                               |

### Directory-scoped defaults

Default argument values applied to all projection instances under the current path unless an instance overrides them.

| Command                                            | Description                   |
| -------------------------------------------------- | ----------------------------- |
| `graphiti query <sid> defaults ls`                 | List defaults at current path |
| `graphiti query <sid> defaults set <name> <value>` | Set a default                 |
| `graphiti query <sid> defaults rm <name>`          | Remove a default              |

### Variables

| Command                                                  | Description              |
| -------------------------------------------------------- | ------------------------ |
| `graphiti query <sid> vars ls`                           | List all query variables |
| `graphiti query <sid> vars set <$name> <type> [default]` | Define a variable        |
| `graphiti query <sid> vars value <$name> <value>`        | Set the runtime value    |
| `graphiti query <sid> vars rm <$name>`                   | Remove a variable        |

### Query output

| Command                         | Description                                  |
| ------------------------------- | -------------------------------------------- |
| `graphiti query <sid> show`     | Print the current GraphQL query string       |
| `graphiti query <sid> validate` | Validate the query against the schema        |
| `graphiti query <sid> execute`  | Execute the query against the Salesforce org |

### Interactive

| Command                            | Description                       |
| ---------------------------------- | --------------------------------- |
| `graphiti query <sid> interactive` | Start an interactive REPL session |

## Performance

Schema and auth data are cached aggressively to minimize cold-start overhead:

- **Auth memoization** — `sf org display` is called at most once per process; the result is cached for the lifetime of the command or interactive session.
- **Session-stored instanceUrl** — The Salesforce instance URL is resolved once at `query new` time and persisted in the session file. Subsequent commands use this URL to locate the cached schema file directly, completely bypassing `sf org display`.
- **SDL schema cache** — After a schema is first built from the introspection JSON, it is serialized to a compact SDL file (`~/.graphiti/schemas/<cacheKey>.graphql`) next to the introspection JSON. Subsequent cold starts load the SDL file instead, which is faster than re-processing the raw introspection format.
- **In-process schema cache** — Within a single interactive session, the parsed `GraphQLSchema` is kept in memory so every command after the first is near-instant.

## Schema storage

Schemas are stored in `~/.graphiti/schemas/` keyed by a hash of the normalized Salesforce instance URL. This means:

- Reusing an org alias for a different org will always pick up the correct schema.
- The companion SDL file (`<cacheKey>.graphql`) is regenerated automatically whenever the introspection JSON is updated.

### Refreshing after a deploy

graphiti caches three things per org: the on-disk introspection JSON, the in-memory parsed schema, and an ObjectInfo cache (picklists, filterable/sortable flags, required-on-create; 1-hour TTL on disk). When you deploy new metadata (fields, picklist values, objects), run a refresh to invalidate **all three** coherently:

- `graphiti connect <org> --refresh` re-downloads the schema and clears all three caches.
- In the MCP server, call the `sf_gql_connect` tool with `forceRefresh: true` — the long-lived server cannot otherwise pick up new metadata, since its in-memory caches outlive a single request.
- Concurrent refreshes (a CLI `--refresh` and an MCP `sf_gql_connect` firing at once) **coalesce** into a single introspection via the on-disk schema lock, so you normally never pay for the (large, multi-second) introspection twice. Coalescing is best-effort: it keys off the cache file's modification time, so on filesystems with coarse mtime resolution two refreshes landing in the same tick may each download — never incorrect, just occasionally redundant.
- If a refresh fails (network/5xx) it is retried once; on terminal failure the previously cached schema is **kept** and a staleness-aware message is surfaced — the cache stays usable.

> **ObjectInfo is keyed by org alias, not instance URL.** Refreshing alias `A` does not invalidate ObjectInfo cached under a different alias `B` that points at the same org; refresh each alias you actively use, or let ObjectInfo's 1-hour TTL heal it.

## Programmatic API (library)

graphiti is primarily a CLI, but it also exposes a small, stable programmatic surface so other tools can **share the same schema cache** instead of running their own introspection. The canonical use is a codegen or IDE pipeline that needs an org's GraphQL SDL on disk:

```ts
import { downloadSchemaSdl } from "@salesforce/graphiti";

// Prime the shared cache (if needed) and write canonical SDL for codegen.
await downloadSchemaSdl({
  org: process.env.SF_TARGET_ORG, // alias or username
  outPath: "schema.graphql",
  maxAgeMs: 10 * 60_000, // re-introspect if the cached schema is older than 10 min
});
```

`downloadSchemaSdl(options)` primes the org's schema through the same instance-URL-keyed cache the CLI and MCP server use, then serializes it with `printSchema`:

| Option         | Description                                                                                                                      |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `org`          | Org alias or username (the value you'd pass to `--target-org`).                                                                  |
| `outPath`      | Where to atomically write the SDL (parent dirs are created). Omit to only receive the SDL in the result.                         |
| `forceRefresh` | Re-download even if cached, clearing all of graphiti's caches coherently (same as `connect --refresh`).                          |
| `maxAgeMs`     | Age gate: force a refresh when the cached schema is older than this. A fresh cache — or an uncached org — is left to lazy prime. |

Because the **schema cache has no TTL** — a plain `connect` serves any existing schema indefinitely (only the separate ObjectInfo cache expires, on its own 1-hour TTL) — callers that must not run against stale metadata, such as codegen or IDE tooling, should pass a small `maxAgeMs`. Whoever asks first (CLI, MCP, or this call) pays the one-time introspection; everyone else reads the same cache, so you still "wait once."

The result reports what happened — `sdl`, `instanceUrl`, `cacheFilePath`, `outPath`, `downloaded`, `refreshed`, and `refreshedDueToAge`. `downloadSchemaSdl` is the only supported export today; deep `@salesforce/graphiti/dist/...` imports are internal and may change without a major version bump.

## Development

```bash
npm run build     # compile TypeScript
npm test          # build + run tests
```
