spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

mods: the telemetry contract exports its types at top level, its hooks and tests importing them from the folder, so the engine's repository reads the same file by pathBrowse files
poteat authored on Sep 12, 2026 parent b95b015 · commit 485170e06b3e
8 changed files+116 -115compared with b95b015
Mmods/README.md
Mmods/telemetry/hooks/entries/is-mark-kind/is-mark-kind.ts
Mmods/telemetry/hooks/entries/mark/mark.ts
Mmods/telemetry/hooks/entries/mark-kinds/mark-kinds.ts
Mmods/telemetry/hooks/telemetry-of/telemetry-of.ts
Mmods/telemetry/tests/fixtures/record.ts
Mmods/telemetry/tests/fixtures/survey-answer.ts
Mmods/telemetry/types/index.d.ts
@@ -81,20 +81,21 @@
8181 ## Composing mods: noun contracts
8282
8383 A mod that adds a noun to `$` in the `engine.create` fold owns that noun's
84-types, and keeps them in one place: its `types/index.d.ts`, an ambient file
85-with no imports that merges into `claude-code`, declaring the noun on
86-`EngineInterface` and exporting the types it is made of, each named for the
87-noun (`telemetry/types/index.d.ts` declares `$.telemetry` and exports
88-`Telemetry`, `TelemetryLogEntry`, `TelemetryMarkEntry` and the rest).
84+types, and keeps them in one place: its `types/index.d.ts`, a declaration
85+file with no imports that exports the types the noun is made of, each named
86+for the noun, and declares the noun on `EngineInterface` in `claude-code`
87+(`telemetry/types/index.d.ts` exports `Telemetry`, `TelemetryLogEntry`,
88+`TelemetryMarkEntry` and the rest, and declares `$.telemetry`).
8989
9090 - The contract is the only declaration of the noun. The mod's own hooks
91- import its types from `claude-code` (`import type { Telemetry } from
92- 'claude-code'`), and the value its `engine.create` hook returns is checked
91+ import its types from the folder (`import type { Telemetry } from
92+ '../types'`), and the value its `engine.create` hook returns is checked
9393 against `EngineInterface['telemetry']`, so the implementation cannot drift
9494 from what callers read.
9595 - A mod that calls another's noun reads the same file and never copies it:
9696 `mods/tsconfig.json` includes `*/types/**/*.d.ts`, so `$.telemetry.log(…)`
97- in `diff` types against `telemetry`'s contract as it stands.
97+ in `diff` types against `telemetry`'s contract as it stands, and a helper
98+ that must name one of its types imports it from that folder by path.
9899 - A test of a mod that calls another's noun seats a provider for it, an inline
99100 plugin whose `engine.create` hook adds the noun, and answers the calls the
100101 way it answers the engine's: `on('telemetry.log', ($, e) => ({ value:
@@ -1,5 +1,4 @@
1-import type { TelemetryMarkKind } from 'claude-code'
2-
1+import type { TelemetryMarkKind } from '../../../types'
32 import { MARK_KINDS } from '../mark-kinds'
43
54 /**
@@ -1,5 +1,4 @@
1-import type { TelemetryMarkKind } from 'claude-code'
2-
1+import type { TelemetryMarkKind } from '../../../types'
32 import type { Fields } from '../fields'
43
54 /**
@@ -1,4 +1,4 @@
1-import type { TelemetryMarkKind } from 'claude-code'
1+import type { TelemetryMarkKind } from '../../../types'
22
33 /**
44 * The three kinds a mark may be, in the order the feature events name them.
@@ -1,5 +1,4 @@
1-import type { Telemetry } from 'claude-code'
2-
1+import type { Telemetry } from '../../types'
32 import Entries from '../entries'
43 import { isAnalyticsOff } from '../is-analytics-off'
54 import type { TelemetryDeps } from '../telemetry-deps'
@@ -1,5 +1,7 @@
1-import type { CommandRunInput, TelemetryLogEntry } from 'claude-code'
1+import type { CommandRunInput } from 'claude-code'
22
3+import type { TelemetryLogEntry } from '../../types'
4+
35 /**
46 * The command that has the recording plugin log an entry, typed as the
57 * person would type it.
@@ -1,4 +1,4 @@
1-import type { TelemetryLogEntry } from 'claude-code'
1+import type { TelemetryLogEntry } from '../../types'
22
33 /**
44 * A survey answered, as a plugin logs it.
@@ -1,115 +1,116 @@
11 /**
22 * The `$.telemetry` noun as every caller sees it: the one contract for the
3- * noun, merged into `claude-code` beside the engine's own declarations.
3+ * noun, its types exported here and the noun declared on `EngineInterface`.
44 *
55 * The telemetry mod adds the noun in the `engine.create` fold and checks its
6- * return against `EngineInterface['telemetry']`; a mod that calls it, and a
7- * test that answers it with `on('telemetry.log', …)`, read the same types by
8- * including this folder in their tsconfig. Nothing here is imported: the file
9- * is ambient, so it merges wherever it is included.
6+ * return against `EngineInterface['telemetry']`; its hooks import these types
7+ * from this folder, a mod that calls the noun and a test that answers it read
8+ * them by including it in their tsconfig, and the engine's repository imports
9+ * the folder by path. Nothing here is imported, so it stands on its own.
1010 */
11-declare module 'claude-code' {
12- interface EngineInterface {
13- /**
14- * A plugin's analytics, one first-party row per call; present only where
15- * the telemetry mod is seated (internal builds), absent everywhere else.
16- */
17- telemetry: Telemetry
18- }
1911
12+/**
13+ * A plugin's analytics, sent one event at a time through `$.telemetry`.
14+ *
15+ * Internal builds alone: the telemetry mod adds the noun in the
16+ * `engine.create` fold, so a plugin on an external build, or one where the
17+ * mod is off, finds no `$.telemetry` and its call throws.
18+ */
19+export type Telemetry = {
2020 /**
21- * A plugin's analytics, sent one event at a time through `$.telemetry`.
21+ * Sends one event, `tengu_plugin_<event>`, as one first-party row;
22+ * resolves once the ingest accepted it.
2223 *
23- * Internal builds alone: the telemetry mod adds the noun in the
24- * `engine.create` fold, so a plugin on an external build, or one where the
25- * mod is off, finds no `$.telemetry` and its call throws.
24+ * The calling mod names itself in `event`; one already named `tengu_…` is
25+ * sent as named. A value is a finite number, a boolean or a
26+ * TelemetryChoice; free text is refused. One input, as every op on `$`
27+ * takes.
28+ *
29+ * @param entry the event's name, a snake_case token, and its properties by
30+ * snake_case key
31+ * @example
32+ * await $.telemetry.log({
33+ * event: "suggest_learning_survey_answered",
34+ * props: {
35+ * answer: 2,
36+ * page: { value: "ready", of: ["ready", "later"] },
37+ * },
38+ * })
2639 */
27- export type Telemetry = {
28- /**
29- * Sends one event, `tengu_plugin_<event>`, as one first-party row;
30- * resolves once the ingest accepted it.
31- *
32- * The calling mod names itself in `event`; one already named `tengu_…` is
33- * sent as named. A value is a finite number, a boolean or a
34- * TelemetryChoice; free text is refused. One input, as every op on `$`
35- * takes.
36- *
37- * @param entry the event's name, a snake_case token, and its properties by
38- * snake_case key
39- * @example
40- * await $.telemetry.log({
41- * event: "suggest_learning_survey_answered",
42- * props: {
43- * answer: 2,
44- * page: { value: "ready", of: ["ready", "later"] },
45- * },
46- * })
47- */
48- log: (entry: TelemetryLogEntry) => Promise<void>
40+ log: (entry: TelemetryLogEntry) => Promise<void>
4941
50- /**
51- * Marks one use of a feature as the CLI's own feature events do, one
52- * `tengu_feature_<kind>` row; resolves once the ingest accepted it.
53- *
54- * The row carries `feature_name`, `error_code` on sad or bad (`reason`,
55- * required there and refused on ok) and the entry's `props`, checked as
56- * `log`'s are; it joins the product-wide feature surface, so no prefix.
57- *
58- * @param entry the feature, how it went, why when not ok, and the row's
59- * properties by snake_case key
60- * @example
61- * await $.telemetry.mark({ feature: "learn_page", kind: "ok" })
62- * await $.telemetry.mark({
63- * feature: "learn_page",
64- * kind: "sad",
65- * reason: "blocked",
66- * })
67- */
68- mark: (entry: TelemetryMarkEntry) => Promise<void>
69- }
70-
7142 /**
72- * What `$.telemetry.log` takes: the event's name after the prefix, and its
73- * properties by snake_case key.
43+ * Marks one use of a feature as the CLI's own feature events do, one
44+ * `tengu_feature_<kind>` row; resolves once the ingest accepted it.
45+ *
46+ * The row carries `feature_name`, `error_code` on sad or bad (`reason`,
47+ * required there and refused on ok) and the entry's `props`, checked as
48+ * `log`'s are; it joins the product-wide feature surface, so no prefix.
49+ *
50+ * @param entry the feature, how it went, why when not ok, and the row's
51+ * properties by snake_case key
52+ * @example
53+ * await $.telemetry.mark({ feature: "learn_page", kind: "ok" })
54+ * await $.telemetry.mark({
55+ * feature: "learn_page",
56+ * kind: "sad",
57+ * reason: "blocked",
58+ * })
7459 */
75- export type TelemetryLogEntry = {
76- event: string
77- props?: Readonly<Record<string, TelemetryProp>>
78- }
60+ mark: (entry: TelemetryMarkEntry) => Promise<void>
61+}
7962
80- /**
81- * What `$.telemetry.mark` takes: the feature, how it went, why when not
82- * ok, and the properties the row carries beside them by snake_case key.
83- */
84- export type TelemetryMarkEntry = {
85- feature: string
86- kind: TelemetryMarkKind
87- reason?: string
88- props?: Readonly<Record<string, TelemetryProp>>
89- }
63+/**
64+ * What `$.telemetry.log` takes: the event's name after the prefix, and its
65+ * properties by snake_case key.
66+ */
67+export type TelemetryLogEntry = {
68+ event: string
69+ props?: Readonly<Record<string, TelemetryProp>>
70+}
9071
91- /**
92- * How a feature went, as the CLI's own feature events count it.
93- *
94- * `ok`: used, the person got what they asked. `sad`: degraded, a fallback
95- * or a partial, the person still got something. `bad`: failed, the person
96- * got nothing.
97- */
98- export type TelemetryMarkKind = 'ok' | 'sad' | 'bad'
72+/**
73+ * What `$.telemetry.mark` takes: the feature, how it went, why when not
74+ * ok, and the properties the row carries beside them by snake_case key.
75+ */
76+export type TelemetryMarkEntry = {
77+ feature: string
78+ kind: TelemetryMarkKind
79+ reason?: string
80+ props?: Readonly<Record<string, TelemetryProp>>
81+}
9982
100- /**
101- * A property's value: a finite number, a boolean, or a TelemetryChoice;
102- * never free text.
103- */
104- export type TelemetryProp = number | boolean | TelemetryChoice
83+/**
84+ * How a feature went, as the CLI's own feature events count it.
85+ *
86+ * `ok`: used, the person got what they asked. `sad`: degraded, a fallback
87+ * or a partial, the person still got something. `bad`: failed, the person
88+ * got nothing.
89+ */
90+export type TelemetryMarkKind = 'ok' | 'sad' | 'bad'
10591
106- /**
107- * A string property: the value and the list it is chosen from, declared
108- * beside it, so no free text reaches the row.
109- *
110- * Every member of `of` is a lowercase token of letters, digits, `_` and
111- * `-`, which may start with a digit, at most 32 of them; `value` is one of
112- * them.
113- */
114- export type TelemetryChoice = { value: string; of: readonly string[] }
92+/**
93+ * A property's value: a finite number, a boolean, or a TelemetryChoice;
94+ * never free text.
95+ */
96+export type TelemetryProp = number | boolean | TelemetryChoice
97+
98+/**
99+ * A string property: the value and the list it is chosen from, declared
100+ * beside it, so no free text reaches the row.
101+ *
102+ * Every member of `of` is a lowercase token of letters, digits, `_` and
103+ * `-`, which may start with a digit, at most 32 of them; `value` is one of
104+ * them.
105+ */
106+export type TelemetryChoice = { value: string; of: readonly string[] }
107+
108+declare module 'claude-code' {
109+ interface EngineInterface {
110+ /**
111+ * A plugin's analytics, one first-party row per call; present only where
112+ * the telemetry mod is seated (internal builds), absent everywhere else.
113+ */
114+ telemetry: Telemetry
115+ }
115116 }