@owlmetry/cli 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist/index.cjs +215 -16
- package/dist/skills/owlmetry-cli/SKILL.md +82 -20
- package/dist/skills/owlmetry-node/SKILL.md +38 -6
- package/dist/skills/owlmetry-swift/SKILL.md +39 -10
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @owlmetry/cli
|
|
2
|
+
|
|
3
|
+
CLI for [OwlMetry](https://owlmetry.com) — self-hosted metrics tracking for mobile and backend apps.
|
|
4
|
+
|
|
5
|
+
Manage projects, apps, metrics, funnels, and events from the terminal. Ships with AI skill files to teach your coding agent how to use OwlMetry.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @owlmetry/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Sign up or log in
|
|
17
|
+
owlmetry auth send-code --email you@example.com
|
|
18
|
+
owlmetry auth verify --email you@example.com --code 123456
|
|
19
|
+
|
|
20
|
+
# Save your credentials
|
|
21
|
+
owlmetry setup --endpoint https://api.owlmetry.com --api-key owl_agent_...
|
|
22
|
+
|
|
23
|
+
# Explore
|
|
24
|
+
owlmetry projects
|
|
25
|
+
owlmetry apps
|
|
26
|
+
owlmetry events --last 1h
|
|
27
|
+
owlmetry metrics
|
|
28
|
+
owlmetry funnels
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## AI Skills
|
|
32
|
+
|
|
33
|
+
This package bundles skill files that teach AI agents (Claude Code, Codex, etc.) how to use OwlMetry — including the CLI, Node SDK, and Swift SDK.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
owlmetry skills
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This prints the absolute paths to each skill file. Point your agent to these files to give it full OwlMetry knowledge.
|
|
40
|
+
|
|
41
|
+
## Links
|
|
42
|
+
|
|
43
|
+
- [Website](https://owlmetry.com)
|
|
44
|
+
- [GitHub](https://github.com/Jasonvdb/owlmetry)
|
package/dist/index.cjs
CHANGED
|
@@ -5757,6 +5757,10 @@ var OwlMetryClient = class {
|
|
|
5757
5757
|
}
|
|
5758
5758
|
return await response.json();
|
|
5759
5759
|
}
|
|
5760
|
+
// Auth
|
|
5761
|
+
async whoami() {
|
|
5762
|
+
return this.request("GET", "/v1/auth/whoami");
|
|
5763
|
+
}
|
|
5760
5764
|
// Projects
|
|
5761
5765
|
async listProjects() {
|
|
5762
5766
|
const result = await this.request("GET", "/v1/projects");
|
|
@@ -5916,6 +5920,7 @@ var import_node_fs = require("fs");
|
|
|
5916
5920
|
var import_node_path = require("path");
|
|
5917
5921
|
var import_node_os2 = require("os");
|
|
5918
5922
|
var DEFAULT_ENDPOINT = "https://api.owlmetry.com";
|
|
5923
|
+
var DEFAULT_INGEST_ENDPOINT = "https://ingest.owlmetry.com";
|
|
5919
5924
|
var CONFIG_DIR = (0, import_node_path.join)((0, import_node_os2.homedir)(), ".owlmetry");
|
|
5920
5925
|
var CONFIG_FILE = (0, import_node_path.join)(CONFIG_DIR, "config.json");
|
|
5921
5926
|
function loadConfig() {
|
|
@@ -5930,12 +5935,59 @@ function saveConfig(config) {
|
|
|
5930
5935
|
(0, import_node_fs.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
5931
5936
|
(0, import_node_fs.writeFileSync)(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
5932
5937
|
}
|
|
5938
|
+
function getActiveProfile(config, teamHint) {
|
|
5939
|
+
const entries = Object.entries(config.teams);
|
|
5940
|
+
if (entries.length === 0) {
|
|
5941
|
+
throw new Error("No team profiles configured. Run `owlmetry auth verify` to add one.");
|
|
5942
|
+
}
|
|
5943
|
+
if (!teamHint) {
|
|
5944
|
+
const profile = config.teams[config.active_team];
|
|
5945
|
+
if (!profile) {
|
|
5946
|
+
throw new Error(
|
|
5947
|
+
`Active team "${config.active_team}" not found in config. Run \`owlmetry auth verify\` to re-authenticate.`
|
|
5948
|
+
);
|
|
5949
|
+
}
|
|
5950
|
+
return { teamId: config.active_team, profile };
|
|
5951
|
+
}
|
|
5952
|
+
if (config.teams[teamHint]) {
|
|
5953
|
+
return { teamId: teamHint, profile: config.teams[teamHint] };
|
|
5954
|
+
}
|
|
5955
|
+
for (const [id, profile] of entries) {
|
|
5956
|
+
if (profile.team_slug === teamHint) {
|
|
5957
|
+
return { teamId: id, profile };
|
|
5958
|
+
}
|
|
5959
|
+
}
|
|
5960
|
+
const lower = teamHint.toLowerCase();
|
|
5961
|
+
for (const [id, profile] of entries) {
|
|
5962
|
+
if (profile.team_name.toLowerCase() === lower) {
|
|
5963
|
+
return { teamId: id, profile };
|
|
5964
|
+
}
|
|
5965
|
+
}
|
|
5966
|
+
const available = entries.map(([id, p]) => ` ${id} ${p.team_name} (${p.team_slug})`).join("\n");
|
|
5967
|
+
throw new Error(`No team matching "${teamHint}". Available teams:
|
|
5968
|
+
${available}`);
|
|
5969
|
+
}
|
|
5970
|
+
function listProfiles(config) {
|
|
5971
|
+
return Object.entries(config.teams).map(([teamId, profile]) => ({
|
|
5972
|
+
teamId,
|
|
5973
|
+
profile,
|
|
5974
|
+
active: teamId === config.active_team
|
|
5975
|
+
}));
|
|
5976
|
+
}
|
|
5933
5977
|
function resolveConfig(opts) {
|
|
5934
5978
|
const envEndpoint = opts.endpoint ?? process.env.OWLMETRY_ENDPOINT;
|
|
5935
5979
|
const envApiKey = opts.apiKey ?? process.env.OWLMETRY_API_KEY;
|
|
5936
5980
|
const file = envEndpoint && envApiKey ? null : loadConfig();
|
|
5937
|
-
|
|
5938
|
-
|
|
5981
|
+
let endpoint = envEndpoint;
|
|
5982
|
+
let api_key = envApiKey;
|
|
5983
|
+
let ingest_endpoint = file?.ingest_endpoint;
|
|
5984
|
+
if (file && !api_key) {
|
|
5985
|
+
const { profile } = getActiveProfile(file, opts.team);
|
|
5986
|
+
api_key = profile.api_key;
|
|
5987
|
+
}
|
|
5988
|
+
if (!endpoint) {
|
|
5989
|
+
endpoint = file?.endpoint;
|
|
5990
|
+
}
|
|
5939
5991
|
if (!endpoint) {
|
|
5940
5992
|
throw new Error(
|
|
5941
5993
|
"Missing endpoint. Use --endpoint, OWLMETRY_ENDPOINT env var, or run `owlmetry auth login`."
|
|
@@ -5946,7 +5998,7 @@ function resolveConfig(opts) {
|
|
|
5946
5998
|
"Missing API key. Use --api-key, OWLMETRY_API_KEY env var, or run `owlmetry auth login`."
|
|
5947
5999
|
);
|
|
5948
6000
|
}
|
|
5949
|
-
return { endpoint, api_key };
|
|
6001
|
+
return { endpoint, api_key, ingest_endpoint };
|
|
5950
6002
|
}
|
|
5951
6003
|
function getGlobals(cmd) {
|
|
5952
6004
|
return cmd.optsWithGlobals();
|
|
@@ -5979,16 +6031,42 @@ var setupCommand = new Command("setup").description("Configure CLI endpoint and
|
|
|
5979
6031
|
endpoint: globals.endpoint,
|
|
5980
6032
|
apiKey: globals.apiKey
|
|
5981
6033
|
});
|
|
6034
|
+
let teamId = "_manual";
|
|
6035
|
+
let teamName = "Manual Setup";
|
|
6036
|
+
let teamSlug = "manual";
|
|
5982
6037
|
try {
|
|
5983
|
-
await client.
|
|
6038
|
+
const whoami = await client.whoami();
|
|
6039
|
+
if (whoami.team) {
|
|
6040
|
+
teamId = whoami.team.id;
|
|
6041
|
+
teamName = whoami.team.name;
|
|
6042
|
+
teamSlug = whoami.team.slug;
|
|
6043
|
+
}
|
|
5984
6044
|
} catch (err) {
|
|
5985
6045
|
console.error(
|
|
5986
6046
|
source_default.red(`Failed to connect: ${err instanceof Error ? err.message : String(err)}`)
|
|
5987
6047
|
);
|
|
5988
6048
|
process.exit(1);
|
|
5989
6049
|
}
|
|
5990
|
-
|
|
5991
|
-
|
|
6050
|
+
const ingestEndpoint = globals.ingestEndpoint || globals.endpoint;
|
|
6051
|
+
const existing = loadConfig();
|
|
6052
|
+
const config = {
|
|
6053
|
+
endpoint: globals.endpoint,
|
|
6054
|
+
ingest_endpoint: ingestEndpoint,
|
|
6055
|
+
active_team: teamId,
|
|
6056
|
+
teams: {
|
|
6057
|
+
...existing?.teams,
|
|
6058
|
+
[teamId]: {
|
|
6059
|
+
api_key: globals.apiKey,
|
|
6060
|
+
team_name: teamName,
|
|
6061
|
+
team_slug: teamSlug
|
|
6062
|
+
}
|
|
6063
|
+
}
|
|
6064
|
+
};
|
|
6065
|
+
saveConfig(config);
|
|
6066
|
+
console.log(source_default.green("\u2713 Configuration saved to ~/.owlmetry/config.json"));
|
|
6067
|
+
console.log(` Team: ${teamName}`);
|
|
6068
|
+
console.log(` API endpoint: ${globals.endpoint}`);
|
|
6069
|
+
console.log(` Ingest endpoint: ${ingestEndpoint}`);
|
|
5992
6070
|
});
|
|
5993
6071
|
|
|
5994
6072
|
// src/commands/projects.ts
|
|
@@ -6494,6 +6572,12 @@ init_cjs_shims();
|
|
|
6494
6572
|
function resolveEndpoint(globals) {
|
|
6495
6573
|
return globals.endpoint || process.env.OWLMETRY_ENDPOINT || DEFAULT_ENDPOINT;
|
|
6496
6574
|
}
|
|
6575
|
+
function resolveIngestEndpointForAuth(globals, endpoint) {
|
|
6576
|
+
const explicit = globals.ingestEndpoint ?? process.env.OWLMETRY_INGEST_ENDPOINT;
|
|
6577
|
+
if (explicit) return explicit;
|
|
6578
|
+
if (endpoint === DEFAULT_ENDPOINT) return DEFAULT_INGEST_ENDPOINT;
|
|
6579
|
+
return endpoint;
|
|
6580
|
+
}
|
|
6497
6581
|
async function apiPost(endpoint, path, body) {
|
|
6498
6582
|
const res = await fetch(`${endpoint.replace(/\/+$/, "")}${path}`, {
|
|
6499
6583
|
method: "POST",
|
|
@@ -6552,19 +6636,38 @@ authCommand.command("verify").description("Verify code and get an agent API key"
|
|
|
6552
6636
|
}
|
|
6553
6637
|
process.exit(1);
|
|
6554
6638
|
}
|
|
6555
|
-
const { api_key, team
|
|
6556
|
-
if (!api_key) {
|
|
6557
|
-
console.error(source_default.red("No API key returned"));
|
|
6639
|
+
const { api_key, team } = data;
|
|
6640
|
+
if (!api_key || !team) {
|
|
6641
|
+
console.error(source_default.red("No API key or team info returned"));
|
|
6558
6642
|
process.exit(1);
|
|
6559
6643
|
}
|
|
6560
|
-
|
|
6644
|
+
const ingestEndpoint = resolveIngestEndpointForAuth(globals, endpoint);
|
|
6645
|
+
const existing = loadConfig();
|
|
6646
|
+
const config = {
|
|
6647
|
+
endpoint,
|
|
6648
|
+
ingest_endpoint: ingestEndpoint,
|
|
6649
|
+
active_team: team.id,
|
|
6650
|
+
teams: {
|
|
6651
|
+
...existing?.teams,
|
|
6652
|
+
[team.id]: {
|
|
6653
|
+
api_key,
|
|
6654
|
+
team_name: team.name,
|
|
6655
|
+
team_slug: team.slug
|
|
6656
|
+
}
|
|
6657
|
+
}
|
|
6658
|
+
};
|
|
6659
|
+
saveConfig(config);
|
|
6660
|
+
const profileCount = Object.keys(config.teams).length;
|
|
6561
6661
|
if (format === "json") {
|
|
6562
|
-
console.log(JSON.stringify({ api_key, endpoint,
|
|
6662
|
+
console.log(JSON.stringify({ api_key, endpoint, ingest_endpoint: ingestEndpoint, team }, null, 2));
|
|
6563
6663
|
} else {
|
|
6564
6664
|
console.log(source_default.green("\u2713 Authenticated! Config saved to ~/.owlmetry/config.json"));
|
|
6565
|
-
console.log(` Team:
|
|
6566
|
-
|
|
6567
|
-
|
|
6665
|
+
console.log(` Team: ${team.name}`);
|
|
6666
|
+
console.log(` API endpoint: ${endpoint}`);
|
|
6667
|
+
console.log(` Ingest endpoint: ${ingestEndpoint}`);
|
|
6668
|
+
if (profileCount > 1) {
|
|
6669
|
+
console.log(` Profiles: ${profileCount} teams configured`);
|
|
6670
|
+
}
|
|
6568
6671
|
}
|
|
6569
6672
|
});
|
|
6570
6673
|
|
|
@@ -6976,10 +7079,104 @@ ${source_default.dim("Point your AI agent to these files to teach it how to use
|
|
|
6976
7079
|
);
|
|
6977
7080
|
});
|
|
6978
7081
|
|
|
7082
|
+
// src/commands/whoami.ts
|
|
7083
|
+
init_cjs_shims();
|
|
7084
|
+
var whoamiCommand = new Command("whoami").description("Show current authentication status and identity").action(async (_opts, cmd) => {
|
|
7085
|
+
const { client, globals } = createClient(cmd);
|
|
7086
|
+
const data = await client.whoami();
|
|
7087
|
+
if (globals.format === "json") {
|
|
7088
|
+
const config2 = loadConfig();
|
|
7089
|
+
const profiles = config2 ? listProfiles(config2) : [];
|
|
7090
|
+
console.log(JSON.stringify({
|
|
7091
|
+
...data,
|
|
7092
|
+
configured_profiles: profiles.map((p) => ({
|
|
7093
|
+
team_id: p.teamId,
|
|
7094
|
+
team_name: p.profile.team_name,
|
|
7095
|
+
team_slug: p.profile.team_slug,
|
|
7096
|
+
active: p.active
|
|
7097
|
+
}))
|
|
7098
|
+
}, null, 2));
|
|
7099
|
+
return;
|
|
7100
|
+
}
|
|
7101
|
+
if (data.type === "api_key") {
|
|
7102
|
+
const team = data.team;
|
|
7103
|
+
const permissions = data.permissions;
|
|
7104
|
+
console.log(source_default.green("\u2713 Authenticated"));
|
|
7105
|
+
console.log(` Team: ${team?.name ?? "unknown"}`);
|
|
7106
|
+
console.log(` Key type: ${data.key_type}`);
|
|
7107
|
+
console.log(` Permissions: ${permissions.join(", ")}`);
|
|
7108
|
+
} else {
|
|
7109
|
+
const teams = data.teams;
|
|
7110
|
+
console.log(source_default.green("\u2713 Authenticated"));
|
|
7111
|
+
console.log(` Email: ${data.email}`);
|
|
7112
|
+
console.log(` Teams: ${teams.map((t) => `${t.name} (${t.role})`).join(", ")}`);
|
|
7113
|
+
}
|
|
7114
|
+
const config = loadConfig();
|
|
7115
|
+
if (config && Object.keys(config.teams).length > 0) {
|
|
7116
|
+
const profiles = listProfiles(config);
|
|
7117
|
+
console.log();
|
|
7118
|
+
console.log("Configured profiles:");
|
|
7119
|
+
for (const { teamId, profile, active } of profiles) {
|
|
7120
|
+
const marker = active ? source_default.green("\u25CF") : " ";
|
|
7121
|
+
const name = active ? source_default.bold(profile.team_name) : profile.team_name;
|
|
7122
|
+
console.log(` ${marker} ${name} (${profile.team_slug}) ${source_default.dim(teamId)}`);
|
|
7123
|
+
}
|
|
7124
|
+
}
|
|
7125
|
+
});
|
|
7126
|
+
|
|
7127
|
+
// src/commands/switch.ts
|
|
7128
|
+
init_cjs_shims();
|
|
7129
|
+
var switchCommand = new Command("switch").description("Switch active team profile or list all profiles").argument("[team]", "Team ID, slug, or name to switch to").action(async (teamArg, _opts, cmd) => {
|
|
7130
|
+
const globals = getGlobals(cmd);
|
|
7131
|
+
const config = loadConfig();
|
|
7132
|
+
if (!config || Object.keys(config.teams).length === 0) {
|
|
7133
|
+
if (globals.format === "json") {
|
|
7134
|
+
console.log(JSON.stringify({ error: "No team profiles configured" }));
|
|
7135
|
+
} else {
|
|
7136
|
+
console.error(source_default.red("No team profiles configured. Run `owlmetry auth verify` to add one."));
|
|
7137
|
+
}
|
|
7138
|
+
process.exit(1);
|
|
7139
|
+
}
|
|
7140
|
+
if (!teamArg) {
|
|
7141
|
+
const profiles = listProfiles(config);
|
|
7142
|
+
if (globals.format === "json") {
|
|
7143
|
+
console.log(JSON.stringify(profiles.map((p) => ({
|
|
7144
|
+
team_id: p.teamId,
|
|
7145
|
+
team_name: p.profile.team_name,
|
|
7146
|
+
team_slug: p.profile.team_slug,
|
|
7147
|
+
active: p.active
|
|
7148
|
+
})), null, 2));
|
|
7149
|
+
return;
|
|
7150
|
+
}
|
|
7151
|
+
for (const { teamId: teamId2, profile: profile2, active } of profiles) {
|
|
7152
|
+
const marker = active ? source_default.green("\u25CF") : " ";
|
|
7153
|
+
const name = active ? source_default.bold(profile2.team_name) : profile2.team_name;
|
|
7154
|
+
console.log(` ${marker} ${name} (${profile2.team_slug}) ${source_default.dim(teamId2)}`);
|
|
7155
|
+
}
|
|
7156
|
+
return;
|
|
7157
|
+
}
|
|
7158
|
+
const { teamId, profile } = getActiveProfile(config, teamArg);
|
|
7159
|
+
if (teamId === config.active_team) {
|
|
7160
|
+
if (globals.format === "json") {
|
|
7161
|
+
console.log(JSON.stringify({ team_id: teamId, team_name: profile.team_name, already_active: true }));
|
|
7162
|
+
} else {
|
|
7163
|
+
console.log(`Already on ${source_default.bold(profile.team_name)} (${profile.team_slug})`);
|
|
7164
|
+
}
|
|
7165
|
+
return;
|
|
7166
|
+
}
|
|
7167
|
+
config.active_team = teamId;
|
|
7168
|
+
saveConfig(config);
|
|
7169
|
+
if (globals.format === "json") {
|
|
7170
|
+
console.log(JSON.stringify({ team_id: teamId, team_name: profile.team_name, team_slug: profile.team_slug }));
|
|
7171
|
+
} else {
|
|
7172
|
+
console.log(source_default.green(`\u2713 Switched to ${source_default.bold(profile.team_name)} (${profile.team_slug})`));
|
|
7173
|
+
}
|
|
7174
|
+
});
|
|
7175
|
+
|
|
6979
7176
|
// src/index.ts
|
|
6980
|
-
var program2 = new Command().name("owlmetry").version("0.1.
|
|
7177
|
+
var program2 = new Command().name("owlmetry").version("0.1.2").description("OwlMetry CLI \u2014 query metrics and manage your apps from the terminal").addOption(
|
|
6981
7178
|
new Option("--format <format>", "Output format").choices(["table", "json", "log"]).default("table")
|
|
6982
|
-
).option("--endpoint <url>", "OwlMetry server URL").option("--api-key <key>", "API key");
|
|
7179
|
+
).option("--endpoint <url>", "OwlMetry API server URL").option("--api-key <key>", "API key").option("--ingest-endpoint <url>", "OwlMetry ingest endpoint URL (for SDKs; defaults to API endpoint for self-hosted)").option("--team <name-or-id>", "Use a specific team profile for this command");
|
|
6983
7180
|
program2.addCommand(authCommand);
|
|
6984
7181
|
program2.addCommand(setupCommand);
|
|
6985
7182
|
program2.addCommand(projectsCommand);
|
|
@@ -6991,6 +7188,8 @@ program2.addCommand(metricsCommand);
|
|
|
6991
7188
|
program2.addCommand(funnelsCommand);
|
|
6992
7189
|
program2.addCommand(auditLogCommand);
|
|
6993
7190
|
program2.addCommand(skillsCommand);
|
|
7191
|
+
program2.addCommand(whoamiCommand);
|
|
7192
|
+
program2.addCommand(switchCommand);
|
|
6994
7193
|
program2.parseAsync().catch((err) => {
|
|
6995
7194
|
const format = program2.opts().format;
|
|
6996
7195
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -25,33 +25,82 @@ If everything is current or the remote is unreachable, continue silently.
|
|
|
25
25
|
|
|
26
26
|
## Setup
|
|
27
27
|
|
|
28
|
+
Follow these steps in order. Skip any step that's already done.
|
|
29
|
+
|
|
30
|
+
### Step 1 — Install the CLI
|
|
31
|
+
|
|
28
32
|
**Prerequisites:** Node.js 20+
|
|
29
33
|
|
|
30
|
-
**Install:**
|
|
31
34
|
```bash
|
|
32
35
|
npm install -g @owlmetry/cli
|
|
33
36
|
```
|
|
34
37
|
|
|
35
|
-
|
|
38
|
+
### Step 2 — Check authentication
|
|
39
|
+
|
|
36
40
|
```bash
|
|
37
|
-
owlmetry
|
|
41
|
+
owlmetry whoami --format json
|
|
38
42
|
```
|
|
39
|
-
|
|
43
|
+
|
|
44
|
+
If this succeeds, authentication is already configured — skip to Step 3.
|
|
45
|
+
|
|
46
|
+
If it fails (missing config or invalid key), run the auth flow:
|
|
47
|
+
|
|
48
|
+
1. Ask the user for their email address.
|
|
49
|
+
2. Send a verification code:
|
|
50
|
+
```bash
|
|
51
|
+
owlmetry auth send-code --email <user-email>
|
|
52
|
+
```
|
|
53
|
+
3. Ask the user for the 6-digit code from their email.
|
|
54
|
+
4. Verify and save credentials:
|
|
55
|
+
```bash
|
|
56
|
+
owlmetry auth verify --email <user-email> --code <code> --format json
|
|
57
|
+
```
|
|
58
|
+
This creates the user account and team (if new), generates an agent API key (`owl_agent_...`), and saves config to `~/.owlmetry/config.json`. Default API endpoint: `https://api.owlmetry.com`. Default ingest endpoint: `https://ingest.owlmetry.com`.
|
|
59
|
+
|
|
60
|
+
**Multi-team setup**: If the user belongs to multiple teams, run `auth verify` once per team (using `--team-id` to select each team). Each team's key is stored as a separate profile. The last verified team becomes the active profile.
|
|
61
|
+
|
|
62
|
+
**Switching teams**: Use `owlmetry switch` to list profiles or `owlmetry switch <name-or-slug>` to switch the active team. Use `--team <name-or-slug>` on any command to use a different team's key for a single command without switching.
|
|
63
|
+
|
|
64
|
+
**Manual setup** (if the user already has an API key):
|
|
40
65
|
```bash
|
|
41
|
-
owlmetry
|
|
66
|
+
owlmetry setup --endpoint <url> --api-key <key> [--ingest-endpoint <url>]
|
|
42
67
|
```
|
|
43
68
|
|
|
44
|
-
|
|
45
|
-
- The response includes an agent API key (`owl_agent_...`), team ID, project ID, and app details.
|
|
46
|
-
- Config is saved to `~/.owlmetry/config.json`.
|
|
47
|
-
- Default endpoint: `https://api.owlmetry.com`
|
|
69
|
+
### Step 3 — Create project and app
|
|
48
70
|
|
|
49
|
-
|
|
71
|
+
After authentication, set up the resources the SDK needs. Check if any projects already exist:
|
|
50
72
|
|
|
51
|
-
**Manual setup** (if the user already has an API key):
|
|
52
73
|
```bash
|
|
53
|
-
owlmetry
|
|
74
|
+
owlmetry projects --format json
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If the user already has a project and app, skip to SDK integration.
|
|
78
|
+
|
|
79
|
+
**Create a project** — infer a good name from the user's repository or directory name:
|
|
80
|
+
```bash
|
|
81
|
+
owlmetry projects create --name "<ProjectName>" --slug "<project-slug>" --format json
|
|
54
82
|
```
|
|
83
|
+
Save the returned `id` — you need it for the next command.
|
|
84
|
+
|
|
85
|
+
**Create an app** — choose the platform based on the project type:
|
|
86
|
+
- Swift/SwiftUI → `apple`
|
|
87
|
+
- Kotlin/Android → `android`
|
|
88
|
+
- Web frontend → `web`
|
|
89
|
+
- Node.js/backend → `backend`
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
owlmetry apps create --project-id <project-id> --name "<AppName>" --platform <platform> [--bundle-id <bundle-id>] --format json
|
|
93
|
+
```
|
|
94
|
+
- `--bundle-id` is required for apple/android/web (e.g., `com.example.myapp`), omitted for backend.
|
|
95
|
+
- The response includes a `client_key` (`owl_client_...`) — this is the SDK API key for event ingestion. Save it.
|
|
96
|
+
|
|
97
|
+
### Step 4 — Integrate the SDK
|
|
98
|
+
|
|
99
|
+
Use the `client_key` from Step 3 to configure the appropriate SDK:
|
|
100
|
+
- **Node.js projects** → follow the `owlmetry-node` skill file
|
|
101
|
+
- **Swift/iOS projects** → follow the `owlmetry-swift` skill file
|
|
102
|
+
|
|
103
|
+
Pass the **ingest endpoint** and client key to the SDK's `configure()` call. Read `~/.owlmetry/config.json` for the `ingest_endpoint` value (set during auth). For the hosted platform it's `https://ingest.owlmetry.com`. For self-hosted it defaults to the API endpoint.
|
|
55
104
|
|
|
56
105
|
## Resource Hierarchy
|
|
57
106
|
|
|
@@ -197,7 +246,10 @@ owlmetry audit-log list --team <id> [--resource-type <type>] [--resource-id <id>
|
|
|
197
246
|
## Key Notes
|
|
198
247
|
|
|
199
248
|
- Always use `--format json` when parsing output programmatically.
|
|
200
|
-
- **Global flags** available on all commands: `--endpoint <url>`, `--api-key <key>`, `--format <format>`
|
|
249
|
+
- **Global flags** available on all commands: `--endpoint <url>`, `--api-key <key>`, `--ingest-endpoint <url>`, `--team <name-or-id>`, `--format <format>`
|
|
250
|
+
- **Team profiles**: Config stores multiple team profiles keyed by team ID. `owlmetry switch` lists/switches the active profile. `--team` flag on any command uses a specific team's key without switching the active profile.
|
|
251
|
+
- **Two endpoints**: `endpoint` is the API server (for CLI/agent queries). `ingest_endpoint` is where SDKs send events. Both are saved in `~/.owlmetry/config.json`. For the hosted platform: `api.owlmetry.com` and `ingest.owlmetry.com`. For self-hosted: ingest defaults to the same as the API endpoint.
|
|
252
|
+
- **Config format**: `~/.owlmetry/config.json` stores `endpoint`, `ingest_endpoint`, `active_team` (team ID), and `teams` (a map of team ID → `{ api_key, team_name, team_slug }`).
|
|
201
253
|
- **Agent keys** (`owl_agent_...`) are for CLI queries. **Client keys** (`owl_client_...`) are for SDK event ingestion.
|
|
202
254
|
- **Time format:** relative (`1h`, `30m`, `7d`) or ISO 8601 (`2026-03-20T00:00:00Z`). Relative times go backwards from now — `1h` means "the last hour", `7d` means "the last 7 days".
|
|
203
255
|
- **Data mode:** `production` (default), `debug`, or `all` — filters events by their debug flag. SDKs auto-detect debug mode (DEBUG builds on iOS, `NODE_ENV !== "production"` on Node). Use `debug` mode during development to see test events; use `production` (the default) for real analytics.
|
|
@@ -207,10 +259,20 @@ owlmetry audit-log list --team <id> [--resource-type <type>] [--resource-id <id>
|
|
|
207
259
|
|
|
208
260
|
A typical end-to-end flow for adding OwlMetry to a new project:
|
|
209
261
|
|
|
210
|
-
1. **Sign up
|
|
211
|
-
2. **Create
|
|
212
|
-
3. **
|
|
213
|
-
4. **
|
|
214
|
-
5. **
|
|
215
|
-
|
|
216
|
-
|
|
262
|
+
1. **Sign up** (CLI): `owlmetry auth send-code` → verify code → team created, config saved
|
|
263
|
+
2. **Create a project** (CLI): `owlmetry projects create --name "..." --slug "..." --format json`
|
|
264
|
+
3. **Create apps** (CLI): `owlmetry apps create --platform apple --bundle-id com.example.myapp` (and/or android, web, backend)
|
|
265
|
+
4. **Note the client key**: from the app creation response — pass this to the SDK
|
|
266
|
+
5. **Integrate the SDK** (switch to `/owlmetry-swift` or `/owlmetry-node` skill): Add the SDK dependency, configure with `ingest_endpoint` and client key, verify the project builds
|
|
267
|
+
|
|
268
|
+
After setup, the SDK skill will prompt the user to choose which instrumentation to start with. The three areas and what's needed:
|
|
269
|
+
|
|
270
|
+
| Area | CLI setup needed? | SDK skill |
|
|
271
|
+
|------|-------------------|-----------|
|
|
272
|
+
| **Event & error logging** | No — just add SDK calls | `Owl.info()`, `Owl.error()`, `Owl.warn()`, `Owl.debug()` |
|
|
273
|
+
| **Structured metrics** | Yes — `owlmetry metrics create` for each metric slug | `Owl.startOperation()`, `Owl.recordMetric()` |
|
|
274
|
+
| **Funnel tracking** | Yes — `owlmetry funnels create` with steps JSON | `Owl.track("step-name")` |
|
|
275
|
+
|
|
276
|
+
For metrics and funnels, the CLI defines **what** to track (server-side definitions), and the SDK implements **where** to track it (code instrumentation). The definition must exist before the SDK emits events for that slug.
|
|
277
|
+
|
|
278
|
+
6. **Query data** (CLI): Use `owlmetry events`, `owlmetry metrics query`, and `owlmetry funnels query` to analyze behavior
|
|
@@ -23,13 +23,9 @@ Run these checks silently. Only inform the user if updates are available.
|
|
|
23
23
|
|
|
24
24
|
## Prerequisite
|
|
25
25
|
|
|
26
|
-
You need an
|
|
26
|
+
You need an **ingest endpoint** and a **client key** (`owl_client_...`) for a backend app. Both come from the CLI setup flow.
|
|
27
27
|
|
|
28
|
-
If the user doesn't have these yet,
|
|
29
|
-
1. Sign up or log in
|
|
30
|
-
2. Create a project (if needed)
|
|
31
|
-
3. Create an app with `--platform backend` (no `--bundle-id` needed)
|
|
32
|
-
4. Note the `client_key` from the app creation response
|
|
28
|
+
If the user doesn't have these yet, follow the `/owlmetry-cli` skill first — it handles sign-up, project creation, and app creation. The ingest endpoint is saved to `~/.owlmetry/config.json` (`ingest_endpoint` field) and the client key is returned when creating an app.
|
|
33
29
|
|
|
34
30
|
## Install
|
|
35
31
|
|
|
@@ -61,6 +57,16 @@ Owl.configure({
|
|
|
61
57
|
- Generates a fresh `sessionId` (UUID) on each `configure()` call
|
|
62
58
|
- Registers a `beforeExit` handler to auto-flush on graceful shutdown
|
|
63
59
|
|
|
60
|
+
## Next Steps — Codebase Instrumentation
|
|
61
|
+
|
|
62
|
+
Once `Owl.configure()` is in place and the project builds, **stop and ask the user** which area they'd like to instrument first. Present these three options:
|
|
63
|
+
|
|
64
|
+
1. **Event & error logging** — Audit the codebase for request handlers, error paths, background jobs, and key operations. Add `Owl.info()`, `Owl.warn()`, `Owl.error()` calls at meaningful points throughout the service. This is SDK-only — no CLI setup required beyond what's already done.
|
|
65
|
+
2. **Structured metrics** — Identify operations worth measuring (API response times, database queries, external service calls, etc.). Add `Owl.startOperation()` / `Owl.recordMetric()` to track durations and success rates. **Requires CLI first:** each metric slug must be defined on the server via `owlmetry metrics create` (use the `/owlmetry-cli` skill) before the SDK can emit events for it.
|
|
66
|
+
3. **Funnel tracking** — Identify server-side user journeys (sign-up flow, payment processing, onboarding steps). Add `Owl.track()` calls at each step to measure drop-off. **Requires CLI first:** the funnel definition (with steps and event filters) must be created via `owlmetry funnels create` (use the `/owlmetry-cli` skill) before tracking makes sense.
|
|
67
|
+
|
|
68
|
+
Wait for the user to choose before proceeding. For whichever option they pick, do a thorough audit of the entire codebase to find all relevant locations, then present a summary of proposed changes before making any edits.
|
|
69
|
+
|
|
64
70
|
## Buffering and Delivery
|
|
65
71
|
|
|
66
72
|
The SDK buffers events in memory and sends them to the server in batches. Understanding how buffering works helps you avoid lost events:
|
|
@@ -313,3 +319,29 @@ fastify.post('/api/process', async (request, reply) => {
|
|
|
313
319
|
return { ok: true };
|
|
314
320
|
});
|
|
315
321
|
```
|
|
322
|
+
|
|
323
|
+
## Instrumentation Strategy
|
|
324
|
+
|
|
325
|
+
When instrumenting a backend service, follow this priority:
|
|
326
|
+
|
|
327
|
+
**Always instrument (events — no CLI setup needed):**
|
|
328
|
+
- Server startup and shutdown (`info`)
|
|
329
|
+
- Request handling: key route hits, responses sent (`info` with method/path/status)
|
|
330
|
+
- Errors and failures: catch blocks, unhandled rejections, external API failures (`error`)
|
|
331
|
+
- Authentication events: login, logout, token refresh (`info`)
|
|
332
|
+
- Core business actions: order placed, payment processed, email sent (`info`)
|
|
333
|
+
- Background jobs: started, completed, failed (`info`/`error`)
|
|
334
|
+
|
|
335
|
+
**Instrument when relevant (metrics — requires CLI `owlmetry metrics create` first):**
|
|
336
|
+
- Lifecycle metrics for operations where duration matters: API response time, database queries, external service calls, file processing
|
|
337
|
+
- Single-shot metrics for point-in-time values: queue depth, cache hit rate, active connections
|
|
338
|
+
|
|
339
|
+
**Instrument when relevant (funnels — requires CLI `owlmetry funnels create` first):**
|
|
340
|
+
- Multi-step server-side flows you want to measure conversion on: signup pipeline, payment processing, onboarding sequence
|
|
341
|
+
- Always use `Owl.withUser()` for funnel events — funnel analytics require user IDs to calculate conversion
|
|
342
|
+
|
|
343
|
+
**What NOT to instrument:**
|
|
344
|
+
- PII (emails, passwords, tokens, IP addresses)
|
|
345
|
+
- High-frequency health checks or heartbeats
|
|
346
|
+
- Every database query (instrument categories, not every call)
|
|
347
|
+
- Sensitive business data (payment amounts, account balances)
|
|
@@ -23,13 +23,9 @@ Run these checks silently. Only inform the user if updates are available.
|
|
|
23
23
|
|
|
24
24
|
## Prerequisite
|
|
25
25
|
|
|
26
|
-
You need an
|
|
26
|
+
You need an **ingest endpoint** and a **client key** (`owl_client_...`) for an Apple-platform app. Both come from the CLI setup flow.
|
|
27
27
|
|
|
28
|
-
If the user doesn't have these yet,
|
|
29
|
-
1. Sign up or log in
|
|
30
|
-
2. Create a project (if needed)
|
|
31
|
-
3. Create an app with `--platform apple --bundle-id <bundle-id>`
|
|
32
|
-
4. Note the `client_key` from the app creation response
|
|
28
|
+
If the user doesn't have these yet, follow the `/owlmetry-cli` skill first — it handles sign-up, project creation, and app creation. The ingest endpoint is saved to `~/.owlmetry/config.json` (`ingest_endpoint` field) and the client key is returned when creating an app.
|
|
33
29
|
|
|
34
30
|
## Add Swift Package
|
|
35
31
|
|
|
@@ -50,6 +46,26 @@ Add to your target:
|
|
|
50
46
|
|
|
51
47
|
**Minimum platforms:** iOS 16.0, macOS 13.0. Zero external dependencies.
|
|
52
48
|
|
|
49
|
+
## Verify Package Integration
|
|
50
|
+
|
|
51
|
+
After adding the package, build the project to verify the dependency resolves and `import OwlMetry` compiles. Do not proceed with configuration until the build succeeds.
|
|
52
|
+
|
|
53
|
+
If the build fails with a "No such module 'OwlMetry'" error, ask the user to add the package manually in Xcode:
|
|
54
|
+
|
|
55
|
+
1. Open the `.xcodeproj` or `.xcworkspace` in Xcode
|
|
56
|
+
2. Select the project in the navigator (blue icon at the top)
|
|
57
|
+
3. Select the app target under "Targets"
|
|
58
|
+
4. Go to the "General" tab
|
|
59
|
+
5. Scroll to "Frameworks, Libraries, and Embedded Content"
|
|
60
|
+
6. Click the **+** button
|
|
61
|
+
7. Click "Add Other…" > "Add Package Dependency…"
|
|
62
|
+
8. Enter the URL: `https://github.com/Jasonvdb/owlmetry.git`
|
|
63
|
+
9. Set "Dependency Rule" to **Branch** → `main`
|
|
64
|
+
10. Click "Add Package"
|
|
65
|
+
11. Select the **OwlMetry** library and click "Add Package"
|
|
66
|
+
|
|
67
|
+
Once the user confirms the package is added, retry the build to verify, then proceed with configuration.
|
|
68
|
+
|
|
53
69
|
## Configure
|
|
54
70
|
|
|
55
71
|
Configuration must happen once, before any other `Owl` calls — typically in your `@main` App init or AppDelegate `didFinishLaunching`. Each `configure()` call generates a fresh `session_id` (UUID) that groups all subsequent events together. This means each app launch gets its own session, making it easy to see everything a user did in a single sitting.
|
|
@@ -81,6 +97,16 @@ struct MyApp: App {
|
|
|
81
97
|
|
|
82
98
|
Auto-detects: bundle ID, debug mode (`#if DEBUG`). Auto-generates: session ID (fresh each launch).
|
|
83
99
|
|
|
100
|
+
## Next Steps — Codebase Instrumentation
|
|
101
|
+
|
|
102
|
+
Once `Owl.configure()` is in place and the project builds, **stop and ask the user** which area they'd like to instrument first. Present these three options:
|
|
103
|
+
|
|
104
|
+
1. **Event & error logging** — Audit the codebase for user actions, screen views, error handling, and key flows. Add `Owl.info()`, `Owl.warn()`, `Owl.error()` calls at meaningful points. This is SDK-only — no CLI setup required beyond what's already done.
|
|
105
|
+
2. **Structured metrics** — Identify operations worth measuring (network requests, data loading, image processing, etc.). Add `Owl.startOperation()` / `Owl.recordMetric()` to track durations and success rates. **Requires CLI first:** each metric slug must be defined on the server via `owlmetry metrics create` (use the `/owlmetry-cli` skill) before the SDK can emit events for it.
|
|
106
|
+
3. **Funnel tracking** — Identify user journeys (onboarding, checkout, key conversions). Add `Owl.track()` calls at each step to measure drop-off. **Requires CLI first:** the funnel definition (with steps and event filters) must be created via `owlmetry funnels create` (use the `/owlmetry-cli` skill) before tracking makes sense.
|
|
107
|
+
|
|
108
|
+
Wait for the user to choose before proceeding. For whichever option they pick, do a thorough audit of the entire codebase to find all relevant locations, then present a summary of proposed changes before making any edits.
|
|
109
|
+
|
|
84
110
|
## Log Events
|
|
85
111
|
|
|
86
112
|
Events are the core unit of data in OwlMetry. Use the four log levels to capture different kinds of information:
|
|
@@ -224,16 +250,19 @@ Owl.clearExperiments()
|
|
|
224
250
|
|
|
225
251
|
When instrumenting a new app, follow this priority:
|
|
226
252
|
|
|
227
|
-
**Always instrument:**
|
|
253
|
+
**Always instrument (events — no CLI setup needed):**
|
|
228
254
|
- App launch / cold start (`info` in `init()` or `didFinishLaunching`)
|
|
229
255
|
- Key screen views (`info` with `screenName` in `onAppear`)
|
|
230
256
|
- Authentication events (login, logout, signup)
|
|
231
257
|
- Errors and failures (`error` in `catch` blocks, error handlers)
|
|
232
258
|
- Core business actions (purchase, share, create, delete)
|
|
233
259
|
|
|
234
|
-
**Instrument when relevant:**
|
|
235
|
-
-
|
|
236
|
-
-
|
|
260
|
+
**Instrument when relevant (metrics — requires CLI `owlmetry metrics create` first):**
|
|
261
|
+
- Lifecycle metrics for operations where duration matters: image uploads, API calls, data syncs, video encoding
|
|
262
|
+
- Single-shot metrics for point-in-time values: app cold-start time, memory usage, items in cart
|
|
263
|
+
|
|
264
|
+
**Instrument when relevant (funnels — requires CLI `owlmetry funnels create` first):**
|
|
265
|
+
- Multi-step flows you want to measure conversion on: onboarding, checkout, activation
|
|
237
266
|
- A/B experiments when testing alternative UI or flows
|
|
238
267
|
|
|
239
268
|
**Where to place calls:**
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmetry/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "OwlMetry CLI — manage projects, apps, metrics, funnels, and events from the terminal. Includes AI skill files for agent-assisted development.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/Jasonvdb/owlmetry.git",
|
|
9
|
+
"url": "git+https://github.com/Jasonvdb/owlmetry.git",
|
|
10
10
|
"directory": "apps/cli"
|
|
11
11
|
},
|
|
12
12
|
"homepage": "https://owlmetry.com",
|