Skip to content
Open
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
129 changes: 129 additions & 0 deletions .claude/skills/segmentcli/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
---
name: segmentcli
description: >-
Use the `segmentcli` CLI to drive a Segment workspace — list sources, manage
Analytics Live Plugins, send test analytics events, import CSV data, and
scaffold plugin/edge-function/importer code. Use whenever the user mentions
"segmentcli", asks to upload/disable a Live Plugin, list Segment sources,
send a track/identify/etc. event from the terminal, import a CSV into
Segment, or scaffold a Segment plugin or edge function.
---

# segmentcli

CLI for working with Segment workspaces. Sources upstream:
[`segment-integrations/segmentcli`](https://github.com/segment-integrations/segmentcli).
Installs as `/usr/local/bin/segmentcli` via `sudo make install` from a checkout.

## Look things up before answering

Always prefer `--help` over guessing — the CLI is small and self-documenting:

- `segmentcli --help` — top-level groups and commands
- `segmentcli <group> --help` — subcommands of a group
- `segmentcli <group> <subcommand> --help` — exact arguments
- `segmentcli version` — version string (currently `1.0.0`)

## Authentication & profiles

State lives in `~/.segmentcli` (JSON). Profiles are workspace tokens with a
local nickname.

- **Auth** (one-time per workspace): `segmentcli auth <ProfileName> <AuthToken>`
- **List**: `segmentcli profile list`
- **Set default**: `segmentcli profile set <ProfileName>`
- **Delete**: `segmentcli profile delete <ProfileName>`
- Most commands accept `-p, --profile <name>` to override the default profile
for that single call.
- `--staging` global flag routes to `api.segmentapis.build` (Segment internal staging).

### Getting a token

1. https://app.segment.com → Settings → Workspace Settings → Access Management → Tokens
2. *Create token* with the **Workspace Owner** role (required for full CLI access).
3. Token format starts with `sgp_…`.

### EU workspaces

The Public API is single-host (`https://api.segmentapis.com`); for EU
workspaces it 30x's to `https://eu1.api.segmentapis.com`. The CLI's auth
session preserves the `Authorization` header across the redirect, so EU
auth works transparently — but only on builds that include the
"re-attach Authorization on redirect" fix. If an EU workspace returns
"Supplied token is not authorized" or "Authorization header is required",
the binary is too old; rebuild from `master`.

> Note: `eu1.api.segmentapis.com` is the EU Public API host (Public API only).
> `events.eu1.segmentapis.com` is the EU **event ingestion** host (TAPI) — used
> by analytics SDKs at runtime, NOT by `segmentcli`.

## Read-only commands (safe to run anytime)

- `segmentcli version`
- `segmentcli profile list`
- `segmentcli sources list` — lists all sources in the workspace (name, id, type, write keys)
- `segmentcli liveplugins latest <sourceId>` — info on the live plugin currently bound to a source
- `segmentcli analytics list` — show locally-pending event batches (does not contact Segment)

## Write / side-effecting commands (confirm before running)

These either mutate workspace state, send live event traffic, write to disk,
or modify local profile state. **Always echo the intended command back to the
user and confirm before running.** Don't auto-fire them.

- `segmentcli auth <Profile> <Token>` — writes a profile to `~/.segmentcli`
- `segmentcli profile set/delete` — mutates local profile state
- `segmentcli liveplugins upload <sourceId> <filePath>` — deploys a Live Plugin to a source
- `segmentcli liveplugins disable <sourceId>` — disables Live Plugins for a source
- `segmentcli analytics {track,identify,screen,group,alias} <writeKey> …` — sends real events
- `segmentcli analytics flush` — flushes locally-queued events to Segment
- `segmentcli analytics reset` — clears local anonId/userId
- `segmentcli import <writeKey> <csvFile>` — bulk-ingests CSV rows as events
- `segmentcli scaffold {-p|-e|-i} [--swift|--kotlin|--java|--objc|--js|--ts] [-n NAME]` — generates code files in CWD
- `segmentcli repl` — interactive Substrata JS REPL

## Common workflows

### Find a source's ID
1. `segmentcli sources list`
2. Match by name; record `id` field. (Or grab from app.segment.com → Connections → Sources → API Keys.)

### Ship a Live Plugin
1. Scaffold: `segmentcli scaffold --plugin --swift -n MyPlugin` (or `--kotlin`/`--java`/`--objc`/`--ts`)
2. Edit the generated file.
3. `segmentcli liveplugins upload <sourceId> <path/to/plugin.js>` (the deployed file is the JS bundle, not the platform source).
4. Verify: `segmentcli liveplugins latest <sourceId>`. Settings propagation can take a few minutes.

### Test events from the terminal
1. Find the source's write key: `segmentcli sources list`.
2. `segmentcli analytics track <writeKey> "Event Name" key=value other=val --flush`
3. `--flush` ensures the batch is sent before the process exits; without it, events sit locally until `analytics flush` is run.

### Import a CSV
1. `segmentcli analytics list` to confirm queue state.
2. `segmentcli import <writeKey> data.csv`
3. The CLI batches rows as track events (one per row). Large files take a while.

### Scaffold an edge function
- `segmentcli scaffold --edgefn --js -n MyEdgeFn` → drops a JS template in CWD.

## Guardrails

- **Confirm before any write.** Especially `liveplugins upload`, `liveplugins disable`, `analytics track/identify/...`, and `import` — they all hit production by default. Use `--staging` if available and intended.
- **Never paste a token into chat or commit it.** Tokens are workspace-scoped and grant full Workspace Owner access. The token lives in `~/.segmentcli` (mode 644 — flag if storing on a shared host).
- **Don't run `repl` non-interactively** — it expects a TTY.
- **Source IDs vs write keys** are different. Live plugin / sources commands take **source IDs** (a short opaque ID per source). Analytics and import commands take **write keys** (a separate opaque ID, also per source). Both are in `sources list` output.
- **Settings file at `~/.segmentcli`** is plain JSON; safe to inspect/back up but treat as a secret.

## When the CLI is not the right tool

- **Bulk Public API operations** (filtering, pagination, fields the CLI doesn't surface): hit `https://api.segmentapis.com` directly with `curl`. The official SDKs are at https://github.com/segmentio (Go, Python, Java, C#, TypeScript, Swift).
- **Event ingestion at scale**: use a real Segment SDK in your app, not `analytics track`.
- **Tracking Plans / Destinations / Warehouses CRUD**: not exposed by `segmentcli`; use the Public API or app.segment.com.

## References

- Repo: https://github.com/segment-integrations/segmentcli
- Public API docs: https://docs.segmentapis.com
- Regional Segment (EU): https://segment.com/docs/guides/regional-segment/
- Analytics Live Plugins SDKs: [Swift](https://github.com/segment-integrations/analytics-swift-live), [Kotlin](https://github.com/segment-integrations/analytics-kotlin-live)
50 changes: 37 additions & 13 deletions Sources/segmentcli/PAPI/PAPI.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// File.swift
//
//
//
// Created by Brandon Sneed on 12/3/21.
//
Expand All @@ -20,6 +20,26 @@ protocol PAPISection {
static var pathEntry: String { get }
}

// URLSession strips Authorization on redirect by default; api.segmentapis.com
// 30x's to regional hosts (e.g. eu1.api.segmentapis.com) for EU workspaces.
final class PAPIRedirectDelegate: NSObject, URLSessionTaskDelegate {
static let shared = PAPIRedirectDelegate()
func urlSession(_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void) {
var newRequest = request
for header in ["Authorization", "Accept"] {
if let value = task.originalRequest?.value(forHTTPHeaderField: header),
newRequest.value(forHTTPHeaderField: header) == nil {
newRequest.setValue(value, forHTTPHeaderField: header)
}
}
completionHandler(newRequest)
}
}

class PAPI {
enum StatusCode: Int {
case unknown = 0
Expand All @@ -34,31 +54,35 @@ class PAPI {
case tooManyRequests = 429
case serverError = 500
}

static let shared = PAPI()


let session = URLSession(configuration: .default,
delegate: PAPIRedirectDelegate.shared,
delegateQueue: nil)

let sources = PAPI.Sources()
let edgeFunctions = PAPI.EdgeFunctions()

func statusCode(response: URLResponse?) -> StatusCode {
if let httpResponse = response as? HTTPURLResponse {
if let status = StatusCode(rawValue: httpResponse.statusCode) {
return status
}
if let httpResponse = response as? HTTPURLResponse,
let status = StatusCode(rawValue: httpResponse.statusCode) {
return status
}
return .unknown
}

func authenticate(token: String, completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
guard let url = URL(string: PAPIEndpoint) else { completion(nil, nil, "Unable to create URL."); return }

var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 30)
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")

let task = URLSession.shared.dataTask(with: request, completionHandler: completion)
request.addValue("application/vnd.segment.v1+json", forHTTPHeaderField: "Accept")

let task = session.dataTask(with: request, completionHandler: completion)
task.resume()
}

}

// MARK: - Global option to support staging
Expand Down
10 changes: 5 additions & 5 deletions Sources/segmentcli/PAPI/PAPIEdgeFunctions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ extension PAPI {
request.httpMethod = "GET"
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")

let task = URLSession.shared.dataTask(with: request, completionHandler: completion)
let task = PAPI.shared.session.dataTask(with: request, completionHandler: completion)
task.resume()
}

Expand All @@ -41,7 +41,7 @@ extension PAPI {
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = "{ \"sourceId\": \"\(sourceId)\" }".data(using: .utf8)

let task = URLSession.shared.dataTask(with: request, completionHandler: completion)
let task = PAPI.shared.session.dataTask(with: request, completionHandler: completion)
task.resume()
}

Expand All @@ -59,7 +59,7 @@ extension PAPI {
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = "{ \"sourceId\": \"\(sourceId)\" }".data(using: .utf8)

let task = URLSession.shared.dataTask(with: request, completionHandler: completion)
let task = PAPI.shared.session.dataTask(with: request, completionHandler: completion)
task.resume()
}

Expand All @@ -79,7 +79,7 @@ extension PAPI {
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = "{ \"uploadURL\": \"\(uploadURL.absoluteString)\", \"sourceId\": \"\(sourceId)\" }".data(using: .utf8)

let task = URLSession.shared.dataTask(with: request, completionHandler: completion)
let task = PAPI.shared.session.dataTask(with: request, completionHandler: completion)
task.resume()
}

Expand All @@ -88,7 +88,7 @@ extension PAPI {
guard let fileURL = fileURL else { completion(nil, nil, "File URL is nil."); return }
var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 30)
request.httpMethod = "PUT"
let task = URLSession.shared.uploadTask(with: request, fromFile: fileURL, completionHandler: completion)
let task = PAPI.shared.session.uploadTask(with: request, fromFile: fileURL, completionHandler: completion)
task.resume()
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/segmentcli/PAPI/PAPISources.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ extension PAPI {
var request = URLRequest(url: newURL, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 30)
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")

let task = URLSession.shared.dataTask(with: request, completionHandler: completion)
let task = PAPI.shared.session.dataTask(with: request, completionHandler: completion)
task.resume()
}
}
Expand Down