Skip to content
Merged
57 changes: 2 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,40 +37,14 @@ make build
./bin/ana --version
```

### Windows SmartScreen

Official release binaries are Authenticode-signed via Azure Trusted Signing
(see the `sign-windows` job in `.github/workflows/release.yml`), so
SmartScreen should not prompt when running an `ana.exe` downloaded from
GitHub Releases.

Binaries built from source (`go install` / `make build`) are **not** signed
and SmartScreen may still block them on first run with "Windows protected
your PC". Click **More info → Run anyway**, or unblock the file before
running:

```powershell
Unblock-File -Path .\ana.exe
```
Windows users: see [docs/windows-smartscreen.md](docs/windows-smartscreen.md).

## Usage

```bash
ana [global flags] <command> [args]
```

Global flags:

| Flag | Description |
|------|-------------|
| `--endpoint <url>` | Override the API endpoint |
| `--token-file <path>` | Path to a bearer-token file |
| `--profile <name>` | Select a config profile |
| `--json` | Emit JSON output |
| `--version`, `-V` | Print version info |

### Getting started

```bash
ana auth login --endpoint https://app.textql.com
ana org show
Expand All @@ -80,17 +54,10 @@ ana chat send "show me last month's revenue"

Run `ana --help` or `ana <verb> --help` for command-specific flags.

Connector-create commands are structured as `dialect auth-mode` subcommands
(e.g. `ana connector create postgres password --name prod --host … --user …
--database … --password-stdin`) so new dialects and auth modes land as
additions rather than growing a conditional flag matrix.

## Configuration

`ana` stores tokens and per-profile endpoints at
`$XDG_CONFIG_HOME/ana/config.json` (falling back to
`~/.config/ana/config.json`). Override with `--token-file` or the
`ANA_TOKEN_FILE` environment variable.
`$XDG_CONFIG_HOME/ana/config.json` (falling back to `~/.config/ana/config.json`).

## Development

Expand All @@ -102,26 +69,6 @@ make build # -> ./bin/ana
make release-local # goreleaser check + snapshot (requires goreleaser)
```

Conventional commits drive the release pipeline: a `feat:` or `fix:` landing
on `main` causes [release-please](https://github.com/googleapis/release-please)
to open a PR; merging that PR tags the release and triggers GoReleaser to
publish binaries, archives, checksums, and SBOMs to GitHub Releases.

### CI scope

PRs are gated by a single required check, `CI Complete`. To keep runner time
proportional to impact, docs-only PRs skip the Go lint / test / build /
goreleaser jobs and `CI Complete` reports green immediately. A PR counts as
"code" when it touches any of:

- `**/*.go`, `go.mod`, `go.sum`
- `Makefile`, `.goreleaser.yml`, `install.sh`
- `.github/workflows/**`

Everything else — `README.md`, `LICENSE`, `docs/**`, `api-catalog/**`,
`.claude/**`, `.gitignore` — skips the heavy jobs. Release-please likewise
ignores doc-only merges on `main`.

## License

MIT — see [LICENSE](LICENSE).
11 changes: 7 additions & 4 deletions cmd/ana/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func run(args []string, stdio cli.IO, env func(string) string) error {
}
client := transport.New(resolved.Endpoint, tokenFn)

verbs := buildVerbs(client, env, cfgPath, profileName)
verbs := buildVerbs(client, env, cfgPath, profileName, resolved.Endpoint)

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
Expand All @@ -127,13 +127,16 @@ func run(args []string, stdio cli.IO, env func(string) string) error {
// client and config path. Pulled out of run() so main_test can exercise it in
// isolation. profileName is the slot that auth.Load/SaveCfg should read from
// and write into — resolved upstream so login/logout always target the same
// profile the rest of the invocation used.
func buildVerbs(client *transport.Client, env func(string) string, cfgPath, profileName string) map[string]cli.Command {
// profile the rest of the invocation used. endpoint is the resolved API base
// URL (not just the --endpoint override), so verbs whose user-facing output
// references the web app — e.g. connector OAuth success notes — can point
// self-hosted and non-prod profiles at the right place.
func buildVerbs(client *transport.Client, env func(string) string, cfgPath, profileName, endpoint string) map[string]cli.Command {
return map[string]cli.Command{
"auth": auth.New(authDeps(client, env, cfgPath, profileName)),
"profile": profile.New(profileDeps(env, cfgPath)),
"org": org.New(org.Deps{Unary: client.Unary}),
"connector": connector.New(connector.Deps{Unary: client.Unary}),
"connector": connector.New(connector.Deps{Unary: client.Unary, Endpoint: endpoint}),
"chat": chat.New(chatDeps(client)),
"dashboard": dashboard.New(dashboard.Deps{Unary: client.Unary}),
"playbook": playbook.New(playbook.Deps{Unary: client.Unary}),
Expand Down
2 changes: 1 addition & 1 deletion cmd/ana/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ func TestAuthDeps_EmptyPath_FallsBackToEnv(t *testing.T) {
func TestBuildVerbs_Shape(t *testing.T) {
t.Parallel()
client := transport.New("https://example", func(context.Context) (string, error) { return "", nil })
verbs := buildVerbs(client, func(string) string { return "" }, "", "default")
verbs := buildVerbs(client, func(string) string { return "" }, "", "default", "https://example")
want := []string{"auth", "profile", "org", "connector", "chat", "dashboard", "playbook", "ontology", "feed", "audit", "version"}
for _, v := range want {
if _, ok := verbs[v]; !ok {
Expand Down
2 changes: 2 additions & 0 deletions docs/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ Human-readable planning docs. Read these before touching `api-catalog/`.

- `features.md` — inventory of TextQL surfaces (auth, chat, connectors, dashboards, playbooks, etc.), service/endpoint counts, verified enums, and per-surface quirks. "Last verified" dated.
- `cli-readiness.md` — CLI-implementer's view of `features.md`. TL;DR, per-surface confidence table (✅/🟡/❗), enum catalog, known quirks, first-cut command shape, and prioritized follow-up probes.
- `ci-scope.md` — which file globs count as "code" and trigger the full CI matrix vs docs-only skip.
- `windows-smartscreen.md` — why signed release binaries run clean but `go install` / `make build` builds get SmartScreen-blocked, and how to unblock.
14 changes: 14 additions & 0 deletions docs/ci-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# CI scope

PRs are gated by a single required check, `CI Complete`. To keep runner time
proportional to impact, docs-only PRs skip the Go lint / test / build /
goreleaser jobs and `CI Complete` reports green immediately. A PR counts as
"code" when it touches any of:

- `**/*.go`, `go.mod`, `go.sum`
- `Makefile`, `.goreleaser.yml`, `install.sh`
- `.github/workflows/**`

Everything else — `README.md`, `LICENSE`, `docs/**`, `api-catalog/**`,
`.claude/**`, `.gitignore` — skips the heavy jobs. Release-please likewise
ignores doc-only merges on `main`.
14 changes: 7 additions & 7 deletions docs/cli-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ A pass over `docs/features.md` and `api-catalog/` to call out what is solid enou
## TL;DR

- **Auth is SOLVED.** `Authorization: Bearer <apiKeyHash>` where `apiKeyHash` is the one-time plaintext returned by `rbac.RBACService/CreateApiKey`. Verified via curl with no cookies. See `features.md#api-shape-global` → "CLI auth".
- **CRUD is solid** for: chats (create/send/stream/get/history/rename/bookmark/duplicate/delete/share), API keys (create/rotate/revoke/list), service accounts (create/delete/list), connectors (test/create/get/update/delete, Postgres dialect).
- **CRUD is solid** for: chats (create/send/stream/get/history/rename/bookmark/duplicate/delete/share), API keys (create/rotate/revoke/list), service accounts (create/delete/list), connectors (test/create/get/update/delete, Postgres + all four Snowflake auth modes shipped).
- **Readonly is solid** for: dashboards, playbooks, feed, notifications, ontology, context library, observability stats, audit log, packages, SCIM, Slack binding.
- **Not covered at all by the probe** (CLI will have to probe or ask): dashboard CRUD, playbook CRUD (schedule + active + runNow), dataset CRUD, ontology CRUD, context prompt CRUD, report CRUD, file/attachment uploads.

Expand All @@ -19,7 +19,7 @@ Confidence key: ✅ full CRUD verified · 🟡 partial / readonly verified ·
| --- | --- | --- |
| Auth (API key) | ✅ | None for personal keys. Service-account keys: untested end-to-end — we created an SA and then deleted it before creating a key scoped to it. |
| Chats | ✅ | `CreateChat.paradigm` — only `universal` observed; unknown if other paradigms exist (SQL-only? notebook?). Tool-selection flags (`sqlEnabled`, `pythonEnabled`, `webSearchEnabled`) defaults undocumented — must inspect `CreateChat` sample. `UpdateChat` only verified to touch `summary`; other mutable fields unknown. |
| Connectors | 🟡 | Three dialects fully probed: Postgres (password), Snowflake (password / key-pair / oauth-sso / oauth-individual), Databricks (access-token / client-credentials / oauth-sso / oauth-individual). See connector grid below. Remaining dialects (BigQuery, Redshift, MySQL, SQLServer, Supabase, MotherDuck, Tableau, PowerBI) still unverified. OAuth leaves ship instructional-only — callback URL hardcoded to webapp, CLI can't complete the handshake. |
| Connectors | 🟡 | Three dialects fully probed: Postgres (password), Snowflake (password / key-pair / oauth-sso / oauth-individual — all four shipped), Databricks (access-token / client-credentials / oauth-sso / oauth-individual — probed, not yet shipped). See connector grid below. Remaining dialects (BigQuery, Redshift, MySQL, SQLServer, Supabase, MotherDuck, Tableau, PowerBI) still unverified. OAuth leaves POST the full create payload in a pending-OAuth state; user completes the browser handshake at `app.textql.com` to activate. |
| Service accounts | 🟡 | Created + deleted. NOT verified: creating an API key *on* a service account (the kebab menu has "Create API Key" — would need to confirm whether that uses `CreateApiKey` with a `memberId` override or a different RPC). |
| Dashboards | 🟡 | List/get/spawn/health covered. Create/update/delete NOT probed. |
| Playbooks | 🟡 | Get/list/reports/lineage covered. Create/update/delete/run-now NOT probed. |
Expand Down Expand Up @@ -64,7 +64,7 @@ Grading: ✅ Live-tested (probe + at least one real connector created) · 🟢 I
| Dialect | password | key-pair | access-token | client-credentials | oauth-sso | oauth-individual |
| --- | --- | --- | --- | --- | --- | --- |
| Postgres | 🟢 | — | — | — | — | — |
| Snowflake | 🟦 | 🟦 | — | — | 🟦* | 🟦* |
| Snowflake | 🟢 | 🟢 | — | — | 🟢* | 🟢* |
| Databricks | — | — | 🟦 | 🟦 | 🟦* | 🟦* |
| BigQuery | ⚪ | ⚪ | ⚪ | ⚪ | ⚪ | ⚪ |
| Redshift | ⚪ | — | — | — | — | — |
Expand All @@ -75,7 +75,7 @@ Grading: ✅ Live-tested (probe + at least one real connector created) · 🟢 I
| Tableau | ⚪ | — | — | — | — | — |
| PowerBI | ⚪ | — | — | — | — | — |

*OAuth leaves ship instructional-only — redirect URI hardcoded to `app.textql.com/auth/<dialect>/callback`, CLI cannot receive the callback. Wire shape captured, but the CLI command will print a message pointing at the webapp rather than completing the handshake.
*OAuth leaves POST the full `CreateConnector` payload (including `clientId` / `clientSecret`) through the normal wire shape; the server accepts the row in a pending-OAuth state. The user then completes the browser handshake at `app.textql.com/auth/<dialect>/callback` to activate the connector — the CLI prints a note to that effect on success. Databricks OAuth leaves (not yet shipped) will follow the same pattern once probed.

Cells that show `—` are auth modes the dialect doesn't expose in the webapp's UI.

Expand All @@ -94,7 +94,7 @@ Cells that show `—` are auth modes the dialect doesn't expose in the webapp's
11. **DeleteServiceAccount cascades:** revokes every API key owned by that SA. Confirm before deleting.
12. **Connector auth discrimination differs by dialect.** Snowflake password vs key-pair both send `authStrategy="service_role"`; the server disambiguates by which credential field is populated (`password` vs `privateKey`). Databricks uses a proper nested `databricksAuth` one-of instead (`pat` / `clientCredentials` / `oauthU2m`). When building the Snowflake per-auth-mode leaves, never set both `password` and `privateKey` on the same request — behavior is undefined.
13. **Databricks OAuth wire name is `oauthU2m`, not `oauthSso`.** The UI labels the two OAuth tabs "OAuth (SSO)" and "OAuth (Individual)", but the wire uses the same `databricksAuth.oauthU2m.{clientId, clientSecret}` variant for both — only the top-level `authStrategy` differs (`oauth_sso` vs `per_member_oauth`). Sending `databricksAuth.oauthSso` returns 400 with `"databricks.databricks_auth requires pat, client_credentials, or oauth_u2m"`.
14. **OAuth leaves cannot complete the handshake from a CLI.** All four OAuth modes (Snowflake sso/individual, Databricks sso/individual) persist the connector with clientId/clientSecret only, but authenticated query access requires a browser redirect to `app.textql.com/auth/<dialect>/callback`. The CLI can't receive that callback. Ship OAuth leaves as instructional errors pointing users to the webapp, not as real creation flows.
14. **OAuth leaves create a pending row; the browser activates it.** All four OAuth modes (Snowflake sso/individual, Databricks sso/individual) persist the connector via the normal `CreateConnector` wire shape with `clientId` / `clientSecret`, and the server accepts the row in a pending-OAuth state. Authenticated query access requires the user to finish the handshake via the browser redirect at `app.textql.com/auth/<dialect>/callback`. The CLI can't receive that callback, so the shipped Snowflake OAuth leaves print a note on success pointing users at the webapp to activate. Databricks OAuth leaves will follow the same pattern once shipped.
15. **Databricks `memberAuthenticated` field.** `GetConnectors` adds a `memberAuthenticated: bool` field only for `per_member_oauth` Databricks connectors, indicating whether the viewing user has completed their personal OAuth dance. Useful for CLI status output ("⚠ you haven't authenticated this connector yet").

## Recommended CLI command shape (first cut)
Expand All @@ -111,8 +111,8 @@ ana connector list
ana connector create postgres password --name X --host ... --database ...
ana connector create snowflake password --name X --locator ABC-123 --database D --warehouse W --user U --password-stdin
ana connector create snowflake key-pair --name X --locator ABC-123 --database D --warehouse W --user U --private-key-file ./rsa.p8
ana connector create snowflake oauth-sso # prints instructional message, exits non-zero
ana connector create snowflake oauth-individual # same
ana connector create snowflake oauth-sso --name X --account ... --database D --warehouse W --client-id ... --client-secret-stdin # creates pending row; user activates at app.textql.com
ana connector create snowflake oauth-individual --name X --account ... --database D --warehouse W --client-id ... --client-secret-stdin # same
ana connector create databricks access-token --name X --host ... --http-path ... --catalog C --schema S --pat-stdin
ana connector create databricks client-credentials --name X --host ... --http-path ... --catalog C --schema S --client-id ... --client-secret-stdin
ana connector create databricks oauth-sso # instructional
Expand Down
15 changes: 15 additions & 0 deletions docs/windows-smartscreen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Windows SmartScreen

Official release binaries are Authenticode-signed via Azure Trusted Signing
(see the `sign-windows` job in `.github/workflows/release.yml`), so
SmartScreen should not prompt when running an `ana.exe` downloaded from
GitHub Releases.

Binaries built from source (`go install` / `make build`) are **not** signed
and SmartScreen may still block them on first run with "Windows protected
your PC". Click **More info → Run anyway**, or unblock the file before
running:

```powershell
Unblock-File -Path .\ana.exe
```
25 changes: 18 additions & 7 deletions internal/connector/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,23 @@ The `ana connector` verb tree: `list`, `get`, `create`, `update`, `delete`, `tes
## Files

- `connector.go` — `New`, `Deps`, service path prefix.
- `types.go` — shared wire shapes consumed by create + update: `createReq`, `updateReq`, `configEnvelope`, `postgresSpec`, `createResp`, `getConnectorResp`. Per-dialect spec types will land alongside (`types_snowflake.go`, etc.) as new dialects are probed.
- `list.go` / `get.go` — `GetConnectors` / `GetConnector` (readonly).
- `create.go` — `newCreateGroup` (dialect-selector Group) + the shared `resolvePassword` helper reused by per-dialect password leaves and `update.go`.
- `create_postgres.go` — `newPostgresCreateGroup` (Postgres dialect Group whose inheritable `Flags` closure owns `--name`/`--ssl`) and `postgresPasswordCmd` (leaf for the `password` auth mode). Implements `cli.Flagger` so `--help` enumerates own + ancestor flags. Additional Postgres auth modes (key-based, cert-based) land as sibling leaf files — no reshuffling.
- `update.go` — `UpdateConnector`. Pre-fetches the baseline so interleaved flags merge correctly (see commit `1433e01`). Still a flat leaf today; dialect-aware validation + auth-mode-swap subtree are Phase 3g work.
- `delete.go`, `test.go` (TestConnector), `tables.go` (ListConnectorTables), `examples.go` (GetExampleQueries) — remaining CRUD + diagnostic verbs.
- `types.go` — shared wire shapes for create + update.
- `types_postgres.go` — Postgres wire spec.
- `types_snowflake.go` — Snowflake wire spec.
- `list.go` / `get.go` — readonly `GetConnectors` / `GetConnector`.
- `create.go` — dialect-selector Group and the shared `resolveSecret` helper.
- `create_postgres.go` — Postgres dialect Group; sibling files add auth-mode leaves.
- `create_snowflake.go` — Snowflake dialect Group; sibling files add auth-mode leaves.
- `create_snowflake_password.go` — `snowflake password` leaf.
- `create_snowflake_keypair.go` — `snowflake keypair` leaf; reads PEM key from file.
- `create_snowflake_oauth_sso.go` — `snowflake oauth-sso` leaf.
- `create_snowflake_oauth_individual.go` — `snowflake oauth-individual` leaf.
- `update.go` — `UpdateConnector`; pre-fetches baseline to merge partial updates.
- `delete.go`, `test.go`, `tables.go`, `examples.go` — remaining CRUD + diagnostic verbs.
- `connector_test.go` — shared `fakeDeps`, `errReader`, `TestNew*`/`TestHelp*`.
- `create_postgres_password_test.go` — covers the Postgres password leaf end-to-end by dispatching through `newCreateGroup` so ancestor-flag plumbing (`--name`, `--ssl` from the Postgres Group) is exercised the same way real CLI dispatch does. `requiredArgs()` builder starts with `"postgres", "password"` because every test routes through the Group.
- `create_postgres_password_test.go` — covers the Postgres password leaf via `newCreateGroup`.
- `create_snowflake_password_test.go` — covers the Snowflake password leaf via `newCreateGroup`.
- `create_snowflake_keypair_test.go` — covers the keypair leaf including key-file edge cases.
- `create_snowflake_oauth_sso_test.go` — covers the oauth-sso leaf.
- `create_snowflake_oauth_individual_test.go` — covers the oauth-individual leaf.
- `list_test.go` / `get_test.go` / `update_test.go` / `delete_test.go` / `test_test.go` / `tables_test.go` / `examples_test.go` — one per non-create source file.
Loading
Loading