@aligndottech/cli 0.37.1 → 0.38.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.
- package/README.md +16 -10
- package/dist/cli.d.ts +28 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +98 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/check.d.ts +1 -1
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +44 -2
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/connect-agents.d.ts +1 -0
- package/dist/commands/connect-agents.d.ts.map +1 -1
- package/dist/commands/connect-agents.js +10 -6
- package/dist/commands/connect-agents.js.map +1 -1
- package/dist/commands/connect.d.ts +31 -0
- package/dist/commands/connect.d.ts.map +1 -0
- package/dist/commands/connect.js +92 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +52 -10
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/decisions/index.d.ts.map +1 -1
- package/dist/commands/decisions/index.js +5 -0
- package/dist/commands/decisions/index.js.map +1 -1
- package/dist/commands/default-action.d.ts +3 -1
- package/dist/commands/default-action.d.ts.map +1 -1
- package/dist/commands/default-action.js +29 -27
- package/dist/commands/default-action.js.map +1 -1
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +3 -0
- package/dist/commands/export.js.map +1 -1
- package/dist/commands/import/sessions.d.ts +10 -2
- package/dist/commands/import/sessions.d.ts.map +1 -1
- package/dist/commands/import/sessions.js +98 -3
- package/dist/commands/import/sessions.js.map +1 -1
- package/dist/commands/import.d.ts +7 -0
- package/dist/commands/import.d.ts.map +1 -1
- package/dist/commands/import.js +42 -2
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/invite.d.ts +25 -0
- package/dist/commands/invite.d.ts.map +1 -0
- package/dist/commands/invite.js +68 -0
- package/dist/commands/invite.js.map +1 -0
- package/dist/commands/links.d.ts.map +1 -1
- package/dist/commands/links.js +5 -0
- package/dist/commands/links.js.map +1 -1
- package/dist/commands/mcp.d.ts +64 -35
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +85 -40
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/ratify.d.ts.map +1 -1
- package/dist/commands/ratify.js +21 -0
- package/dist/commands/ratify.js.map +1 -1
- package/dist/commands/registry.d.ts +30 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +70 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/commands/setup.d.ts +45 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +197 -88
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +10 -22
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/telemetry.d.ts +7 -3
- package/dist/commands/telemetry.d.ts.map +1 -1
- package/dist/commands/telemetry.js +12 -7
- package/dist/commands/telemetry.js.map +1 -1
- package/dist/commands/why.d.ts.map +1 -1
- package/dist/commands/why.js +52 -10
- package/dist/commands/why.js.map +1 -1
- package/dist/index.js +4 -90
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-rules.d.ts +1 -0
- package/dist/lib/agent-rules.d.ts.map +1 -1
- package/dist/lib/agent-rules.js +60 -0
- package/dist/lib/agent-rules.js.map +1 -1
- package/dist/lib/commands-doc.d.ts +15 -0
- package/dist/lib/commands-doc.d.ts.map +1 -0
- package/dist/lib/commands-doc.js +85 -0
- package/dist/lib/commands-doc.js.map +1 -0
- package/dist/lib/config.d.ts +7 -2
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/connect-prompt.d.ts +1 -1
- package/dist/lib/connect-prompt.js +2 -2
- package/dist/lib/connect-prompt.js.map +1 -1
- package/dist/lib/decision-links.d.ts +39 -0
- package/dist/lib/decision-links.d.ts.map +1 -1
- package/dist/lib/decision-links.js +51 -0
- package/dist/lib/decision-links.js.map +1 -1
- package/dist/lib/first-decision.d.ts +22 -0
- package/dist/lib/first-decision.d.ts.map +1 -0
- package/dist/lib/first-decision.js +22 -0
- package/dist/lib/first-decision.js.map +1 -0
- package/dist/lib/gateway-client.d.ts +13 -0
- package/dist/lib/gateway-client.d.ts.map +1 -1
- package/dist/lib/gateway-client.js +14 -0
- package/dist/lib/gateway-client.js.map +1 -1
- package/dist/lib/git.d.ts +28 -0
- package/dist/lib/git.d.ts.map +1 -1
- package/dist/lib/git.js +42 -0
- package/dist/lib/git.js.map +1 -1
- package/dist/lib/hook-context.d.ts +7 -0
- package/dist/lib/hook-context.d.ts.map +1 -0
- package/dist/lib/hook-context.js +37 -0
- package/dist/lib/hook-context.js.map +1 -0
- package/dist/lib/hook-payload.d.ts +7 -0
- package/dist/lib/hook-payload.d.ts.map +1 -1
- package/dist/lib/hook-payload.js +56 -8
- package/dist/lib/hook-payload.js.map +1 -1
- package/dist/lib/invite-prompt.d.ts +46 -0
- package/dist/lib/invite-prompt.d.ts.map +1 -0
- package/dist/lib/invite-prompt.js +63 -0
- package/dist/lib/invite-prompt.js.map +1 -0
- package/dist/lib/mcp-instructions.shared.d.ts +40 -0
- package/dist/lib/mcp-instructions.shared.d.ts.map +1 -0
- package/dist/lib/mcp-instructions.shared.js +56 -0
- package/dist/lib/mcp-instructions.shared.js.map +1 -0
- package/dist/lib/mcp-setup.d.ts +32 -2
- package/dist/lib/mcp-setup.d.ts.map +1 -1
- package/dist/lib/mcp-setup.js +92 -6
- package/dist/lib/mcp-setup.js.map +1 -1
- package/dist/lib/next-step.d.ts +92 -0
- package/dist/lib/next-step.d.ts.map +1 -0
- package/dist/lib/next-step.js +97 -0
- package/dist/lib/next-step.js.map +1 -0
- package/dist/lib/personal-email-domains.d.ts +8 -0
- package/dist/lib/personal-email-domains.d.ts.map +1 -0
- package/dist/lib/personal-email-domains.js +39 -0
- package/dist/lib/personal-email-domains.js.map +1 -0
- package/dist/lib/personal-import.d.ts +2 -0
- package/dist/lib/personal-import.d.ts.map +1 -1
- package/dist/lib/personal-import.js +4 -1
- package/dist/lib/personal-import.js.map +1 -1
- package/dist/lib/read-value-rollup.d.ts +18 -0
- package/dist/lib/read-value-rollup.d.ts.map +1 -0
- package/dist/lib/read-value-rollup.js +33 -0
- package/dist/lib/read-value-rollup.js.map +1 -0
- package/dist/lib/sessions/import-summary.d.ts +39 -0
- package/dist/lib/sessions/import-summary.d.ts.map +1 -0
- package/dist/lib/sessions/import-summary.js +41 -0
- package/dist/lib/sessions/import-summary.js.map +1 -0
- package/dist/lib/sessions/memory-files.d.ts +85 -0
- package/dist/lib/sessions/memory-files.d.ts.map +1 -0
- package/dist/lib/sessions/memory-files.js +235 -0
- package/dist/lib/sessions/memory-files.js.map +1 -0
- package/dist/lib/sessions/source-url.d.ts +11 -0
- package/dist/lib/sessions/source-url.d.ts.map +1 -1
- package/dist/lib/sessions/source-url.js +15 -0
- package/dist/lib/sessions/source-url.js.map +1 -1
- package/dist/lib/setup-funnel.d.ts +41 -0
- package/dist/lib/setup-funnel.d.ts.map +1 -0
- package/dist/lib/setup-funnel.js +28 -0
- package/dist/lib/setup-funnel.js.map +1 -0
- package/dist/lib/telemetry-consent.d.ts +17 -8
- package/dist/lib/telemetry-consent.d.ts.map +1 -1
- package/dist/lib/telemetry-consent.js +21 -6
- package/dist/lib/telemetry-consent.js.map +1 -1
- package/dist/lib/telemetry-env.d.ts +19 -0
- package/dist/lib/telemetry-env.d.ts.map +1 -0
- package/dist/lib/telemetry-env.js +13 -0
- package/dist/lib/telemetry-env.js.map +1 -0
- package/dist/lib/usage-telemetry.d.ts +108 -13
- package/dist/lib/usage-telemetry.d.ts.map +1 -1
- package/dist/lib/usage-telemetry.js +227 -55
- package/dist/lib/usage-telemetry.js.map +1 -1
- package/dist/lib/user-hooks.d.ts +15 -0
- package/dist/lib/user-hooks.d.ts.map +1 -0
- package/dist/lib/user-hooks.js +144 -0
- package/dist/lib/user-hooks.js.map +1 -0
- package/dist/lib/value-rollup.d.ts +10 -3
- package/dist/lib/value-rollup.d.ts.map +1 -1
- package/dist/lib/value-rollup.js +11 -4
- package/dist/lib/value-rollup.js.map +1 -1
- package/package.json +2 -1
|
@@ -5,13 +5,20 @@ import { type EnvironmentConfig, type TelemetryConsent } from './config.js';
|
|
|
5
5
|
*
|
|
6
6
|
* Cloud mode is opt-out: a cloud user is already on an authenticated connection to our
|
|
7
7
|
* gateway, so an event about a call already being made is not a new phone-home. Local-embedded
|
|
8
|
-
* mode (ALI-
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* mode has two tiers (ALI-954, superseding the ALI-618 "nothing by default" for local mode):
|
|
9
|
+
* - Two anonymous BEACONS send by default: `cli.funnel.install` (once, on the first ever run,
|
|
10
|
+
* see recordInstallBeacon) and `cli.funnel.setup_completed`. They exist so the funnel has a
|
|
11
|
+
* denominator - opt-in usage alone cannot tell a 10% consent rate from 90%.
|
|
12
|
+
* - Everything else (`cli.command`, the other funnel stages) sends only with the stored
|
|
13
|
+
* consent (`config.ts`'s `getTelemetryConsent`, set by the one-time prompt at the end of
|
|
14
|
+
* setup, default No). `--local` users have no account and no tenant, so there is nothing
|
|
15
|
+
* to authenticate an event against - the consent is the gate.
|
|
16
|
+
* Both modes send only a command name, never arguments or content. docs/telemetry.md lists
|
|
17
|
+
* every event and field, and telemetry-docs-parity.test.ts keeps that page true.
|
|
12
18
|
*
|
|
13
|
-
* `ALIGN_TELEMETRY=0`
|
|
14
|
-
* both modes, over a granted local consent included (ALI-618 D3b - one consent model, not
|
|
19
|
+
* `ALIGN_TELEMETRY=0` and `DO_NOT_TRACK=1` (telemetry-env.ts) turn everything off, both tiers,
|
|
20
|
+
* in both modes, over a granted local consent included (ALI-618 D3b - one consent model, not
|
|
21
|
+
* two). `align telemetry off` stores 'off', which does the same thing without an env var.
|
|
15
22
|
*/
|
|
16
23
|
export declare const TELEMETRY_TIMEOUT_MS = 2000;
|
|
17
24
|
export interface TelemetryStatus {
|
|
@@ -22,8 +29,9 @@ export interface TelemetryStatus {
|
|
|
22
29
|
* ALI-618 D3b: what `align telemetry status` prints. Takes the consent decision as a plain
|
|
23
30
|
* argument rather than reading `config.ts` itself, so the two consent MODELS stay visibly
|
|
24
31
|
* distinct in one function a reader can hold in their head - cloud's opt-out default and
|
|
25
|
-
* local's stored
|
|
26
|
-
* both,
|
|
32
|
+
* local's stored decision (ALI-954: three values, and the two default-on beacons named where
|
|
33
|
+
* usage is off but they are not) - with the env switches as the thing that overrides both,
|
|
34
|
+
* checked first.
|
|
27
35
|
*/
|
|
28
36
|
export declare function getTelemetryStatus(env: EnvironmentConfig, localConsent: TelemetryConsent | undefined): TelemetryStatus;
|
|
29
37
|
export declare function recordCommandUsage(env: EnvironmentConfig, command: string): Promise<void>;
|
|
@@ -31,20 +39,107 @@ export declare function recordCommandUsage(env: EnvironmentConfig, command: stri
|
|
|
31
39
|
* ALI-795: the activation-funnel stages, matching the gateway's closed enum
|
|
32
40
|
* (telemetryAnonymousRoutes.ts). Repeat-use and D7 are deliberately absent - both derive
|
|
33
41
|
* server-side from install_id timestamps, so a client event would be a second writer.
|
|
42
|
+
*
|
|
43
|
+
* ALI-938: `teammate_requested` is the bottom-up-thesis signal - fired by `align invite`
|
|
44
|
+
* for every genuine attempt to bring a teammate in, whether or not the invite is
|
|
45
|
+
* actually sent (a member blocked by the org_admin gate is still demand). `align invite`
|
|
46
|
+
* refuses local-embedded mode outright (it needs a real cloud tenant to invite anyone
|
|
47
|
+
* into), so in practice this ALWAYS reaches recordFunnelStage with a cloud env and takes
|
|
48
|
+
* the /telemetry/ingest path, which accepts any eventName as a string. The gateway's
|
|
49
|
+
* FUNNEL_STAGES enum lists it too since ALI-954, so the local path no longer 400s either.
|
|
50
|
+
*
|
|
51
|
+
* ALI-954: `install` is the once-per-install beacon and has its own emitter
|
|
52
|
+
* (recordInstallBeacon) - it is not offered to recordFunnelStage, whose `command` argument
|
|
53
|
+
* is a provenance the install beacon must not carry.
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* ALI-835: the stages the gateway ACCEPTS a measurement on. Permission, not obligation - three
|
|
57
|
+
* of the four come from one `align import sessions` run and carry a count and an agent name,
|
|
58
|
+
* while `decisions_ratified` comes from `align ratify` and deliberately carries neither, since
|
|
59
|
+
* a ratification is a person standing behind a claim rather than anything an agent counted.
|
|
60
|
+
*
|
|
61
|
+
* Session import is the first funnel event whose value IS a number; every earlier stage only
|
|
62
|
+
* had to happen.
|
|
63
|
+
*
|
|
64
|
+
* Declared above FUNNEL_STAGES and spread into it rather than listed in both, so the four
|
|
65
|
+
* strings have one writer here as they now do on the gateway side (align-stack #2237).
|
|
34
66
|
*/
|
|
35
|
-
export
|
|
67
|
+
export declare const SESSION_IMPORT_STAGES: readonly ["sessions_scanned", "candidates_found", "candidates_confirmed", "decisions_ratified"];
|
|
68
|
+
export declare const FUNNEL_STAGES: readonly ["setup_started", "setup_completed", "import_completed", "mcp_wired", "first_useful_decision", "teammate_requested", "sessions_scanned", "candidates_found", "candidates_confirmed", "decisions_ratified"];
|
|
69
|
+
/**
|
|
70
|
+
* What a session-import stage reports. A count and the agent's name, and nothing else - never a
|
|
71
|
+
* repo, a path, a session id or a line of any transcript. The gateway refuses both fields on
|
|
72
|
+
* every other stage.
|
|
73
|
+
*/
|
|
74
|
+
export interface FunnelMeasurement {
|
|
75
|
+
count: number;
|
|
76
|
+
agent: string;
|
|
77
|
+
}
|
|
78
|
+
export type FunnelStage = (typeof FUNNEL_STAGES)[number];
|
|
79
|
+
/**
|
|
80
|
+
* ALI-954: the two stages that send BY DEFAULT in local mode, with no consent - the
|
|
81
|
+
* denominator of the funnel. `install` goes through recordInstallBeacon; `setup_completed`
|
|
82
|
+
* goes through recordFunnelStage like every other stage and is the one stage there that
|
|
83
|
+
* does not read the consent decision. Adding a stage here is a documented promise changing:
|
|
84
|
+
* docs/telemetry.md lists this set, and telemetry-docs-parity.test.ts checks it.
|
|
85
|
+
*/
|
|
86
|
+
export declare const BEACON_STAGES: readonly ["install", "setup_completed"];
|
|
36
87
|
/**
|
|
37
88
|
* The single funnel-stage emitter. Same consent model as recordCommandUsage, no new
|
|
38
|
-
* consent surface: cloud is opt-out behind a token, local is
|
|
39
|
-
*
|
|
40
|
-
* ("first_useful_decision via ask"), never arguments
|
|
89
|
+
* consent surface: cloud is opt-out behind a token, local is behind the stored decision
|
|
90
|
+
* except for the beacon tier (BEACON_STAGES), and the env switches beat everything.
|
|
91
|
+
* `command` is the stage's provenance ("first_useful_decision via ask"), never arguments
|
|
92
|
+
* or content.
|
|
41
93
|
*
|
|
42
94
|
* first_useful_decision is once-per-install, guarded HERE rather than at call sites so
|
|
43
95
|
* there is exactly one enforcement point. Marked before the send, deliberately: a ping
|
|
44
96
|
* lost to a timeout costs one funnel row in the undercount direction, while marking
|
|
45
97
|
* after would re-send forever on a machine that cannot reach the gateway.
|
|
98
|
+
*
|
|
99
|
+
* Resolves `true` when a send was made (not necessarily delivered - see the once-mark
|
|
100
|
+
* reasoning above, the same trade), `false` when this call could not send: opted out, no
|
|
101
|
+
* consent, no token, the store threw, or - for first_useful_decision only, the one
|
|
102
|
+
* once-per-install stage - already recorded on this install. ALI-949: the setup wizard
|
|
103
|
+
* offers setup_started at several checkpoints because local-mode consent arrives
|
|
104
|
+
* mid-wizard, and this is how it knows which offer landed (lib/setup-funnel.ts).
|
|
105
|
+
*/
|
|
106
|
+
export declare function recordFunnelStage(env: EnvironmentConfig, stage: FunnelStage, command: string, measurement?: FunnelMeasurement): Promise<boolean>;
|
|
107
|
+
/**
|
|
108
|
+
* ALI-954: the install beacon - the funnel's denominator. Sent from the preAction hook in
|
|
109
|
+
* index.ts, so it goes out BEFORE any prompt on the very first run of this install id, and
|
|
110
|
+
* never again. The payload is the CLI version, the OS (`process.platform`, a closed enum the
|
|
111
|
+
* gateway also constrains), the install id, and the literal command `align` - the endpoint
|
|
112
|
+
* requires a command and this one must not carry which command was run first, so it sends
|
|
113
|
+
* the program's own name, which carries no information. Nothing about the repo, the graph,
|
|
114
|
+
* or the user.
|
|
115
|
+
*
|
|
116
|
+
* "First ever run" is a moment, not a state, so the install is marked recorded on that run
|
|
117
|
+
* whether or not the beacon went out: `DO_NOT_TRACK=1` or `ALIGN_TELEMETRY=0` set before the
|
|
118
|
+
* first run means it is NEVER sent, by construction, even if the variable is later unset.
|
|
119
|
+
* That is the opposite trade from first_useful_decision's once-mark (which waits for
|
|
120
|
+
* sendability so a later opt-in still emits), and it is deliberate: a beacon on the fifth
|
|
121
|
+
* run would not be the install it claims to be, and "never" is the promise the docs make.
|
|
122
|
+
*
|
|
123
|
+
* Two cases do not consume the first run: `align telemetry ...` as the user's first command
|
|
124
|
+
* (the off switch must not be raced by the thing it switches off), and a run that already
|
|
125
|
+
* holds a cloud token (cloud mode is unchanged by this ticket - its funnel is the authed
|
|
126
|
+
* one). Resolves true when a send was made, false otherwise; never throws, like every other
|
|
127
|
+
* emitter here.
|
|
128
|
+
*/
|
|
129
|
+
export declare function recordInstallBeacon(commandPath: string): Promise<boolean>;
|
|
130
|
+
/**
|
|
131
|
+
* The command path the postAction hook reports: the full path ("local ask", "import git")
|
|
132
|
+
* so recordCommandUsage can exclude the offline `local` group, and never the leaf alone.
|
|
133
|
+
*
|
|
134
|
+
* ALI-949: the ROOT command has no parent. Bare `align` (`program.action(runDefaultAction)`,
|
|
135
|
+
* ALI-773) is the primary first-run path, and walking `.parent` from the root yielded '' -
|
|
136
|
+
* so every bare invocation was reported as no command at all and the funnel's "activated"
|
|
137
|
+
* stage could not see it. The root reports under the program's own name.
|
|
46
138
|
*/
|
|
47
|
-
export declare function
|
|
139
|
+
export declare function invocationCommandPath(actionCommand: {
|
|
140
|
+
name(): string;
|
|
141
|
+
parent: unknown;
|
|
142
|
+
}): string;
|
|
48
143
|
/**
|
|
49
144
|
* The `--env` the user actually typed, including one Commander handed to a parent.
|
|
50
145
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage-telemetry.d.ts","sourceRoot":"","sources":["../../src/lib/usage-telemetry.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"usage-telemetry.d.ts","sourceRoot":"","sources":["../../src/lib/usage-telemetry.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAKtG;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,oBAAoB,OAAQ,CAAC;AAkC1C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,iBAAiB,EACtB,YAAY,EAAE,gBAAgB,GAAG,SAAS,GACzC,eAAe,CAyBjB;AAED,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA+B/F;AAwCD;;;;;;;;;;;;;;;;GAgBG;AACH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qBAAqB,iGAExB,CAAC;AAEX,eAAO,MAAM,aAAa,qNAUhB,CAAC;AAEX;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AACD,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,yCAA0C,CAAC;AAErE;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,iBAAiB,GAC9B,OAAO,CAAC,OAAO,CAAC,CA8ElB;AA2BD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAyB/E;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,aAAa,EAAE;IAAE,IAAI,IAAI,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,CAKhG;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE;IAAE,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG,MAAM,GAAG,SAAS,CASjG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAMf"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ALIGN_HOSTED_GATEWAY_URL } from './config.js';
|
|
2
|
+
import { telemetryDisabledByEnv } from './telemetry-env.js';
|
|
3
|
+
import { inHookContext } from './hook-context.js';
|
|
2
4
|
import pkg from '../../package.json' with { type: 'json' };
|
|
3
5
|
/**
|
|
4
6
|
* ALI-403: emit one `cli.command` event per invocation so cloud CLI activation and weekly
|
|
@@ -6,28 +8,24 @@ import pkg from '../../package.json' with { type: 'json' };
|
|
|
6
8
|
*
|
|
7
9
|
* Cloud mode is opt-out: a cloud user is already on an authenticated connection to our
|
|
8
10
|
* gateway, so an event about a call already being made is not a new phone-home. Local-embedded
|
|
9
|
-
* mode (ALI-
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* mode has two tiers (ALI-954, superseding the ALI-618 "nothing by default" for local mode):
|
|
12
|
+
* - Two anonymous BEACONS send by default: `cli.funnel.install` (once, on the first ever run,
|
|
13
|
+
* see recordInstallBeacon) and `cli.funnel.setup_completed`. They exist so the funnel has a
|
|
14
|
+
* denominator - opt-in usage alone cannot tell a 10% consent rate from 90%.
|
|
15
|
+
* - Everything else (`cli.command`, the other funnel stages) sends only with the stored
|
|
16
|
+
* consent (`config.ts`'s `getTelemetryConsent`, set by the one-time prompt at the end of
|
|
17
|
+
* setup, default No). `--local` users have no account and no tenant, so there is nothing
|
|
18
|
+
* to authenticate an event against - the consent is the gate.
|
|
19
|
+
* Both modes send only a command name, never arguments or content. docs/telemetry.md lists
|
|
20
|
+
* every event and field, and telemetry-docs-parity.test.ts keeps that page true.
|
|
13
21
|
*
|
|
14
|
-
* `ALIGN_TELEMETRY=0`
|
|
15
|
-
* both modes, over a granted local consent included (ALI-618 D3b - one consent model, not
|
|
22
|
+
* `ALIGN_TELEMETRY=0` and `DO_NOT_TRACK=1` (telemetry-env.ts) turn everything off, both tiers,
|
|
23
|
+
* in both modes, over a granted local consent included (ALI-618 D3b - one consent model, not
|
|
24
|
+
* two). `align telemetry off` stores 'off', which does the same thing without an env var.
|
|
16
25
|
*/
|
|
17
26
|
export const TELEMETRY_TIMEOUT_MS = 2_000;
|
|
18
|
-
/**
|
|
19
|
-
* Set and not recognisably ON means OFF. Enumerating the falsy words instead - `0|false|no|off` -
|
|
20
|
-
* guesses at what a user will type, and every guess that misses is a live send by someone who
|
|
21
|
-
* believes they opted out: `disabled` and `n` both sent. Trimmed, because a trailing space or
|
|
22
|
-
* newline comes free from a `.env` file or a here-doc. Unset is ON, which is the documented
|
|
23
|
-
* cloud default and the only value this cannot see.
|
|
24
|
-
*/
|
|
25
|
-
const OPT_IN_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
|
26
27
|
function telemetryOptedOut() {
|
|
27
|
-
|
|
28
|
-
if (raw === undefined || raw.trim() === '')
|
|
29
|
-
return false;
|
|
30
|
-
return !OPT_IN_VALUES.has(raw.trim().toLowerCase());
|
|
28
|
+
return telemetryDisabledByEnv() !== undefined;
|
|
31
29
|
}
|
|
32
30
|
/**
|
|
33
31
|
* POST with a hard timeout, and never throw - telemetry must never fail or delay a command. A
|
|
@@ -61,21 +59,33 @@ async function postWithTimeout(url, init) {
|
|
|
61
59
|
* ALI-618 D3b: what `align telemetry status` prints. Takes the consent decision as a plain
|
|
62
60
|
* argument rather than reading `config.ts` itself, so the two consent MODELS stay visibly
|
|
63
61
|
* distinct in one function a reader can hold in their head - cloud's opt-out default and
|
|
64
|
-
* local's stored
|
|
65
|
-
* both,
|
|
62
|
+
* local's stored decision (ALI-954: three values, and the two default-on beacons named where
|
|
63
|
+
* usage is off but they are not) - with the env switches as the thing that overrides both,
|
|
64
|
+
* checked first.
|
|
66
65
|
*/
|
|
67
66
|
export function getTelemetryStatus(env, localConsent) {
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
const envSwitch = telemetryDisabledByEnv();
|
|
68
|
+
if (envSwitch === 'DO_NOT_TRACK') {
|
|
69
|
+
return { enabled: false, reason: 'off: DO_NOT_TRACK is set - nothing is sent' };
|
|
70
|
+
}
|
|
71
|
+
if (envSwitch === 'ALIGN_TELEMETRY') {
|
|
72
|
+
return { enabled: false, reason: 'off: ALIGN_TELEMETRY is set to an opt-out value - nothing is sent' };
|
|
70
73
|
}
|
|
71
74
|
if (env.mode === 'local-embedded') {
|
|
75
|
+
// ALI-954: "off" here is about usage. The two beacons still send unless the user ran
|
|
76
|
+
// `align telemetry off` (stored 'off') - and the line has to say so, or "off" would be
|
|
77
|
+
// read as "nothing is sent" over a beacon that still is.
|
|
78
|
+
const beacons = ' - the two anonymous counts (install, setup completed) still send; `align telemetry off` stops those too';
|
|
72
79
|
if (localConsent === 'granted') {
|
|
73
80
|
return { enabled: true, reason: 'on: local mode, you opted in when asked' };
|
|
74
81
|
}
|
|
75
82
|
if (localConsent === 'declined') {
|
|
76
|
-
return { enabled: false, reason:
|
|
83
|
+
return { enabled: false, reason: `off: local mode, you declined when asked${beacons}` };
|
|
77
84
|
}
|
|
78
|
-
|
|
85
|
+
if (localConsent === 'off') {
|
|
86
|
+
return { enabled: false, reason: 'off: local mode, you ran `align telemetry off` - nothing is sent' };
|
|
87
|
+
}
|
|
88
|
+
return { enabled: false, reason: `off: local mode, you have not been asked yet${beacons}` };
|
|
79
89
|
}
|
|
80
90
|
return { enabled: true, reason: 'on: cloud mode, opt-out default' };
|
|
81
91
|
}
|
|
@@ -87,9 +97,10 @@ export async function recordCommandUsage(env, command) {
|
|
|
87
97
|
// is not enough on its own.
|
|
88
98
|
if (command === 'local' || command.startsWith('local '))
|
|
89
99
|
return;
|
|
90
|
-
// The mode is the consent boundary (PR #77: cloud is opt-out; ALI-618: local
|
|
91
|
-
// stored consent decision
|
|
92
|
-
//
|
|
100
|
+
// The mode is the consent boundary (PR #77: cloud is opt-out; ALI-618: local usage sends
|
|
101
|
+
// only with the stored consent decision - the ALI-954 beacons are the two named exceptions,
|
|
102
|
+
// and this per-command ping is not one of them). It has to gate on its own because a token
|
|
103
|
+
// can be in scope without cloud consent: ALIGN_TOKEN exported into the shell, or a
|
|
93
104
|
// logged-in default env resolved by the caller.
|
|
94
105
|
if (env.mode === 'local-embedded') {
|
|
95
106
|
await recordAnonymousCommandUsage(command);
|
|
@@ -131,14 +142,7 @@ async function recordAnonymousCommandUsage(command) {
|
|
|
131
142
|
const config = createConfigStore();
|
|
132
143
|
if (config.getTelemetryConsent() !== 'granted')
|
|
133
144
|
return;
|
|
134
|
-
|
|
135
|
-
const commandPath = commandPathOf(command);
|
|
136
|
-
const target = process.env['ALIGN_GATEWAY_URL'] || ALIGN_HOSTED_GATEWAY_URL;
|
|
137
|
-
await postWithTimeout(`${target}/telemetry/anonymous`, {
|
|
138
|
-
method: 'POST',
|
|
139
|
-
headers: { 'Content-Type': 'application/json' },
|
|
140
|
-
body: JSON.stringify({ installId, command: commandPath, cliVersion: pkg.version }),
|
|
141
|
-
});
|
|
145
|
+
await postAnonymous({ installId: config.getInstallId(), command: commandPathOf(command), cliVersion: pkg.version });
|
|
142
146
|
}
|
|
143
147
|
/**
|
|
144
148
|
* ALI-795: the command groups whose second word is a SUBCOMMAND, mirroring the gateway's
|
|
@@ -155,18 +159,77 @@ function commandPathOf(command) {
|
|
|
155
159
|
return `${top} ${sub}`;
|
|
156
160
|
return top ?? command;
|
|
157
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* ALI-795: the activation-funnel stages, matching the gateway's closed enum
|
|
164
|
+
* (telemetryAnonymousRoutes.ts). Repeat-use and D7 are deliberately absent - both derive
|
|
165
|
+
* server-side from install_id timestamps, so a client event would be a second writer.
|
|
166
|
+
*
|
|
167
|
+
* ALI-938: `teammate_requested` is the bottom-up-thesis signal - fired by `align invite`
|
|
168
|
+
* for every genuine attempt to bring a teammate in, whether or not the invite is
|
|
169
|
+
* actually sent (a member blocked by the org_admin gate is still demand). `align invite`
|
|
170
|
+
* refuses local-embedded mode outright (it needs a real cloud tenant to invite anyone
|
|
171
|
+
* into), so in practice this ALWAYS reaches recordFunnelStage with a cloud env and takes
|
|
172
|
+
* the /telemetry/ingest path, which accepts any eventName as a string. The gateway's
|
|
173
|
+
* FUNNEL_STAGES enum lists it too since ALI-954, so the local path no longer 400s either.
|
|
174
|
+
*
|
|
175
|
+
* ALI-954: `install` is the once-per-install beacon and has its own emitter
|
|
176
|
+
* (recordInstallBeacon) - it is not offered to recordFunnelStage, whose `command` argument
|
|
177
|
+
* is a provenance the install beacon must not carry.
|
|
178
|
+
*/
|
|
179
|
+
/**
|
|
180
|
+
* ALI-835: the stages the gateway ACCEPTS a measurement on. Permission, not obligation - three
|
|
181
|
+
* of the four come from one `align import sessions` run and carry a count and an agent name,
|
|
182
|
+
* while `decisions_ratified` comes from `align ratify` and deliberately carries neither, since
|
|
183
|
+
* a ratification is a person standing behind a claim rather than anything an agent counted.
|
|
184
|
+
*
|
|
185
|
+
* Session import is the first funnel event whose value IS a number; every earlier stage only
|
|
186
|
+
* had to happen.
|
|
187
|
+
*
|
|
188
|
+
* Declared above FUNNEL_STAGES and spread into it rather than listed in both, so the four
|
|
189
|
+
* strings have one writer here as they now do on the gateway side (align-stack #2237).
|
|
190
|
+
*/
|
|
191
|
+
export const SESSION_IMPORT_STAGES = [
|
|
192
|
+
'sessions_scanned', 'candidates_found', 'candidates_confirmed', 'decisions_ratified',
|
|
193
|
+
];
|
|
194
|
+
export const FUNNEL_STAGES = [
|
|
195
|
+
'setup_started',
|
|
196
|
+
'setup_completed',
|
|
197
|
+
'import_completed',
|
|
198
|
+
'mcp_wired',
|
|
199
|
+
'first_useful_decision',
|
|
200
|
+
'teammate_requested',
|
|
201
|
+
// The gateway's own FUNNEL_STAGES must list these too or every ping 400s; that half
|
|
202
|
+
// shipped first, deliberately (align-stack #2237, the ALI-790 lesson).
|
|
203
|
+
...SESSION_IMPORT_STAGES,
|
|
204
|
+
];
|
|
205
|
+
/**
|
|
206
|
+
* ALI-954: the two stages that send BY DEFAULT in local mode, with no consent - the
|
|
207
|
+
* denominator of the funnel. `install` goes through recordInstallBeacon; `setup_completed`
|
|
208
|
+
* goes through recordFunnelStage like every other stage and is the one stage there that
|
|
209
|
+
* does not read the consent decision. Adding a stage here is a documented promise changing:
|
|
210
|
+
* docs/telemetry.md lists this set, and telemetry-docs-parity.test.ts checks it.
|
|
211
|
+
*/
|
|
212
|
+
export const BEACON_STAGES = ['install', 'setup_completed'];
|
|
158
213
|
/**
|
|
159
214
|
* The single funnel-stage emitter. Same consent model as recordCommandUsage, no new
|
|
160
|
-
* consent surface: cloud is opt-out behind a token, local is
|
|
161
|
-
*
|
|
162
|
-
* ("first_useful_decision via ask"), never arguments
|
|
215
|
+
* consent surface: cloud is opt-out behind a token, local is behind the stored decision
|
|
216
|
+
* except for the beacon tier (BEACON_STAGES), and the env switches beat everything.
|
|
217
|
+
* `command` is the stage's provenance ("first_useful_decision via ask"), never arguments
|
|
218
|
+
* or content.
|
|
163
219
|
*
|
|
164
220
|
* first_useful_decision is once-per-install, guarded HERE rather than at call sites so
|
|
165
221
|
* there is exactly one enforcement point. Marked before the send, deliberately: a ping
|
|
166
222
|
* lost to a timeout costs one funnel row in the undercount direction, while marking
|
|
167
223
|
* after would re-send forever on a machine that cannot reach the gateway.
|
|
224
|
+
*
|
|
225
|
+
* Resolves `true` when a send was made (not necessarily delivered - see the once-mark
|
|
226
|
+
* reasoning above, the same trade), `false` when this call could not send: opted out, no
|
|
227
|
+
* consent, no token, the store threw, or - for first_useful_decision only, the one
|
|
228
|
+
* once-per-install stage - already recorded on this install. ALI-949: the setup wizard
|
|
229
|
+
* offers setup_started at several checkpoints because local-mode consent arrives
|
|
230
|
+
* mid-wizard, and this is how it knows which offer landed (lib/setup-funnel.ts).
|
|
168
231
|
*/
|
|
169
|
-
export async function recordFunnelStage(env, stage, command) {
|
|
232
|
+
export async function recordFunnelStage(env, stage, command, measurement) {
|
|
170
233
|
// The whole body is guarded: telemetry must never fail or delay a command (the same
|
|
171
234
|
// invariant postWithTimeout enforces for the network half, extended to the config
|
|
172
235
|
// half). The concrete case: an emitter call site inside a command's try block plus a
|
|
@@ -174,11 +237,18 @@ export async function recordFunnelStage(env, stage, command) {
|
|
|
174
237
|
// in a file this change never touched said so (tdd.md's hand-built-fake rule).
|
|
175
238
|
try {
|
|
176
239
|
if (telemetryOptedOut())
|
|
177
|
-
return;
|
|
240
|
+
return false;
|
|
241
|
+
// ALI-835: never from inside an agent hook. A hook runs on the agent's clock, many times a
|
|
242
|
+
// session, with nobody watching - so a ping from there would measure an editing loop rather
|
|
243
|
+
// than a person choosing to do something, which is what every funnel stage means. Guarded
|
|
244
|
+
// HERE for the same reason first_useful_decision's once-check is: one enforcement point
|
|
245
|
+
// rather than one per call site.
|
|
246
|
+
if (inHookContext())
|
|
247
|
+
return false;
|
|
178
248
|
const { createConfigStore } = await import('./config.js');
|
|
179
249
|
const config = createConfigStore();
|
|
180
250
|
if (stage === 'first_useful_decision' && config.wasFunnelStageRecorded(stage))
|
|
181
|
-
return;
|
|
251
|
+
return false;
|
|
182
252
|
// Whether this call CAN send, checked before the once-mark (Copilot on #215):
|
|
183
253
|
// marking an unsendable call permanently burned the stage for exactly the opt-in
|
|
184
254
|
// cohort - a not-yet-consented user's first real answer marked the install, and
|
|
@@ -187,26 +257,34 @@ export async function recordFunnelStage(env, stage, command) {
|
|
|
187
257
|
// re-sending forever.
|
|
188
258
|
const isLocal = env.mode === 'local-embedded';
|
|
189
259
|
const canSend = isLocal
|
|
190
|
-
? config.getTelemetryConsent()
|
|
260
|
+
? localTierAllows(config.getTelemetryConsent(), stage)
|
|
191
261
|
: Boolean(env.authToken && env.tenantId);
|
|
192
262
|
if (!canSend)
|
|
193
|
-
return;
|
|
263
|
+
return false;
|
|
194
264
|
if (stage === 'first_useful_decision')
|
|
195
265
|
config.markFunnelStageRecorded(stage);
|
|
196
266
|
const commandPath = commandPathOf(command);
|
|
267
|
+
// ALI-835 (Copilot on #286): the measurement is admitted only on the stages the gateway
|
|
268
|
+
// permits it on. Gated HERE rather than at the two payload sites below, for the same
|
|
269
|
+
// reason first_useful_decision's once-check is: one enforcement point. The gateway's
|
|
270
|
+
// schema is `.strict()` with a superRefine refusing `count` and `agent` on every other
|
|
271
|
+
// stage, so a future call site passing a measurement alongside, say, `mcp_wired` would
|
|
272
|
+
// 400 the whole ping - and the catch below swallows that, so the stage would go silently
|
|
273
|
+
// missing rather than fail loudly. Dropping the two fields keeps the stage itself.
|
|
274
|
+
const measured = measurement && SESSION_IMPORT_STAGES.includes(stage)
|
|
275
|
+
? { count: measurement.count, agent: measurement.agent }
|
|
276
|
+
: {};
|
|
197
277
|
if (isLocal) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
stage,
|
|
207
|
-
}),
|
|
278
|
+
await postAnonymous({
|
|
279
|
+
installId: config.getInstallId(),
|
|
280
|
+
command: commandPath,
|
|
281
|
+
cliVersion: pkg.version,
|
|
282
|
+
stage,
|
|
283
|
+
// Spread rather than set: the gateway's schema is `.strict()`, and an explicit
|
|
284
|
+
// undefined key is still a key.
|
|
285
|
+
...measured,
|
|
208
286
|
});
|
|
209
|
-
return;
|
|
287
|
+
return true;
|
|
210
288
|
}
|
|
211
289
|
await postWithTimeout(`${env.gatewayUrl}/telemetry/ingest`, {
|
|
212
290
|
method: 'POST',
|
|
@@ -219,14 +297,108 @@ export async function recordFunnelStage(env, stage, command) {
|
|
|
219
297
|
eventName: `cli.funnel.${stage}`,
|
|
220
298
|
category: 'engagement',
|
|
221
299
|
platform: 'cli',
|
|
222
|
-
properties: { command: commandPath },
|
|
300
|
+
properties: { command: commandPath, ...measured },
|
|
223
301
|
}),
|
|
224
302
|
});
|
|
303
|
+
return true;
|
|
225
304
|
}
|
|
226
305
|
catch {
|
|
227
306
|
// Swallowed for the reason above. The funnel loses one row; the command survives.
|
|
307
|
+
return false;
|
|
228
308
|
}
|
|
229
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* ALI-954: whether a local-mode stage may send, given the stored decision. A beacon stage
|
|
312
|
+
* sends unless the user ran `align telemetry off` (stored 'off'); every other stage needs
|
|
313
|
+
* a granted consent. A prompt-declined consent ('declined') is a decision about USAGE and
|
|
314
|
+
* leaves the beacons on - that split is the ticket's decision, and docs/telemetry.md says
|
|
315
|
+
* it in the user's words.
|
|
316
|
+
*/
|
|
317
|
+
function localTierAllows(consent, stage) {
|
|
318
|
+
if (BEACON_STAGES.includes(stage))
|
|
319
|
+
return consent !== 'off';
|
|
320
|
+
return consent === 'granted';
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* The anonymous payload, the one place its shape is spelled. Targets ALIGN_HOSTED_GATEWAY_URL,
|
|
324
|
+
* never a `gatewayUrl` off the env - see recordAnonymousCommandUsage for why.
|
|
325
|
+
*/
|
|
326
|
+
async function postAnonymous(payload) {
|
|
327
|
+
const target = process.env['ALIGN_GATEWAY_URL'] || ALIGN_HOSTED_GATEWAY_URL;
|
|
328
|
+
await postWithTimeout(`${target}/telemetry/anonymous`, {
|
|
329
|
+
method: 'POST',
|
|
330
|
+
headers: { 'Content-Type': 'application/json' },
|
|
331
|
+
body: JSON.stringify(payload),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* ALI-954: the install beacon - the funnel's denominator. Sent from the preAction hook in
|
|
336
|
+
* index.ts, so it goes out BEFORE any prompt on the very first run of this install id, and
|
|
337
|
+
* never again. The payload is the CLI version, the OS (`process.platform`, a closed enum the
|
|
338
|
+
* gateway also constrains), the install id, and the literal command `align` - the endpoint
|
|
339
|
+
* requires a command and this one must not carry which command was run first, so it sends
|
|
340
|
+
* the program's own name, which carries no information. Nothing about the repo, the graph,
|
|
341
|
+
* or the user.
|
|
342
|
+
*
|
|
343
|
+
* "First ever run" is a moment, not a state, so the install is marked recorded on that run
|
|
344
|
+
* whether or not the beacon went out: `DO_NOT_TRACK=1` or `ALIGN_TELEMETRY=0` set before the
|
|
345
|
+
* first run means it is NEVER sent, by construction, even if the variable is later unset.
|
|
346
|
+
* That is the opposite trade from first_useful_decision's once-mark (which waits for
|
|
347
|
+
* sendability so a later opt-in still emits), and it is deliberate: a beacon on the fifth
|
|
348
|
+
* run would not be the install it claims to be, and "never" is the promise the docs make.
|
|
349
|
+
*
|
|
350
|
+
* Two cases do not consume the first run: `align telemetry ...` as the user's first command
|
|
351
|
+
* (the off switch must not be raced by the thing it switches off), and a run that already
|
|
352
|
+
* holds a cloud token (cloud mode is unchanged by this ticket - its funnel is the authed
|
|
353
|
+
* one). Resolves true when a send was made, false otherwise; never throws, like every other
|
|
354
|
+
* emitter here.
|
|
355
|
+
*/
|
|
356
|
+
export async function recordInstallBeacon(commandPath) {
|
|
357
|
+
try {
|
|
358
|
+
if (commandPath === 'telemetry' || commandPath.startsWith('telemetry '))
|
|
359
|
+
return false;
|
|
360
|
+
const { createConfigStore } = await import('./config.js');
|
|
361
|
+
const { resolveEnv } = await import('./resolve-env.js');
|
|
362
|
+
const config = createConfigStore();
|
|
363
|
+
if (config.wasFunnelStageRecorded('install'))
|
|
364
|
+
return false;
|
|
365
|
+
const env = config.getEnvironment(resolveEnv(undefined, { preferLocalEmbedded: true }));
|
|
366
|
+
if (env.authToken)
|
|
367
|
+
return false;
|
|
368
|
+
config.markFunnelStageRecorded('install');
|
|
369
|
+
if (telemetryOptedOut())
|
|
370
|
+
return false;
|
|
371
|
+
if (!localTierAllows(config.getTelemetryConsent(), 'install'))
|
|
372
|
+
return false;
|
|
373
|
+
await postAnonymous({
|
|
374
|
+
installId: config.getInstallId(),
|
|
375
|
+
command: 'align',
|
|
376
|
+
cliVersion: pkg.version,
|
|
377
|
+
stage: 'install',
|
|
378
|
+
os: process.platform,
|
|
379
|
+
});
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
// Telemetry must never fail or delay a command; the funnel loses one row.
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* The command path the postAction hook reports: the full path ("local ask", "import git")
|
|
389
|
+
* so recordCommandUsage can exclude the offline `local` group, and never the leaf alone.
|
|
390
|
+
*
|
|
391
|
+
* ALI-949: the ROOT command has no parent. Bare `align` (`program.action(runDefaultAction)`,
|
|
392
|
+
* ALI-773) is the primary first-run path, and walking `.parent` from the root yielded '' -
|
|
393
|
+
* so every bare invocation was reported as no command at all and the funnel's "activated"
|
|
394
|
+
* stage could not see it. The root reports under the program's own name.
|
|
395
|
+
*/
|
|
396
|
+
export function invocationCommandPath(actionCommand) {
|
|
397
|
+
const parts = [];
|
|
398
|
+
for (let c = actionCommand; c?.parent; c = c.parent)
|
|
399
|
+
parts.unshift(c.name());
|
|
400
|
+
return parts.length > 0 ? parts.join(' ') : actionCommand.name();
|
|
401
|
+
}
|
|
230
402
|
/**
|
|
231
403
|
* The `--env` the user actually typed, including one Commander handed to a parent.
|
|
232
404
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage-telemetry.js","sourceRoot":"","sources":["../../src/lib/usage-telemetry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAiD,MAAM,aAAa,CAAC;AACtG,OAAO,
|
|
1
|
+
{"version":3,"file":"usage-telemetry.js","sourceRoot":"","sources":["../../src/lib/usage-telemetry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type HookHost = 'codex' | 'cursor' | 'copilot';
|
|
2
|
+
export interface UserHookTarget {
|
|
3
|
+
host: HookHost;
|
|
4
|
+
/** The hook file for this host, e.g. ~/.codex/hooks.json. */
|
|
5
|
+
path: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function advisoryHookCommand(host: HookHost, env?: string): string;
|
|
8
|
+
export declare function writeUserHooks(target: UserHookTarget, env?: string): void;
|
|
9
|
+
/**
|
|
10
|
+
* Take our entries back out. Returns whether there was anything of ours to remove.
|
|
11
|
+
* Deletes the file only when nothing but what we wrote (plus the version we added) is left,
|
|
12
|
+
* so a file the user pre-populated survives with their hooks in it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function removeUserHooks(target: UserHookTarget): boolean;
|
|
15
|
+
//# sourceMappingURL=user-hooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-hooks.d.ts","sourceRoot":"","sources":["../../src/lib/user-hooks.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEtD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,QAAQ,CAAC;IACf,6DAA6D;IAC7D,IAAI,EAAE,MAAM,CAAC;CACd;AAMD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAIxE;AA6FD,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAkBzE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAwB/D"}
|