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
| @@ -76,8 +76,37 @@ | ||
| 76 | 76 | rendered, as a click in the terminal does. |
| 77 | 77 | |
| 78 | 78 | `tsc -p mods/tsconfig.json` typechecks every mod's hooks and tests against |
| 79 | -`types/`. | |
| 79 | +`types/` and each mod's own `types/` contract. | |
| 80 | 80 | |
| 81 | +## Composing mods: noun contracts | |
| 82 | + | |
| 83 | +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). | |
| 89 | + | |
| 90 | +- 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 | |
| 93 | + against `EngineInterface['telemetry']`, so the implementation cannot drift | |
| 94 | + from what callers read. | |
| 95 | +- A mod that calls another's noun reads the same file and never copies it: | |
| 96 | + `mods/tsconfig.json` includes `*/types/**/*.d.ts`, so `$.telemetry.log(…)` | |
| 97 | + in `diff` types against `telemetry`'s contract as it stands. | |
| 98 | +- A test of a mod that calls another's noun seats a provider for it, an inline | |
| 99 | + plugin whose `engine.create` hook adds the noun, and answers the calls the | |
| 100 | + way it answers the engine's: `on('telemetry.log', ($, e) => ({ value: | |
| 101 | + undefined }))` runs above the provider's own method, its `e` typed by the | |
| 102 | + contract. With no provider loaded the `$` build refuses the hook, naming the | |
| 103 | + noun nobody provides. | |
| 104 | + | |
| 105 | +A plugin outside this repository that depends on a mod's noun points its | |
| 106 | +tsconfig `include` at that mod's `types/` folder for now; once the engine | |
| 107 | +writes the contracts of the plugins a session has installed, `/plugin-types` | |
| 108 | +will put them beside `claude-code.d.ts` and the include goes away. | |
| 109 | + | |
| 81 | 110 | Early access: hooks modules load only where function hooks are enabled, and |
| 82 | 111 | the API these mods are written against may change between releases without |
| 83 | 112 | notice. They are not listed in this repository's marketplace; the copies that |
| @@ -1,57 +1,0 @@ | ||
| 1 | -/** | |
| 2 | - * The `$.telemetry` noun as this plugin calls it, declared for a build of | |
| 3 | - * this folder on its own; no module imports it. | |
| 4 | - * | |
| 5 | - * The telemetry plugin adds the noun in the engine.create fold on internal | |
| 6 | - * builds and nowhere else: where no plugin provides it the calls throw and | |
| 7 | - * record/ drops the row. The engine's repository leaves this file out. | |
| 8 | - * | |
| 9 | - * @entry | |
| 10 | - */ | |
| 11 | -declare module 'claude-code' { | |
| 12 | - /** | |
| 13 | - * How one use of a feature went: as hoped, degraded, or failed outright. | |
| 14 | - */ | |
| 15 | - type DiffTelemetryMarkKind = 'ok' | 'sad' | 'bad' | |
| 16 | - | |
| 17 | - /** | |
| 18 | - * A string property: the value and the list it is chosen from. | |
| 19 | - */ | |
| 20 | - type DiffTelemetryChoice = { value: string; of: readonly string[] } | |
| 21 | - | |
| 22 | - /** | |
| 23 | - * One property value a row may carry. | |
| 24 | - */ | |
| 25 | - type DiffTelemetryProp = number | boolean | DiffTelemetryChoice | |
| 26 | - | |
| 27 | - /** | |
| 28 | - * What `$.telemetry.log` takes: the event's name after the prefix, and | |
| 29 | - * its properties by snake_case key. | |
| 30 | - */ | |
| 31 | - type DiffTelemetryLogEntry = { | |
| 32 | - event: string | |
| 33 | - props?: Readonly<Record<string, DiffTelemetryProp>> | |
| 34 | - } | |
| 35 | - | |
| 36 | - /** | |
| 37 | - * What `$.telemetry.mark` takes: the feature, how it went, why when not | |
| 38 | - * ok, and the properties the row carries beside them by snake_case key. | |
| 39 | - */ | |
| 40 | - type DiffTelemetryMarkEntry = { | |
| 41 | - feature: string | |
| 42 | - kind: DiffTelemetryMarkKind | |
| 43 | - reason?: string | |
| 44 | - props?: Readonly<Record<string, DiffTelemetryProp>> | |
| 45 | - } | |
| 46 | - | |
| 47 | - interface EngineInterface { | |
| 48 | - /** | |
| 49 | - * A plugin's analytics, one first-party row per call; present only | |
| 50 | - * where the telemetry plugin is seated. | |
| 51 | - */ | |
| 52 | - telemetry: { | |
| 53 | - log: (entry: DiffTelemetryLogEntry) => Promise<void> | |
| 54 | - mark: (entry: DiffTelemetryMarkEntry) => Promise<void> | |
| 55 | - } | |
| 56 | - } | |
| 57 | -} |
| @@ -4,5 +4,6 @@ | ||
| 4 | 4 | "description": "Plugin analytics: adds $.telemetry in the engine.create fold, so a plugin logs an event or marks a feature's use as one first-party row per call, sent with the session's own credential.", |
| 5 | 5 | "author": { |
| 6 | 6 | "name": "Anthropic" |
| 7 | - } | |
| 7 | + }, | |
| 8 | + "types": "types/index.d.ts" | |
| 8 | 9 | } |
| @@ -28,8 +28,9 @@ | ||
| 28 | 28 | the last two and refused on the first. An entry that breaks a rule is |
| 29 | 29 | refused before anything is sent. |
| 30 | 30 | |
| 31 | -`hooks/register.ts` is the module; `hooks/telemetry-types/` is the noun's | |
| 32 | -type as a caller sees it. | |
| 31 | +`hooks/register.ts` is the module; `types/index.d.ts` is the noun's contract, | |
| 32 | +the one declaration of `$.telemetry` that this mod's hooks, a mod calling the | |
| 33 | +noun and a test answering it all read. | |
| 33 | 34 | |
| 34 | 35 | ## What it hooks |
| 35 | 36 |
| @@ -1,6 +1,6 @@ | ||
| 1 | -import type TelemetryTypes from '../../telemetry-types' | |
| 2 | 1 | import { checkedValue } from '../checked-value' |
| 3 | 2 | import { isRecord } from '../is-record' |
| 3 | +import type { Method } from '../method' | |
| 4 | 4 | import { PROP_LIMIT } from '../prop-limit' |
| 5 | 5 | import { refusal } from '../refusal' |
| 6 | 6 | import { TOKEN } from '../token' |
| @@ -15,7 +15,7 @@ | ||
| 15 | 15 | */ |
| 16 | 16 | export function checkedProps( |
| 17 | 17 | props: unknown, |
| 18 | - method: TelemetryTypes.Method, | |
| 18 | + method: Method, | |
| 19 | 19 | ): Record<string, string | number | boolean> { |
| 20 | 20 | if (!isRecord(props)) { |
| 21 | 21 | throw refusal('props: an object of properties by key', method) |
| @@ -1,7 +1,7 @@ | ||
| 1 | -import type TelemetryTypes from '../../telemetry-types' | |
| 2 | 1 | import { CHOICE_TOKEN } from '../choice-token' |
| 3 | 2 | import { CHOICES_LIMIT } from '../choices-limit' |
| 4 | 3 | import { isRecord } from '../is-record' |
| 4 | +import type { Method } from '../method' | |
| 5 | 5 | import { refusal } from '../refusal' |
| 6 | 6 | |
| 7 | 7 | /** |
| @@ -19,7 +19,7 @@ | ||
| 19 | 19 | export function checkedValue( |
| 20 | 20 | key: string, |
| 21 | 21 | value: unknown, |
| 22 | - method: TelemetryTypes.Method = 'log', | |
| 22 | + method: Method = 'log', | |
| 23 | 23 | ): string | number | boolean { |
| 24 | 24 | if (typeof value === 'boolean') { |
| 25 | 25 | return value |
| @@ -16,6 +16,7 @@ | ||
| 16 | 16 | export * from './mark' |
| 17 | 17 | export * from './mark-fields-of' |
| 18 | 18 | export * from './mark-kinds' |
| 19 | +export * from './method' | |
| 19 | 20 | export * from './prop-limit' |
| 20 | 21 | export * from './refusal' |
| 21 | 22 | export * from './row-session' |
| @@ -1,4 +1,5 @@ | ||
| 1 | -import type TelemetryTypes from '../../telemetry-types' | |
| 1 | +import type { TelemetryMarkKind } from 'claude-code' | |
| 2 | + | |
| 2 | 3 | import { MARK_KINDS } from '../mark-kinds' |
| 3 | 4 | |
| 4 | 5 | /** |
| @@ -7,5 +8,5 @@ | ||
| 7 | 8 | * @param value what the caller passed as `kind` |
| 8 | 9 | * @returns whether value names one of the three mark kinds |
| 9 | 10 | */ |
| 10 | -export const isMarkKind = (value: unknown): value is TelemetryTypes.MarkKind => | |
| 11 | +export const isMarkKind = (value: unknown): value is TelemetryMarkKind => | |
| 11 | 12 | MARK_KINDS.some(kind => kind === value) |
| @@ -1,4 +1,5 @@ | ||
| 1 | -import type TelemetryTypes from '../../telemetry-types' | |
| 1 | +import type { TelemetryMarkKind } from 'claude-code' | |
| 2 | + | |
| 2 | 3 | import type { Fields } from '../fields' |
| 3 | 4 | |
| 4 | 5 | /** |
| @@ -6,7 +7,7 @@ | ||
| 6 | 7 | * and its properties as they go into the row. |
| 7 | 8 | */ |
| 8 | 9 | export type Mark = { |
| 9 | - kind: TelemetryTypes.MarkKind | |
| 10 | + kind: TelemetryMarkKind | |
| 10 | 11 | feature: string |
| 11 | 12 | reason?: string |
| 12 | 13 | props: Fields['props'] |
| @@ -1,10 +1,6 @@ | ||
| 1 | -import type TelemetryTypes from '../../telemetry-types' | |
| 1 | +import type { TelemetryMarkKind } from 'claude-code' | |
| 2 | 2 | |
| 3 | 3 | /** |
| 4 | 4 | * The three kinds a mark may be, in the order the feature events name them. |
| 5 | 5 | */ |
| 6 | -export const MARK_KINDS: readonly TelemetryTypes.MarkKind[] = [ | |
| 7 | - 'ok', | |
| 8 | - 'sad', | |
| 9 | - 'bad', | |
| 10 | -] | |
| 6 | +export const MARK_KINDS: readonly TelemetryMarkKind[] = ['ok', 'sad', 'bad'] |
| @@ -1,0 +1,3 @@ | ||
| 1 | +export type * from './method.js' | |
| 2 | + | |
| 3 | +export * as default from '.' |
| @@ -1,0 +1,4 @@ | ||
| 1 | +/** | |
| 2 | + * The two methods of `$.telemetry`, named in a refusal. | |
| 3 | + */ | |
| 4 | +export type Method = 'log' | 'mark' |
| @@ -1,4 +1,4 @@ | ||
| 1 | -import type TelemetryTypes from '../../telemetry-types' | |
| 1 | +import type { Method } from '../method' | |
| 2 | 2 | |
| 3 | 3 | /** |
| 4 | 4 | * The error a refused entry rejects with, naming the method and what was wrong. |
| @@ -11,7 +11,5 @@ | ||
| 11 | 11 | * @returns the error to reject the call with, naming the method and what was |
| 12 | 12 | * wrong |
| 13 | 13 | */ |
| 14 | -export const refusal = ( | |
| 15 | - what: string, | |
| 16 | - method: TelemetryTypes.Method = 'log', | |
| 17 | -): Error => new Error(`$.telemetry.${method}: ${what}`) | |
| 14 | +export const refusal = (what: string, method: Method = 'log'): Error => | |
| 15 | + new Error(`$.telemetry.${method}: ${what}`) |
| @@ -4,6 +4,5 @@ | ||
| 4 | 4 | export * from './register.js' |
| 5 | 5 | export * from './telemetry-deps' |
| 6 | 6 | export * from './telemetry-of' |
| 7 | -export * from './telemetry-types' | |
| 8 | 7 | |
| 9 | 8 | export * as default from '.' |
| @@ -1,4 +1,4 @@ | ||
| 1 | -import type { On } from 'claude-code' | |
| 1 | +import type { EngineInterface, On } from 'claude-code' | |
| 2 | 2 | |
| 3 | 3 | import { telemetryOf } from './telemetry-of' |
| 4 | 4 | |
| @@ -15,33 +15,30 @@ | ||
| 15 | 15 | on('engine.create', async ($, e, next) => { |
| 16 | 16 | const beneath = await next(e) |
| 17 | 17 | |
| 18 | - return { | |
| 19 | - ...beneath, | |
| 20 | - telemetry: telemetryOf({ | |
| 21 | - authorize: () => beneath.session.authorize(), | |
| 22 | - id: () => beneath.session.id(), | |
| 23 | - model: () => beneath.session.model(), | |
| 24 | - environment: async () => ({ | |
| 25 | - userType: await beneath.env.get('USER_TYPE'), | |
| 26 | - disableTelemetry: await beneath.env.get('DISABLE_TELEMETRY'), | |
| 27 | - disableNonessentialTraffic: await beneath.env.get( | |
| 28 | - 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', | |
| 29 | - ), | |
| 30 | - doNotTrack: await beneath.env.get('DO_NOT_TRACK'), | |
| 31 | - customOauthUrl: await beneath.env.get('CLAUDE_CODE_CUSTOM_OAUTH_URL'), | |
| 32 | - useBedrock: await beneath.env.get('CLAUDE_CODE_USE_BEDROCK'), | |
| 33 | - useVertex: await beneath.env.get('CLAUDE_CODE_USE_VERTEX'), | |
| 34 | - useFoundry: await beneath.env.get('CLAUDE_CODE_USE_FOUNDRY'), | |
| 35 | - useAnthropicAws: await beneath.env.get( | |
| 36 | - 'CLAUDE_CODE_USE_ANTHROPIC_AWS', | |
| 37 | - ), | |
| 38 | - useAnthropicGoogleCloud: await beneath.env.get( | |
| 39 | - 'CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD', | |
| 40 | - ), | |
| 41 | - useMantle: await beneath.env.get('CLAUDE_CODE_USE_MANTLE'), | |
| 42 | - }), | |
| 43 | - fetch: (url, init) => beneath.http.fetch(url, init), | |
| 18 | + const telemetry: EngineInterface['telemetry'] = telemetryOf({ | |
| 19 | + authorize: () => beneath.session.authorize(), | |
| 20 | + id: () => beneath.session.id(), | |
| 21 | + model: () => beneath.session.model(), | |
| 22 | + environment: async () => ({ | |
| 23 | + userType: await beneath.env.get('USER_TYPE'), | |
| 24 | + disableTelemetry: await beneath.env.get('DISABLE_TELEMETRY'), | |
| 25 | + disableNonessentialTraffic: await beneath.env.get( | |
| 26 | + 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', | |
| 27 | + ), | |
| 28 | + doNotTrack: await beneath.env.get('DO_NOT_TRACK'), | |
| 29 | + customOauthUrl: await beneath.env.get('CLAUDE_CODE_CUSTOM_OAUTH_URL'), | |
| 30 | + useBedrock: await beneath.env.get('CLAUDE_CODE_USE_BEDROCK'), | |
| 31 | + useVertex: await beneath.env.get('CLAUDE_CODE_USE_VERTEX'), | |
| 32 | + useFoundry: await beneath.env.get('CLAUDE_CODE_USE_FOUNDRY'), | |
| 33 | + useAnthropicAws: await beneath.env.get('CLAUDE_CODE_USE_ANTHROPIC_AWS'), | |
| 34 | + useAnthropicGoogleCloud: await beneath.env.get( | |
| 35 | + 'CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD', | |
| 36 | + ), | |
| 37 | + useMantle: await beneath.env.get('CLAUDE_CODE_USE_MANTLE'), | |
| 44 | 38 | }), |
| 45 | - } | |
| 39 | + fetch: (url, init) => beneath.http.fetch(url, init), | |
| 40 | + }) | |
| 41 | + | |
| 42 | + return { ...beneath, telemetry } | |
| 46 | 43 | }) |
| 47 | 44 | } |
| @@ -1,7 +1,8 @@ | ||
| 1 | +import type { Telemetry } from 'claude-code' | |
| 2 | + | |
| 1 | 3 | import Entries from '../entries' |
| 2 | 4 | import { isAnalyticsOff } from '../is-analytics-off' |
| 3 | 5 | import type { TelemetryDeps } from '../telemetry-deps' |
| 4 | -import type TelemetryTypes from '../telemetry-types' | |
| 5 | 6 | |
| 6 | 7 | /** |
| 7 | 8 | * Builds `$.telemetry`: `log` and `mark` check the entry, read the environment |
| @@ -14,12 +15,12 @@ | ||
| 14 | 15 | * @param deps the calls on the nouns beneath |
| 15 | 16 | * @returns the `$.telemetry` interface, `log` and `mark` |
| 16 | 17 | */ |
| 17 | -export function telemetryOf(deps: TelemetryDeps): TelemetryTypes.Telemetry { | |
| 18 | +export function telemetryOf(deps: TelemetryDeps): Telemetry { | |
| 18 | 19 | let queue: Promise<unknown> = Promise.resolve() |
| 19 | 20 | |
| 20 | 21 | async function post( |
| 21 | 22 | fields: Entries.Fields, |
| 22 | - method: TelemetryTypes.Method, | |
| 23 | + method: Entries.Method, | |
| 23 | 24 | ): Promise<void> { |
| 24 | 25 | const environment = await deps.environment() |
| 25 | 26 | |
| @@ -59,7 +60,7 @@ | ||
| 59 | 60 | |
| 60 | 61 | function queued( |
| 61 | 62 | fields: Entries.Fields, |
| 62 | - method: TelemetryTypes.Method, | |
| 63 | + method: Entries.Method, | |
| 63 | 64 | ): Promise<void> { |
| 64 | 65 | const turn = queue.then(() => post(fields, method)) |
| 65 | 66 | queue = turn.catch(() => undefined) |
| @@ -1,9 +1,0 @@ | ||
| 1 | -/** | |
| 2 | - * A string property: the value and the list it is chosen from, declared | |
| 3 | - * beside it, so no free text reaches the row. | |
| 4 | - * | |
| 5 | - * Every member of `of` is a lowercase token of letters, digits, `_` and | |
| 6 | - * `-`, which may start with a digit, at most CHOICES_LIMIT of them; `value` | |
| 7 | - * is one of them. | |
| 8 | - */ | |
| 9 | -export type Choice = { value: string; of: readonly string[] } |
| @@ -1,3 +1,0 @@ | ||
| 1 | -export type * from './choice.js' | |
| 2 | - | |
| 3 | -export * as default from '.' |
| @@ -1,9 +1,0 @@ | ||
| 1 | -export * from './choice' | |
| 2 | -export * from './log-entry' | |
| 3 | -export * from './mark-entry' | |
| 4 | -export * from './mark-kind' | |
| 5 | -export type * from './method.js' | |
| 6 | -export * from './prop' | |
| 7 | -export type * from './telemetry.js' | |
| 8 | - | |
| 9 | -export * as default from '.' |
| @@ -1,3 +1,0 @@ | ||
| 1 | -export type * from './log-entry.js' | |
| 2 | - | |
| 3 | -export * as default from '.' |
| @@ -1,10 +1,0 @@ | ||
| 1 | -import type { Prop } from '../prop' | |
| 2 | - | |
| 3 | -/** | |
| 4 | - * What `$.telemetry.log` takes: the event's name after the prefix, and its | |
| 5 | - * properties by snake_case key. | |
| 6 | - */ | |
| 7 | -export type LogEntry = { | |
| 8 | - event: string | |
| 9 | - props?: Readonly<Record<string, Prop>> | |
| 10 | -} |
| @@ -1,3 +1,0 @@ | ||
| 1 | -export type * from './mark-entry.js' | |
| 2 | - | |
| 3 | -export * as default from '.' |
| @@ -1,13 +1,0 @@ | ||
| 1 | -import type { MarkKind } from '../mark-kind' | |
| 2 | -import type { Prop } from '../prop' | |
| 3 | - | |
| 4 | -/** | |
| 5 | - * What `$.telemetry.mark` takes: the feature, how it went, why when not | |
| 6 | - * ok, and the properties the row carries beside them by snake_case key. | |
| 7 | - */ | |
| 8 | -export type MarkEntry = { | |
| 9 | - feature: string | |
| 10 | - kind: MarkKind | |
| 11 | - reason?: string | |
| 12 | - props?: Readonly<Record<string, Prop>> | |
| 13 | -} |
| @@ -1,3 +1,0 @@ | ||
| 1 | -export type * from './mark-kind.js' | |
| 2 | - | |
| 3 | -export * as default from '.' |
| @@ -1,8 +1,0 @@ | ||
| 1 | -/** | |
| 2 | - * How a feature went, as the CLI's own feature events count it. | |
| 3 | - * | |
| 4 | - * `ok`: used, the person got what they asked. `sad`: degraded, a fallback | |
| 5 | - * or a partial, the person still got something. `bad`: failed, the person | |
| 6 | - * got nothing. | |
| 7 | - */ | |
| 8 | -export type MarkKind = 'ok' | 'sad' | 'bad' |
| @@ -1,4 +1,0 @@ | ||
| 1 | -/** | |
| 2 | - * The two methods of `$.telemetry`, named in a refusal. | |
| 3 | - */ | |
| 4 | -export type Method = 'log' | 'mark' |
| @@ -1,3 +1,0 @@ | ||
| 1 | -export type * from './prop.js' | |
| 2 | - | |
| 3 | -export * as default from '.' |
| @@ -1,7 +1,0 @@ | ||
| 1 | -import type { Choice } from '../choice' | |
| 2 | - | |
| 3 | -/** | |
| 4 | - * A property's value: a finite number, a boolean, or a Choice; never free | |
| 5 | - * text. | |
| 6 | - */ | |
| 7 | -export type Prop = number | boolean | Choice |
| @@ -1,57 +1,0 @@ | ||
| 1 | -import type { LogEntry } from './log-entry' | |
| 2 | -import type { MarkEntry } from './mark-entry' | |
| 3 | - | |
| 4 | -/** | |
| 5 | - * A plugin's analytics, sent one event at a time through `$.telemetry`. | |
| 6 | - * | |
| 7 | - * Internal builds alone: the telemetry built-in adds the noun in the | |
| 8 | - * engine.create fold, so a plugin on an external build, or one where the | |
| 9 | - * built-in is off, finds no `$.telemetry`. | |
| 10 | - */ | |
| 11 | -export type Telemetry = { | |
| 12 | - /** | |
| 13 | - * Sends one event, `tengu_plugin_<event>`, as one first-party row; | |
| 14 | - * resolves once the ingest accepted it. | |
| 15 | - * | |
| 16 | - * The calling built-in names itself in `event`; one already named `tengu_…` | |
| 17 | - * is sent as named. A value is a finite number, a boolean or a Choice; free | |
| 18 | - * text is refused. One input, as every op on `$` takes. | |
| 19 | - * | |
| 20 | - * @param entry the event's name, a snake_case token, and its properties by | |
| 21 | - * snake_case key | |
| 22 | - * @example | |
| 23 | - * await $.telemetry.log({ | |
| 24 | - * event: "suggest_learning_survey_answered", | |
| 25 | - * props: { | |
| 26 | - * answer: 2, | |
| 27 | - * page: { value: "ready", of: ["ready", "later"] }, | |
| 28 | - * }, | |
| 29 | - * }) | |
| 30 | - */ | |
| 31 | - log: (entry: LogEntry) => Promise<void> | |
| 32 | - | |
| 33 | - /** | |
| 34 | - * Marks one use of a feature as the CLI's own feature events do, one | |
| 35 | - * `tengu_feature_<kind>` row; resolves once the ingest accepted it. | |
| 36 | - * | |
| 37 | - * The row carries `feature_name`, `error_code` on sad or bad (`reason`, | |
| 38 | - * required there and refused on ok) and the entry's `props`, checked as | |
| 39 | - * `log`'s are; it joins the product-wide feature surface, so no prefix. | |
| 40 | - * | |
| 41 | - * @param entry the feature, how it went, why when not ok, and the row's | |
| 42 | - * properties by snake_case key | |
| 43 | - * @example | |
| 44 | - * await $.telemetry.mark({ feature: "learn_page", kind: "ok" }) | |
| 45 | - * await $.telemetry.mark({ | |
| 46 | - * feature: "learn_page", | |
| 47 | - * kind: "ok", | |
| 48 | - * props: { page: { value: "later", of: ["ready", "later"] } }, | |
| 49 | - * }) | |
| 50 | - * await $.telemetry.mark({ | |
| 51 | - * feature: "learn_page", | |
| 52 | - * kind: "sad", | |
| 53 | - * reason: "blocked", | |
| 54 | - * }) | |
| 55 | - */ | |
| 56 | - mark: (entry: MarkEntry) => Promise<void> | |
| 57 | -} |
| @@ -1,7 +1,5 @@ | ||
| 1 | -import type { CommandRunInput } from 'claude-code' | |
| 1 | +import type { CommandRunInput, TelemetryLogEntry } from 'claude-code' | |
| 2 | 2 | |
| 3 | -import type { LogEntry } from '../../hooks/telemetry-types' | |
| 4 | - | |
| 5 | 3 | /** |
| 6 | 4 | * The command that has the recording plugin log an entry, typed as the |
| 7 | 5 | * person would type it. |
| @@ -9,7 +7,7 @@ | ||
| 9 | 7 | * @param entry what to log |
| 10 | 8 | * @returns `/record <entry>` |
| 11 | 9 | */ |
| 12 | -export const record = (entry: LogEntry): CommandRunInput => ({ | |
| 10 | +export const record = (entry: TelemetryLogEntry): CommandRunInput => ({ | |
| 13 | 11 | command: 'record', |
| 14 | 12 | args: JSON.stringify(entry), |
| 15 | 13 | origin: { kind: 'composer' }, |
| @@ -1,11 +1,11 @@ | ||
| 1 | -import type { LogEntry } from '../../hooks/telemetry-types' | |
| 1 | +import type { TelemetryLogEntry } from 'claude-code' | |
| 2 | 2 | |
| 3 | 3 | /** |
| 4 | 4 | * A survey answered, as a plugin logs it. |
| 5 | 5 | * |
| 6 | 6 | * @returns the entry, fresh each call |
| 7 | 7 | */ |
| 8 | -export const surveyAnswer = (): LogEntry => ({ | |
| 8 | +export const surveyAnswer = (): TelemetryLogEntry => ({ | |
| 9 | 9 | event: 'survey_answered', |
| 10 | 10 | props: { answer: 2, seen: true }, |
| 11 | 11 | }) |
| @@ -1,0 +1,115 @@ | ||
| 1 | +/** | |
| 2 | + * 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. | |
| 4 | + * | |
| 5 | + * 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. | |
| 10 | + */ | |
| 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 | + } | |
| 19 | + | |
| 20 | + /** | |
| 21 | + * A plugin's analytics, sent one event at a time through `$.telemetry`. | |
| 22 | + * | |
| 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. | |
| 26 | + */ | |
| 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> | |
| 49 | + | |
| 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 | + | |
| 71 | + /** | |
| 72 | + * What `$.telemetry.log` takes: the event's name after the prefix, and its | |
| 73 | + * properties by snake_case key. | |
| 74 | + */ | |
| 75 | + export type TelemetryLogEntry = { | |
| 76 | + event: string | |
| 77 | + props?: Readonly<Record<string, TelemetryProp>> | |
| 78 | + } | |
| 79 | + | |
| 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 | + } | |
| 90 | + | |
| 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' | |
| 99 | + | |
| 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 | |
| 105 | + | |
| 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[] } | |
| 115 | +} |
| @@ -13,5 +13,5 @@ | ||
| 13 | 13 | "jsxFactory": "h", |
| 14 | 14 | "jsxFragmentFactory": "Fragment" |
| 15 | 15 | }, |
| 16 | - "include": ["types", "*/hooks", "*/tests"] | |
| 16 | + "include": ["types", "*/types/**/*.d.ts", "*/hooks", "*/tests"] | |
| 17 | 17 | } |