Skip to content
Closed
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
45 changes: 43 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ src/
│ ├── alert/ # Alert commands (list, view)
│ ├── auth/ # Auth commands (configure, login, logout, status)
│ ├── cli/ # CLI management (install, uninstall, upgrade)
│ ├── content/ # Content pack management
│ ├── content/ # Content pack management (experimental: gated + hidden)
│ │ ├── host/ # Host Explorer (install, view)
│ │ ├── kubernetes/ # Kubernetes Explorer (install, view)
│ │ └── tracing/ # Trace Explorer (install, view)
│ ├── dataset/ # Dataset commands (list, view)
│ ├── datastream/ # Datastream commands (create, list, view, update)
│ ├── ingest-token/ # Ingest token commands (create, list, view, update)
│ ├── ingest-token/ # Ingest token commands (experimental: gated + hidden)
│ ├── metric/ # Metric commands (list, view)
│ ├── skill/ # AI agent skill commands (list, view)
│ ├── tag-key/ # Tag key commands (list)
Expand Down Expand Up @@ -78,6 +78,7 @@ src/
├── cel.ts # CEL expression support
├── kg-search.ts # Knowledge graph search
├── writer.ts # Output writer
├── experimental.ts # Experimental command gating (env flag, badge, hide)
└── format-error.ts # Error formatting
```

Expand All @@ -101,6 +102,46 @@ src/
- Update the **Project Structure** section in this `AGENTS.md` to include the new command/resource.
- Update the **Commands** table in `README.md`. The README command order must always match the route order in `src/app.ts`.

### Adding an Experimental Command

Experimental commands live behind `OBSERVE_CLI_EXPERIMENTAL=1`: hidden from help
and non-runnable unless the flag is set, tagged `[experimental]` when visible.
All helpers come from [`src/lib/experimental.ts`](src/lib/experimental.ts).

A developer marks a command experimental by touching three things:

1. **Gate the handler** — wrap the loader so it refuses to run without the flag
(see [`data-connection/generate-stack-url.ts`](src/commands/data-connection/generate-stack-url.ts)):

```typescript
import { gateExperimental } from "../../lib/experimental";
loader: async () => gateExperimental(myHandler),
```

2. **Badge the brief** — so it reads `[experimental] …` in help:

```typescript
import { withExperimentalBadge } from "../../lib/experimental";
docs: { brief: withExperimentalBadge("Do the experimental thing") },
```

3. **Hide it in the parent route map** — a command cannot hide itself, so list
it in the parent's `docs.hideRoute`. For a subcommand, hide it in its group's
route map; for a top-level command, hide it in `src/app.ts`:

```typescript
import { hideExperimentalRoutes } from "../../lib/experimental";
docs: { brief: "…", hideRoute: hideExperimentalRoutes(["myCommand"]) },
```

**Marking a whole group experimental** (see the `content` group): badge the
group's brief in its `index.ts`, hide the group name in the parent map's
`hideRoute`, and — because hiding does not block execution — wrap the loader of
_every leaf command_ in the group with `gateExperimental`.

**Promote to GA** by deleting those markers — drop `gateExperimental`, drop
`withExperimentalBadge`, and remove the name from `hideExperimentalRoutes`.

### Command Pattern

Commands follow this structure:
Expand Down
31 changes: 12 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ Command line interface for [Observe Inc](https://www.observeinc.com).
- **OPAL Query Execution** - Run OPAL queries directly from your terminal with schema-aware table output.
- **AI Agent Skills** - List and view reusable AI-agent instruction documents stored in Observe.
- **Alert Monitoring** - List and view alerts with severity filtering and active-only views.
- **Content Packs** - Install and view Host Explorer, Kubernetes Explorer, and Trace Explorer content.
- **Ingest Token Management** - Full CRUD for ingest tokens with datastream association.
- **Data Integrations** - Create data connections and datasources (AWS, Kubernetes, host) and generate CloudFormation quick-create URLs for AWS filedrop deployments.
- **Datastream Management** - Create, list, view, and update datastreams.
- **Multiple Output Formats** - All commands support `--format json` and `--format csv` for scripting and pipelines.
- **Responsive Tables** - Terminal-aware column widths with automatic text wrapping.
Expand Down Expand Up @@ -53,22 +50,6 @@ To update installed skills after edits in this repo, run `npx skills update`.
| `observe skill view` | View skill details and content |
| `observe alert list` | List alerts with severity and status filtering |
| `observe alert view` | View full alert details |
| `observe content host install` | Install Host Explorer content |
| `observe content host view` | View Host Explorer content |
| `observe content kubernetes install` | Install Kubernetes Explorer content |
| `observe content kubernetes view` | View Kubernetes Explorer content |
| `observe content tracing install` | Install Trace Explorer content |
| `observe content tracing view` | View Trace Explorer content |
| `observe ingest-token create` | Create a new ingest token |
| `observe ingest-token list` | List and search ingest tokens |
| `observe ingest-token view` | View an ingest token by ID |
| `observe ingest-token update` | Update an ingest token |
| `observe datasource create` | Create a datasource attached to a data connection |
| `observe datasource update` | Update an existing datasource's config |
| `observe datasource generate-stack-url` | Build a CloudFormation quick-create URL for a filedrop |
| `observe data-connection create` | Create a data connection (AWS, kubernetes, host, etc.) |
| `observe data-connection list` | List data connections |
| `observe data-connection view` | View a data connection by ID (with its datasources) |
| `observe datastream create` | Create a new datastream |
| `observe datastream list` | List datastreams |
| `observe datastream view` | View a datastream by ID |
Expand All @@ -78,6 +59,18 @@ To update installed skills after edits in this repo, run `npx skills update`.
| `observe cli uninstall` | Remove shell integration |
| `observe cli upgrade` | Upgrade to the latest version |

### Experimental commands

Experimental commands are hidden by default and gated behind an environment
variable. They are **not** covered by SemVer — their names, flags, and output
may change or be removed without notice.

```bash
# Enable experimental commands for the session
export OBSERVE_CLI_EXPERIMENTAL=1
observe help # experimental commands now appear, tagged [experimental]
```

## Configuration

Credentials are stored in `~/.observe/config.json` with mode `600` (owner-only access). Permissions are automatically enforced on every write.
Expand Down
8 changes: 8 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { skillRoutes } from "./commands/skill/index.js";
import { tagKeyRoutes } from "./commands/tag-key/index.js";
import { tagValueRoutes } from "./commands/tag-value/index.js";
import { CURRENT_CLI_VERSION } from "./lib/constants.js";
import { hideExperimentalRoutes } from "./lib/experimental.js";

/** Top-level route map containing all CLI commands */
export const routes = buildRouteMap({
Expand Down Expand Up @@ -44,6 +45,13 @@ export const routes = buildRouteMap({
fullDescription:
"observe is a command-line interface for interacting with Observe Inc. " +
"It provides commands for configuration, querying datasets, and more.",
// Hide experimental command groups unless OBSERVE_CLI_EXPERIMENTAL=1.
hideRoute: hideExperimentalRoutes([
"content",
"data-connection",
"ingest-token",
"datasource",
]),
},
});

Expand Down
3 changes: 2 additions & 1 deletion src/commands/content/host/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { buildRouteMap } from "@stricli/core";
import { withExperimentalBadge } from "../../../lib/experimental";
import { installCommand } from "./install";
import { viewCommand } from "./view";

Expand All @@ -8,6 +9,6 @@ export const hostContentRoutes = buildRouteMap({
view: viewCommand,
},
docs: {
brief: "Manage Host Explorer content",
brief: withExperimentalBadge("Manage Host Explorer content"),
},
});
9 changes: 7 additions & 2 deletions src/commands/content/host/install.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { buildCommand } from "@stricli/core";
import {
gateExperimental,
withExperimentalBadge,
} from "../../../lib/experimental";
import type { LocalContext } from "../../../context";
import { updateHostContent } from "../../../gql/content/update-host-content";
import { GqlApiError } from "../../../gql/gql-request";
Expand Down Expand Up @@ -42,8 +46,9 @@ export async function install(
}
}

// EXPERIMENTAL
export const installCommand = buildCommand({
loader: async () => install,
loader: async () => gateExperimental(install),
parameters: {
positional: {
kind: "tuple",
Expand All @@ -65,7 +70,7 @@ export const installCommand = buildCommand({
},
},
docs: {
brief: "Install or update Host Explorer content",
brief: withExperimentalBadge("Install or update Host Explorer content"),
fullDescription: [
"Install or update Host Explorer content. This creates the derived host logs",
"content used by the Host Explorer UI.",
Expand Down
9 changes: 7 additions & 2 deletions src/commands/content/host/view.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { buildCommand } from "@stricli/core";
import {
gateExperimental,
withExperimentalBadge,
} from "../../../lib/experimental";
import type { LocalContext } from "../../../context";
import { getHostContent } from "../../../gql/content/view-host-content";
import { GqlApiError } from "../../../gql/gql-request";
Expand Down Expand Up @@ -36,8 +40,9 @@ export async function view(
}
}

// EXPERIMENTAL
export const viewCommand = buildCommand({
loader: async () => view,
loader: async () => gateExperimental(view),
parameters: {
positional: {
kind: "tuple",
Expand All @@ -46,6 +51,6 @@ export const viewCommand = buildCommand({
flags: {},
},
docs: {
brief: "View current Host Explorer content status",
brief: withExperimentalBadge("View current Host Explorer content status"),
},
});
13 changes: 4 additions & 9 deletions src/commands/content/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { buildRouteMap } from "@stricli/core";
import { withExperimentalBadge } from "../../lib/experimental";
import { hostContentRoutes } from "./host/index";
import { kubernetesContentRoutes } from "./kubernetes/index";
import { tracingContentRoutes } from "./tracing/index";
Expand All @@ -10,14 +11,8 @@ export const contentRoutes = buildRouteMap({
tracing: tracingContentRoutes,
},
docs: {
brief: "Manage installed content",
fullDescription: [
"Install and view content packs in Observe.",
"",
"Commands:",
" host Manage Host Explorer content",
" kubernetes Manage Kubernetes Explorer content",
" tracing Manage Trace Explorer content",
].join("\n"),
// EXPERIMENTAL
brief: withExperimentalBadge("Manage installed content"),
fullDescription: "Install and view content packs in Observe.",
},
});
3 changes: 2 additions & 1 deletion src/commands/content/kubernetes/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { buildRouteMap } from "@stricli/core";
import { withExperimentalBadge } from "../../../lib/experimental";
import { installCommand } from "./install";
import { viewCommand } from "./view";

Expand All @@ -8,6 +9,6 @@ export const kubernetesContentRoutes = buildRouteMap({
view: viewCommand,
},
docs: {
brief: "Manage Kubernetes Explorer content",
brief: withExperimentalBadge("Manage Kubernetes Explorer content"),
},
});
9 changes: 7 additions & 2 deletions src/commands/content/kubernetes/install.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { buildCommand } from "@stricli/core";
import {
gateExperimental,
withExperimentalBadge,
} from "../../../lib/experimental";
import type { LocalContext } from "../../../context";
import { updateKubernetesContent } from "../../../gql/content/update-kubernetes-content";
import { GqlApiError } from "../../../gql/gql-request";
Expand Down Expand Up @@ -51,8 +55,9 @@ export async function install(
}
}

// EXPERIMENTAL
export const installCommand = buildCommand({
loader: async () => install,
loader: async () => gateExperimental(install),
parameters: {
positional: {
kind: "tuple",
Expand Down Expand Up @@ -87,7 +92,7 @@ export const installCommand = buildCommand({
},
},
docs: {
brief: "Install or update Kubernetes content",
brief: withExperimentalBadge("Install or update Kubernetes content"),
fullDescription:
"Install or update Kubernetes Explorer content. This creates correlation tags,\n" +
"RBAC rules, and derived datasets for Kubernetes observability.\n\n" +
Expand Down
9 changes: 7 additions & 2 deletions src/commands/content/kubernetes/view.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { buildCommand } from "@stricli/core";
import {
gateExperimental,
withExperimentalBadge,
} from "../../../lib/experimental";
import type { LocalContext } from "../../../context";
import { getKubernetesContent } from "../../../gql/content/view-kubernetes-content";
import { GqlApiError } from "../../../gql/gql-request";
Expand Down Expand Up @@ -36,8 +40,9 @@ export async function view(
}
}

// EXPERIMENTAL
export const viewCommand = buildCommand({
loader: async () => view,
loader: async () => gateExperimental(view),
parameters: {
positional: {
kind: "tuple",
Expand All @@ -46,6 +51,6 @@ export const viewCommand = buildCommand({
flags: {},
},
docs: {
brief: "View current Kubernetes content status",
brief: withExperimentalBadge("View current Kubernetes content status"),
},
});
3 changes: 2 additions & 1 deletion src/commands/content/tracing/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { buildRouteMap } from "@stricli/core";
import { withExperimentalBadge } from "../../../lib/experimental";
import { installCommand } from "./install";
import { viewCommand } from "./view";

Expand All @@ -8,6 +9,6 @@ export const tracingContentRoutes = buildRouteMap({
view: viewCommand,
},
docs: {
brief: "Manage Trace Explorer content",
brief: withExperimentalBadge("Manage Trace Explorer content"),
},
});
9 changes: 7 additions & 2 deletions src/commands/content/tracing/install.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { buildCommand } from "@stricli/core";
import {
gateExperimental,
withExperimentalBadge,
} from "../../../lib/experimental";
import type { LocalContext } from "../../../context";
import { installTracingContent } from "../../../gql/content/install-tracing-content";
import { GqlApiError } from "../../../gql/gql-request";
Expand Down Expand Up @@ -55,8 +59,9 @@ export async function install(
}
}

// EXPERIMENTAL
export const installCommand = buildCommand({
loader: async () => install,
loader: async () => gateExperimental(install),
parameters: {
positional: {
kind: "tuple",
Expand Down Expand Up @@ -90,7 +95,7 @@ export const installCommand = buildCommand({
},
},
docs: {
brief: "Install Trace Explorer content",
brief: withExperimentalBadge("Install Trace Explorer content"),
fullDescription:
"Install Trace Explorer content. If dataset IDs are provided, uses those\n" +
"specific datasets; otherwise auto-discovers from existing datastreams.\n\n" +
Expand Down
9 changes: 7 additions & 2 deletions src/commands/content/tracing/view.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { buildCommand } from "@stricli/core";
import {
gateExperimental,
withExperimentalBadge,
} from "../../../lib/experimental";
import type { LocalContext } from "../../../context";
import { getTracingContent } from "../../../gql/content/view-tracing-content";
import { GqlApiError } from "../../../gql/gql-request";
Expand Down Expand Up @@ -36,8 +40,9 @@ export async function view(
}
}

// EXPERIMENTAL
export const viewCommand = buildCommand({
loader: async () => view,
loader: async () => gateExperimental(view),
parameters: {
positional: {
kind: "tuple",
Expand All @@ -46,6 +51,6 @@ export const viewCommand = buildCommand({
flags: {},
},
docs: {
brief: "View current Trace Explorer content status",
brief: withExperimentalBadge("View current Trace Explorer content status"),
},
});
Loading