@alisio/plugin-telemetry 0.1.0

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.
Files changed (53) hide show
  1. package/.agents/skills/telemetry/SKILL.md +52 -0
  2. package/LICENSE +21 -0
  3. package/README.md +188 -0
  4. package/cover.svg +54 -0
  5. package/dist/commands.d.ts +19 -0
  6. package/dist/commands.d.ts.map +1 -0
  7. package/dist/commands.js +298 -0
  8. package/dist/commands.js.map +1 -0
  9. package/dist/config.d.ts +145 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +524 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/database.d.ts +16 -0
  14. package/dist/database.d.ts.map +1 -0
  15. package/dist/database.js +67 -0
  16. package/dist/database.js.map +1 -0
  17. package/dist/events.d.ts +135 -0
  18. package/dist/events.d.ts.map +1 -0
  19. package/dist/events.js +255 -0
  20. package/dist/events.js.map +1 -0
  21. package/dist/format.d.ts +36 -0
  22. package/dist/format.d.ts.map +1 -0
  23. package/dist/format.js +49 -0
  24. package/dist/format.js.map +1 -0
  25. package/dist/index.d.ts +38 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +78 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/otlp.d.ts +69 -0
  30. package/dist/otlp.d.ts.map +1 -0
  31. package/dist/otlp.js +616 -0
  32. package/dist/otlp.js.map +1 -0
  33. package/dist/redact.d.ts +36 -0
  34. package/dist/redact.d.ts.map +1 -0
  35. package/dist/redact.js +163 -0
  36. package/dist/redact.js.map +1 -0
  37. package/dist/runtime.d.ts +80 -0
  38. package/dist/runtime.d.ts.map +1 -0
  39. package/dist/runtime.js +290 -0
  40. package/dist/runtime.js.map +1 -0
  41. package/dist/store.d.ts +188 -0
  42. package/dist/store.d.ts.map +1 -0
  43. package/dist/store.js +539 -0
  44. package/dist/store.js.map +1 -0
  45. package/dist/tools.d.ts +21 -0
  46. package/dist/tools.d.ts.map +1 -0
  47. package/dist/tools.js +292 -0
  48. package/dist/tools.js.map +1 -0
  49. package/dist/version.d.ts +3 -0
  50. package/dist/version.d.ts.map +1 -0
  51. package/dist/version.js +3 -0
  52. package/dist/version.js.map +1 -0
  53. package/package.json +54 -0
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: telemetry
3
+ description: "Trigger: latency, token cost, tool failures, regressions, usage questions. Read bounded telemetry summaries from the local store before guessing."
4
+ license: Apache-2.0
5
+ metadata:
6
+ author: "alisio-contributors"
7
+ version: "1.0"
8
+ ---
9
+
10
+ ## Activation Contract
11
+
12
+ Load this skill when a question is about how the agent has been performing, costing, or failing:
13
+ slow turns, token usage, model mix, tool error rate, recent sessions, or a suspected regression.
14
+ Telemetry is local-first and privacy-preserving; consult the local store through the read-only
15
+ `telemetry_*` tools instead of guessing.
16
+
17
+ ## Hard Rules
18
+
19
+ - Telemetry is evidence about past runs, not a substitute for reading the current code. Verify a
20
+ regression against the actual change before blaming it.
21
+ - Every tool returns a bounds envelope (`returned`, `total`, `truncated`, `hint`). When `truncated`
22
+ is true, narrow the time window or raise `limit`; never assume the visible slice is the whole story.
23
+ - Content search only returns text when content capture is explicitly enabled. When capture is off,
24
+ answer from metadata (counts, tokens, latency) and say that content is not recorded.
25
+ - Telemetry can reveal how someone works, which repositories they use, and what they type. Treat any
26
+ captured content as sensitive: do not paste it elsewhere, and never enable capture without the
27
+ operator's explicit decision.
28
+ - Remote export is opt-in. Local-only is the default; do not claim data left the machine unless
29
+ remote export is configured and a flush succeeded.
30
+ - Never ask for or handle the OTLP credential. It exists only as an environment variable read at
31
+ export time.
32
+
33
+ ## Decision Gates
34
+
35
+ | Question | Tool | Notes |
36
+ | --- | --- | --- |
37
+ | Overall volume, errors, latency | `telemetry_summary` | Start here; default window is one day |
38
+ | Which models, and their token cost | `telemetry_models` | Compare input/output and cached input |
39
+ | Which tools fail or get slow | `telemetry_tools` | Look at error rate and effects |
40
+ | What happened recently | `telemetry_sessions` | Bounded session/run metadata |
41
+ | Find a specific captured text | `telemetry_search` | Requires content capture |
42
+
43
+ ## Execution Steps
44
+
45
+ 1. Pick a bounded window (`windowMinutes`) and a small `limit`; start with `telemetry_summary`.
46
+ 2. Drill into `telemetry_models` and `telemetry_tools` to locate the regression.
47
+ 3. Correlate timing with a code change, then confirm by reading the code.
48
+ 4. If a store is empty or unavailable, say so plainly and continue without telemetry.
49
+
50
+ ## References
51
+
52
+ - `README.md` in this package documents configuration, privacy defaults, and deletion.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gustavo Gutiérrez Mercado
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # @alisio/plugin-telemetry
2
+
3
+ Privacy-first local agent observability for
4
+ [Alisio](https://github.com/GustavoGutierrez/alisio). It records what the agent
5
+ does into a local SQLite database, exposes bounded read-only query tools, and can
6
+ optionally export OpenTelemetry-aligned data over OTLP. Local-only is the default;
7
+ remote export is opt-in.
8
+
9
+ This is an independent plugin. It is not affiliated with the OpenTelemetry
10
+ project and claims no conformance to a stable semantic-convention release.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ alisio install npm:@alisio/plugin-telemetry
16
+ ```
17
+
18
+ ## What it does
19
+
20
+ - Observes Alisio `RunEvent`s through a synchronous, non-blocking handler and
21
+ writes compact records to a local SQLite database.
22
+ - Serves five read-only tools that return bounded, aggregated, privacy-preserving
23
+ summaries.
24
+ - Optionally exports traces, logs and metrics to an OTLP/HTTP endpoint with
25
+ durable, idempotent batching.
26
+ - Ships a `telemetry` agent skill explaining when and how to consult it.
27
+
28
+ ## Privacy defaults
29
+
30
+ - **Metadata-only by default.** Prompt, completion, tool-argument and tool-result
31
+ content capture is OFF unless explicitly enabled.
32
+ - **Local-only by default.** No network call is made until remote export is
33
+ enabled with an endpoint.
34
+ - When content capture is enabled, every captured value passes through
35
+ allowlist-based redaction: bearer/authorization headers, tokens, private keys,
36
+ secrets in key/value form, absolute machine paths and (in `strict` mode) long
37
+ high-entropy runs are stripped, and every field is bounded. Redaction is never
38
+ denylist-only.
39
+ - **The OTLP credential is never persisted, logged, returned or serialized.** It
40
+ is read at export time only, from the environment variable named by
41
+ `otlp.tokenEnv` (default `ALISIO_TELEMETRY_OTLP_TOKEN`). The config file may
42
+ only reference the variable's name.
43
+ - Telemetry can reveal how someone works, which repositories they use, and what
44
+ they type. Keep `strict` redaction on, prefer metadata-only, and set a
45
+ retention window you are comfortable with.
46
+
47
+ ## Configuration
48
+
49
+ Configuration precedence, highest first:
50
+
51
+ 1. Explicit non-secret command/tool argument (for example
52
+ `/telemetry:telemetry-setup --retention-days 7`).
53
+ 2. Environment variables.
54
+ 3. The config file at `<configHome>/telemetry/config.json`.
55
+ 4. Built-in safe defaults.
56
+
57
+ `configHome` resolves as `ALISIO_CONFIG_HOME`, else `XDG_CONFIG_HOME/alisio`,
58
+ else `~/.config/alisio`. `stateHome` resolves as `ALISIO_STATE_HOME`, else
59
+ `XDG_STATE_HOME/alisio`, else `~/.local/state/alisio`.
60
+
61
+ The database path is `ALISIO_TELEMETRY_DB`, else
62
+ `<stateHome>/telemetry/telemetry.sqlite`. Telemetry stores its data through the
63
+ host-provided SQLite storage port (`api.storage.sqlite`), so the plugin never
64
+ depends on a runtime SQLite driver. Because the host port sets no pragmas, the
65
+ plugin hardens the port right after opening: it enables WAL, a `busy_timeout`,
66
+ foreign keys and `NORMAL` synchronous mode, ensures parent directories are
67
+ `0700` and the database file is `0600`, and periodically runs `PRAGMA optimize`.
68
+
69
+ An invalid configuration **fails closed**: telemetry is disabled and an actionable
70
+ message is reported. It never crashes the host.
71
+
72
+ ### Settings
73
+
74
+ | Field | Meaning |
75
+ | --- | --- |
76
+ | `retentionDays` | How long records are kept before `telemetry-prune` removes them |
77
+ | `capture.prompts` / `.completions` / `.toolArguments` / `.toolResults` | Opt-in content capture (redacted, bounded) |
78
+ | `redaction.mode` | `strict` (default) or `standard` |
79
+ | `otlp.enabled` | Master switch for remote export |
80
+ | `otlp.endpoint` | OTLP/HTTP base URL, e.g. `https://collector.example.com:4318` |
81
+ | `otlp.headers` | Non-secret headers only; secret-shaped headers are refused |
82
+ | `otlp.tokenEnv` | Name of the environment variable that holds the token |
83
+ | `otlp.serviceName` / `.environment` / `.instanceId` | Resource attributes |
84
+ | `otlp.samplingRatio` | Deterministic per-run sampling, `0`–`1` |
85
+ | `otlp.signals.traces` / `.logs` / `.metrics` | Which signals to export |
86
+ | `otlp.gzip` | Gzip the request body |
87
+ | `otlp.maxAttempts` / `.timeoutMs` / `.batchSize` | Export bounds |
88
+ | `batch.batchSize` / `.flushIntervalMs` / `.maxQueue` | Local write bounds |
89
+
90
+ ### Environment variables
91
+
92
+ `ALISIO_TELEMETRY_DB`, `ALISIO_CONFIG_HOME`, `ALISIO_STATE_HOME`,
93
+ `XDG_CONFIG_HOME`, `XDG_STATE_HOME`, `ALISIO_TELEMETRY_RETENTION_DAYS`,
94
+ `ALISIO_TELEMETRY_OTLP_ENABLED`, `ALISIO_TELEMETRY_OTLP_ENDPOINT`,
95
+ `ALISIO_TELEMETRY_OTLP_TOKEN_ENV`, `ALISIO_TELEMETRY_OTLP_INSTANCE_ID`,
96
+ `ALISIO_TELEMETRY_SERVICE_NAME`, `ALISIO_TELEMETRY_ENVIRONMENT`,
97
+ `ALISIO_TELEMETRY_SAMPLING_RATIO`, `ALISIO_TELEMETRY_CAPTURE_PROMPTS`,
98
+ `ALISIO_TELEMETRY_CAPTURE_COMPLETIONS`, `ALISIO_TELEMETRY_CAPTURE_TOOL_ARGUMENTS`,
99
+ `ALISIO_TELEMETRY_CAPTURE_TOOL_RESULTS`, `ALISIO_TELEMETRY_REDACTION_MODE`,
100
+ `ALISIO_TELEMETRY_OTLP_SIGNALS`, `ALISIO_TELEMETRY_OTLP_GZIP`,
101
+ `ALISIO_TELEMETRY_BATCH_SIZE`, `ALISIO_TELEMETRY_FLUSH_INTERVAL_MS`,
102
+ `ALISIO_TELEMETRY_MAX_QUEUE`.
103
+
104
+ The token itself is read only from `ALISIO_TELEMETRY_OTLP_TOKEN` (or the variable
105
+ named by `otlp.tokenEnv`).
106
+
107
+ ## Tools
108
+
109
+ All tools are read-only (`read`), closed-schema, and return a bounds envelope
110
+ (`window`, `returned`, `total`, `truncated`, `hint`). They never dump raw events
111
+ and never expose secrets or absolute machine paths.
112
+
113
+ | Tool | Returns |
114
+ | --- | --- |
115
+ | `telemetry_summary` | Sessions, runs, turns, tokens, tool errors, latency and a time series |
116
+ | `telemetry_models` | Model mix with token usage, calls and average turn duration |
117
+ | `telemetry_tools` | Tool call counts, error rate, latency and effects |
118
+ | `telemetry_sessions` | Recent sessions/runs with bounded metadata |
119
+ | `telemetry_search` | Bounded content search; only returns content when capture is enabled |
120
+
121
+ ## Commands
122
+
123
+ Alisio namespaces external plugin commands as `<plugin id>:<name>`, so the real
124
+ invocation form is:
125
+
126
+ ```
127
+ /telemetry:telemetry-setup [options]
128
+ /telemetry:telemetry-status
129
+ /telemetry:telemetry-flush
130
+ /telemetry:telemetry-prune [--days <n>]
131
+ ```
132
+
133
+ `telemetry-setup` persists configuration non-interactively and reports exactly
134
+ which fields changed, without ever echoing a credential. Use `--json` on any of
135
+ these for machine-readable output.
136
+
137
+ ## OpenTelemetry alignment
138
+
139
+ The plugin emits OTLP/HTTP JSON to `/v1/traces`, `/v1/logs` and `/v1/metrics`
140
+ under the configured base endpoint. It uses standard names where they exist, for
141
+ example `gen_ai.operation.name`, `gen_ai.provider.name`, `gen_ai.request.model`,
142
+ `gen_ai.response.model`, `gen_ai.usage.input_tokens`,
143
+ `gen_ai.usage.output_tokens`, `gen_ai.usage.cached_input_tokens`,
144
+ `gen_ai.tool.name`, `gen_ai.tool.call.id`, `gen_ai.conversation.id`, and the
145
+ stable core attributes `error.type`, `server.address` and `server.port`. Span
146
+ names follow `{gen_ai.operation.name} {target}`, using `invoke_agent`, `chat` and
147
+ `execute_tool`.
148
+
149
+ **Honesty constraint.** Every `gen_ai.*` semantic convention is currently
150
+ Development/experimental, not stable, and the GenAI conventions moved out of the
151
+ main semantic-conventions repository. There is no stable release for metrics.
152
+ These attribute names may change and this plugin claims no stability. Where no
153
+ standard exists — cost, retry rate, cache-hit ratio, approval outcomes, context
154
+ growth — the plugin uses clearly namespaced `alisio.telemetry.*` attributes or
155
+ metrics instead of inventing `gen_ai.*` names. Cost and approval outcomes are not
156
+ captured at all.
157
+
158
+ Export behavior: HTTP 200 (including a partial-success body) is success and is
159
+ never retried; only `429`, `502`, `503` and `504` are retried, with bounded
160
+ attempts and floor-ed exponential backoff honoring `Retry-After`. Redirects are
161
+ refused and response bodies are read through a hard cap. Payloads are persisted as
162
+ batches keyed by their content hash before any network call, so a failed export
163
+ stays queued and a retry is idempotent — data is neither lost nor duplicated
164
+ silently. Non-retryable responses are marked `dead` and surfaced in
165
+ `telemetry-status` rather than discarded.
166
+
167
+ ## Deleting telemetry
168
+
169
+ `/telemetry:telemetry-prune [--days <n>]` deletes runs, turns, tool calls and
170
+ content older than the cutoff, purges the full-text search index, then runs
171
+ `optimize` and `VACUUM`. To remove everything, run it with a cutoff in the
172
+ future (for example `--days 0`), or delete the SQLite file directly.
173
+
174
+ **FTS caveat:** the full-text search shadow tables (`content_fts\*`) can retain
175
+ fragments even after rows are deleted; prune clears the index and vacuums, but a
176
+ copy of the database made earlier may still contain forensic traces. Treat any
177
+ captured content as sensitive.
178
+
179
+ ## Requirements
180
+
181
+ Node.js **>= 22.16** and an Alisio host with `@alisio/sdk`
182
+ `>=0.1.0-alpha.10` that provides the SQLite storage port
183
+ (`api.storage.sqlite`). Zero runtime dependencies: Node built-ins and native
184
+ `fetch` only.
185
+
186
+ ## License
187
+
188
+ MIT.
package/cover.svg ADDED
@@ -0,0 +1,54 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1600" height="900" viewBox="0 0 1600 900" role="img" aria-labelledby="title desc">
2
+ <title id="title">Telemetry</title>
3
+ <desc id="desc">A dashboard with a token-usage chart, tool-call bars, and a local database cylinder feeding an opt-in export arrow.</desc>
4
+ <defs>
5
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
6
+ <stop offset="0" stop-color="#07111f"/>
7
+ <stop offset="1" stop-color="#0d1c31"/>
8
+ </linearGradient>
9
+ <linearGradient id="accent" x1="0" y1="0" x2="1" y2="0">
10
+ <stop offset="0" stop-color="#22d3ee"/>
11
+ <stop offset="1" stop-color="#6366f1"/>
12
+ </linearGradient>
13
+ <linearGradient id="area" x1="0" y1="0" x2="0" y2="1">
14
+ <stop offset="0" stop-color="#22d3ee" stop-opacity="0.55"/>
15
+ <stop offset="1" stop-color="#22d3ee" stop-opacity="0.02"/>
16
+ </linearGradient>
17
+ </defs>
18
+ <rect width="1600" height="900" fill="url(#bg)"/>
19
+ <rect x="120" y="110" width="940" height="520" rx="32" fill="#0b1a2e" stroke="#1e3a5f" stroke-width="6"/>
20
+ <rect x="170" y="160" width="250" height="22" rx="11" fill="#94a3b8" opacity="0.55"/>
21
+ <rect x="170" y="196" width="150" height="22" rx="11" fill="#38bdf8" opacity="0.75"/>
22
+ <path d="M180 540 L320 470 L460 500 L600 370 L740 410 L880 300 L1020 340" fill="none" stroke="url(#accent)" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
23
+ <path d="M180 540 L320 470 L460 500 L600 370 L740 410 L880 300 L1020 340 L1020 560 L180 560 Z" fill="url(#area)"/>
24
+ <g fill="#1b2f4d" stroke="#2b4a76" stroke-width="4">
25
+ <rect x="170" y="250" width="70" height="290" rx="10"/>
26
+ <rect x="270" y="320" width="70" height="220" rx="10"/>
27
+ <rect x="370" y="290" width="70" height="250" rx="10"/>
28
+ <rect x="470" y="380" width="70" height="160" rx="10"/>
29
+ <rect x="570" y="300" width="70" height="240" rx="10"/>
30
+ </g>
31
+ <g fill="#38bdf8" opacity="0.85">
32
+ <rect x="170" y="470" width="70" height="70" rx="10"/>
33
+ <rect x="270" y="470" width="70" height="70" rx="10"/>
34
+ <rect x="370" y="470" width="70" height="70" rx="10"/>
35
+ <rect x="470" y="470" width="70" height="70" rx="10"/>
36
+ <rect x="570" y="470" width="70" height="70" rx="10"/>
37
+ </g>
38
+ <g transform="translate(1140 150)">
39
+ <ellipse cx="160" cy="70" rx="150" ry="46" fill="#132a45" stroke="#2b4a76" stroke-width="6"/>
40
+ <path d="M10 70v300c0 25 67 46 150 46s150-21 150-46V70" fill="#0f2138" stroke="#2b4a76" stroke-width="6"/>
41
+ <path d="M10 170c0 25 67 46 150 46s150-21 150-46M10 270c0 25 67 46 150 46s150-21 150-46" fill="none" stroke="#2b4a76" stroke-width="6"/>
42
+ <ellipse cx="160" cy="70" rx="118" ry="30" fill="none" stroke="#22d3ee" stroke-width="6" opacity="0.8"/>
43
+ </g>
44
+ <path d="M1080 420h140" stroke="#475569" stroke-width="12" stroke-linecap="round" stroke-dasharray="4 24"/>
45
+ <path d="M1330 420h150" stroke="url(#accent)" stroke-width="12" stroke-linecap="round"/>
46
+ <path d="M1478 388l52 32-52 32z" fill="#6366f1"/>
47
+ <g transform="translate(1150 560)">
48
+ <path d="M20 46a42 42 0 0 1 84 0" fill="none" stroke="url(#accent)" stroke-width="12" stroke-linecap="round"/>
49
+ <rect x="0" y="44" width="124" height="70" rx="16" fill="#0f2138" stroke="url(#accent)" stroke-width="12"/>
50
+ <circle cx="62" cy="82" r="10" fill="#22d3ee"/>
51
+ </g>
52
+ <text x="800" y="790" fill="#f8fafc" font-family="sans-serif" font-size="82" font-weight="700" text-anchor="middle">TELEMETRY</text>
53
+ <text x="800" y="850" fill="#94a3b8" font-family="sans-serif" font-size="30" text-anchor="middle">local-first agent observability, opt-in OTLP export</text>
54
+ </svg>
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Operator commands: setup, status, maintenance.
3
+ *
4
+ * Alisio namespaces external plugin commands as `<plugin id>:<name>`. This
5
+ * plugin registers the names below, so the real invocation form is
6
+ * `/telemetry:telemetry-setup`, `/telemetry:telemetry-status`,
7
+ * `/telemetry:telemetry-flush` and `/telemetry:telemetry-prune`.
8
+ *
9
+ * `telemetry-setup` persists configuration non-interactively and reports exactly
10
+ * which fields changed. It never echoes a credential: the OTLP token is only
11
+ * ever referenced by the name of the environment variable that holds it.
12
+ */
13
+ import type { PluginAPI } from "@alisio/sdk";
14
+ import type { TelemetryRuntime } from "./runtime.js";
15
+ export declare const COMMAND_NAMES: readonly ["telemetry-setup", "telemetry-status", "telemetry-flush", "telemetry-prune"];
16
+ /** Replace a home-directory prefix with `~` so a path is not machine-specific. */
17
+ export declare function displayPath(path: string, home: string | undefined): string;
18
+ export declare function registerCommands(api: PluginAPI, runtime: TelemetryRuntime): void;
19
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAkB,SAAS,EAAE,MAAM,aAAa,CAAC;AAU7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,eAAO,MAAM,aAAa,YACxB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,CACT,CAAC;AAiKX,kFAAkF;AAClF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAI1E;AAMD,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,gBAAgB,GAAG,IAAI,CA0JhF"}
@@ -0,0 +1,298 @@
1
+ import { ConfigError, createDefaultConfig, describeConfig, updateConfig, writeConfigFile, } from "./config.js";
2
+ import { sanitizeField } from "./format.js";
3
+ export const COMMAND_NAMES = [
4
+ "telemetry-setup",
5
+ "telemetry-status",
6
+ "telemetry-flush",
7
+ "telemetry-prune",
8
+ ];
9
+ const SETUP_USAGE = [
10
+ "Usage: /telemetry:telemetry-setup [options]",
11
+ " --enable-remote | --disable-remote",
12
+ " --endpoint <url>",
13
+ " --token-env <ENV_VAR_NAME> (the value is never stored)",
14
+ " --service-name <name> --environment <name> --instance-id <id>",
15
+ " --sampling <0..1> --retention-days <n>",
16
+ " --capture <prompts,completions,tool-arguments,tool-results|none>",
17
+ " --redaction <strict|standard>",
18
+ " --flush-interval-ms <n> --batch-size <n> --otlp-batch-size <n>",
19
+ " --gzip | --no-gzip --json",
20
+ ].join("\n");
21
+ function parseArguments(args) {
22
+ const tokens = args.trim() === "" ? [] : args.trim().split(/\s+/);
23
+ const values = new Map();
24
+ const flags = new Set();
25
+ for (let index = 0; index < tokens.length; index += 1) {
26
+ const token = tokens[index];
27
+ if (!token.startsWith("--"))
28
+ return { values, flags };
29
+ const key = token.slice(2);
30
+ if (key === "")
31
+ return { values, flags };
32
+ if (["enable-remote", "disable-remote", "gzip", "no-gzip", "json"].includes(key)) {
33
+ flags.add(key);
34
+ continue;
35
+ }
36
+ const value = tokens[index + 1];
37
+ if (value === undefined || value.startsWith("--")) {
38
+ values.set(key, "");
39
+ continue;
40
+ }
41
+ values.set(key, value);
42
+ index += 1;
43
+ }
44
+ return { values, flags };
45
+ }
46
+ function integerValue(parsed, key) {
47
+ const raw = parsed.values.get(key);
48
+ if (raw === undefined || raw === "")
49
+ return undefined;
50
+ if (!/^-?\d+$/.test(raw))
51
+ throw new ConfigError(`--${key}: expected an integer`);
52
+ return Number(raw);
53
+ }
54
+ function ratioValue(parsed, key) {
55
+ const raw = parsed.values.get(key);
56
+ if (raw === undefined || raw === "")
57
+ return undefined;
58
+ const value = Number(raw);
59
+ if (!Number.isFinite(value) || value < 0 || value > 1)
60
+ throw new ConfigError(`--${key}: expected a number between 0 and 1`);
61
+ return value;
62
+ }
63
+ function captureValue(parsed) {
64
+ const raw = parsed.values.get("capture");
65
+ if (raw === undefined)
66
+ return undefined;
67
+ if (raw === "none" || raw === "")
68
+ return { prompts: false, completions: false, toolArguments: false, toolResults: false };
69
+ const enabled = new Set(raw.split(",").map((value) => value.trim().toLowerCase()));
70
+ const known = ["prompts", "completions", "tool-arguments", "tool-results"];
71
+ for (const value of enabled) {
72
+ if (!known.includes(value))
73
+ throw new ConfigError(`--capture: unknown value "${value}"`);
74
+ }
75
+ return {
76
+ prompts: enabled.has("prompts"),
77
+ completions: enabled.has("completions"),
78
+ toolArguments: enabled.has("tool-arguments"),
79
+ toolResults: enabled.has("tool-results"),
80
+ };
81
+ }
82
+ function buildOverrides(parsed) {
83
+ const overrides = {};
84
+ if (parsed.flags.has("enable-remote") || parsed.flags.has("disable-remote")) {
85
+ overrides.otlp = {
86
+ ...(overrides.otlp ?? {}),
87
+ enabled: parsed.flags.has("enable-remote"),
88
+ };
89
+ }
90
+ const endpoint = parsed.values.get("endpoint");
91
+ if (endpoint !== undefined && endpoint !== "") {
92
+ overrides.otlp = { ...(overrides.otlp ?? {}), endpoint };
93
+ }
94
+ const tokenEnv = parsed.values.get("token-env");
95
+ if (tokenEnv !== undefined && tokenEnv !== "") {
96
+ overrides.otlp = { ...(overrides.otlp ?? {}), tokenEnv };
97
+ }
98
+ const serviceName = parsed.values.get("service-name");
99
+ if (serviceName !== undefined && serviceName !== "") {
100
+ overrides.otlp = { ...(overrides.otlp ?? {}), serviceName };
101
+ }
102
+ const environment = parsed.values.get("environment");
103
+ if (environment !== undefined) {
104
+ overrides.otlp = { ...(overrides.otlp ?? {}), environment };
105
+ }
106
+ const instanceId = parsed.values.get("instance-id");
107
+ if (instanceId !== undefined) {
108
+ overrides.otlp = { ...(overrides.otlp ?? {}), instanceId };
109
+ }
110
+ const sampling = ratioValue(parsed, "sampling");
111
+ if (sampling !== undefined)
112
+ overrides.otlp = { ...(overrides.otlp ?? {}), samplingRatio: sampling };
113
+ if (parsed.flags.has("gzip") || parsed.flags.has("no-gzip")) {
114
+ overrides.otlp = { ...(overrides.otlp ?? {}), gzip: parsed.flags.has("gzip") };
115
+ }
116
+ const otlpBatchSize = integerValue(parsed, "otlp-batch-size");
117
+ if (otlpBatchSize !== undefined)
118
+ overrides.otlp = { ...(overrides.otlp ?? {}), batchSize: otlpBatchSize };
119
+ const retentionDays = integerValue(parsed, "retention-days");
120
+ if (retentionDays !== undefined)
121
+ overrides.retentionDays = retentionDays;
122
+ const capture = captureValue(parsed);
123
+ if (capture !== undefined)
124
+ overrides.capture = capture;
125
+ const redaction = parsed.values.get("redaction");
126
+ if (redaction !== undefined && redaction !== "") {
127
+ if (redaction !== "strict" && redaction !== "standard")
128
+ throw new ConfigError("--redaction: expected strict or standard");
129
+ overrides.redaction = { mode: redaction };
130
+ }
131
+ const flushIntervalMs = integerValue(parsed, "flush-interval-ms");
132
+ if (flushIntervalMs !== undefined)
133
+ overrides.batch = { ...(overrides.batch ?? {}), flushIntervalMs };
134
+ const batchSize = integerValue(parsed, "batch-size");
135
+ if (batchSize !== undefined)
136
+ overrides.batch = { ...(overrides.batch ?? {}), batchSize };
137
+ return overrides;
138
+ }
139
+ /** Flatten a config to comparable dot-path primitives, without credentials. */
140
+ function flatten(value, prefix = "") {
141
+ if (value === null || typeof value !== "object")
142
+ return { [prefix]: JSON.stringify(value) };
143
+ const out = {};
144
+ for (const [key, item] of Object.entries(value)) {
145
+ const path = prefix === "" ? key : `${prefix}.${key}`;
146
+ Object.assign(out, flatten(item, path));
147
+ }
148
+ return out;
149
+ }
150
+ function diffConfig(before, after) {
151
+ const left = flatten(before);
152
+ const right = flatten(after);
153
+ const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
154
+ const changes = [];
155
+ for (const key of [...keys].sort()) {
156
+ if (left[key] !== right[key])
157
+ changes.push(`${key}: ${left[key] ?? "(unset)"} -> ${right[key] ?? "(unset)"}`);
158
+ }
159
+ return changes;
160
+ }
161
+ /** Replace a home-directory prefix with `~` so a path is not machine-specific. */
162
+ export function displayPath(path, home) {
163
+ if (home !== undefined && home !== "" && path.startsWith(home))
164
+ return `~${path.slice(home.length)}`;
165
+ return path;
166
+ }
167
+ function asJson(parsed) {
168
+ return parsed.flags.has("json");
169
+ }
170
+ export function registerCommands(api, runtime) {
171
+ const register = (name, description, argumentHint, handler) => {
172
+ api.commands.register(name, handler, { description, argumentHint });
173
+ };
174
+ register("telemetry-setup", "Persist telemetry configuration non-interactively", "[options]", async (args) => {
175
+ const parsed = parseArguments(args);
176
+ try {
177
+ const base = runtime.config ?? createDefaultConfig();
178
+ const overrides = buildOverrides(parsed);
179
+ const next = updateConfig(base, overrides);
180
+ writeConfigFile(runtime.paths.configFile, next);
181
+ const before = runtime.config ? describeConfig(runtime.config, runtime.env) : null;
182
+ const after = describeConfig(next, runtime.env);
183
+ const changes = before === null ? ["configuration file created"] : diffConfig(before, after);
184
+ const payload = {
185
+ saved: true,
186
+ configFile: displayPath(runtime.paths.configFile, runtime.env.HOME),
187
+ changed: changes,
188
+ restartRequired: true,
189
+ note: "The OTLP token is never stored. It is read at export time from the environment variable named below.",
190
+ tokenEnv: next.otlp.tokenEnv,
191
+ };
192
+ if (asJson(parsed))
193
+ return JSON.stringify(payload);
194
+ return [
195
+ `Telemetry configuration saved to ${payload.configFile}.`,
196
+ changes.length === 0 ? "No fields changed." : "Changed:",
197
+ ...changes.map((change) => ` ${change}`),
198
+ `Token environment variable: ${payload.tokenEnv} (value never stored).`,
199
+ "Restart Alisio to apply the new settings.",
200
+ ].join("\n");
201
+ }
202
+ catch (error) {
203
+ const message = error instanceof ConfigError ? error.message : "configuration could not be saved";
204
+ return asJson(parsed)
205
+ ? JSON.stringify({ saved: false, error: message })
206
+ : `Telemetry setup failed: ${message}\n${SETUP_USAGE}`;
207
+ }
208
+ });
209
+ register("telemetry-status", "Show effective telemetry configuration and local store stats", "", async (args) => {
210
+ const parsed = parseArguments(args);
211
+ const counts = runtime.store?.counts() ?? null;
212
+ const payload = {
213
+ enabled: runtime.store !== null,
214
+ error: runtime.configError,
215
+ config: runtime.config ? describeConfig(runtime.config, runtime.env) : null,
216
+ database: displayPath(runtime.paths.database, runtime.env.HOME),
217
+ configFile: displayPath(runtime.paths.configFile, runtime.env.HOME),
218
+ remoteExport: runtime.exporter !== null,
219
+ counts,
220
+ };
221
+ if (asJson(parsed))
222
+ return JSON.stringify(payload);
223
+ if (runtime.configError !== null) {
224
+ return `Telemetry is disabled: ${runtime.configError}\nConfig file: ${payload.configFile}\nDatabase: ${payload.database}`;
225
+ }
226
+ const config = payload.config ?? {};
227
+ return [
228
+ "Telemetry status",
229
+ ` local store: ${runtime.store === null ? "unavailable" : "open"}`,
230
+ ` remote export: ${payload.remoteExport ? "enabled" : "disabled (local-only)"}`,
231
+ ` database: ${payload.database}`,
232
+ ` config file: ${payload.configFile}`,
233
+ ` retention: ${config.retentionDays ?? "-"} day(s)`,
234
+ ` counts: ${counts ? `${counts.runs} run(s), ${counts.turns} turn(s), ${counts.toolCalls} tool call(s), ${counts.content} content row(s)` : "-"}`,
235
+ ` batches: ${counts ? `${counts.pendingBatches} pending, ${counts.deadBatches} dead` : "-"}`,
236
+ " content capture is metadata-only unless explicitly enabled; the OTLP token is never stored.",
237
+ ].join("\n");
238
+ });
239
+ register("telemetry-flush", "Flush queued records and attempt remote export", "", async (args) => {
240
+ const parsed = parseArguments(args);
241
+ const report = await runtime.flush();
242
+ const payload = {
243
+ records: report.records,
244
+ content: report.content,
245
+ dropped: report.dropped,
246
+ queueSize: report.queueSize,
247
+ optimized: report.optimized,
248
+ error: report.error,
249
+ exports: report.exports.map((result) => ({
250
+ signal: result.signal,
251
+ status: result.status,
252
+ attempts: result.attempts,
253
+ httpStatus: result.httpStatus,
254
+ error: sanitizeField(result.error ?? "", 200),
255
+ })),
256
+ };
257
+ if (asJson(parsed))
258
+ return JSON.stringify(payload);
259
+ const lines = [
260
+ `Flushed ${report.records} record(s) (${report.content} content row(s)) to the local store.`,
261
+ `Queue: ${report.queueSize} pending, ${report.dropped} dropped since start.`,
262
+ report.error === null ? "No flush errors." : `Flush error: ${report.error}`,
263
+ ];
264
+ if (payload.exports.length === 0)
265
+ lines.push("Remote export is disabled (local-only); no network call was made.");
266
+ else
267
+ for (const result of payload.exports)
268
+ lines.push(` ${result.signal}: ${result.status} after ${result.attempts} attempt(s)${result.httpStatus === null ? "" : ` (HTTP ${result.httpStatus})`}`);
269
+ return lines.join("\n");
270
+ });
271
+ register("telemetry-prune", "Delete telemetry older than the retention window", "[--days <n>] [--json]", async (args) => {
272
+ const parsed = parseArguments(args);
273
+ const days = integerValue(parsed, "days");
274
+ try {
275
+ const report = runtime.prune(days);
276
+ const payload = {
277
+ cutoff: report.cutoff,
278
+ retentionDays: report.retentionDays,
279
+ deleted: report.deleted,
280
+ note: report.note,
281
+ };
282
+ if (asJson(parsed))
283
+ return JSON.stringify(payload);
284
+ return [
285
+ `Pruned telemetry older than ${report.retentionDays} day(s) (before ${report.cutoff}).`,
286
+ `Deleted ${report.deleted.runs} run(s), ${report.deleted.turns} turn(s), ${report.deleted.toolCalls} tool call(s), ${report.deleted.content} content row(s).`,
287
+ report.note,
288
+ ].join("\n");
289
+ }
290
+ catch (error) {
291
+ const message = error instanceof ConfigError ? error.message : "prune failed";
292
+ return asJson(parsed)
293
+ ? JSON.stringify({ pruned: false, error: message })
294
+ : `Telemetry prune failed: ${message}`;
295
+ }
296
+ });
297
+ }
298
+ //# sourceMappingURL=commands.js.map