nanoai-cli 1.0.11 → 1.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.
package/README.md CHANGED
@@ -86,6 +86,7 @@ NANOAGENT_SKIP_UPDATE_CHECK=1 nanoai
86
86
  | Variable | Purpose |
87
87
  | --- | --- |
88
88
  | `NANOAGENT_SKIP_DOWNLOAD` | Set to `1` to skip the install-time download. The binary will still be fetched on first run. |
89
+ | `NANOAGENT_TELEMETRY_DISABLED` | Set to `1` to opt out of the anonymous `nanoagent cli installed` analytics event. `DO_NOT_TRACK=1` is also honored. |
89
90
  | `NANOAGENT_SKIP_UPDATE_CHECK` | Set to `1` to disable the runtime check for newer GitHub releases. |
90
91
  | `NANOAGENT_CLI_TAG` | Override the release tag to download, such as `v1.2.3`. |
91
92
  | `NANOAGENT_CLI_VERSION` | Override the version used to derive the default release tag. |
package/bin/nanoai.js CHANGED
@@ -5,6 +5,7 @@ const { spawn } = require("child_process");
5
5
 
6
6
  const { ensureBinary } = require("../scripts/download");
7
7
  const { maybeUpdateBinary } = require("../scripts/update");
8
+ const { trackInstall } = require("../scripts/telemetry");
8
9
 
9
10
  function log(message) {
10
11
  console.error(`[nanoagent] ${message}`);
@@ -23,7 +24,9 @@ function forwardSignal(child, signal) {
23
24
  }
24
25
 
25
26
  async function main() {
26
- let binaryPath = await ensureBinary({ log });
27
+ // On a clean `bun add` (postinstall is skipped) the binary is fetched here on
28
+ // first launch; record the install once at that point.
29
+ let binaryPath = await ensureBinary({ log, onDownloaded: () => trackInstall() });
27
30
  binaryPath = await maybeUpdateBinary(binaryPath, { log });
28
31
 
29
32
  const child = spawn(binaryPath, process.argv.slice(2), {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nanoai-cli",
3
- "version": "1.0.11",
3
+ "version": "1.1.0",
4
4
  "description": "Terminal UI, ACP server, and automation-friendly CLI for NanoAgent.",
5
5
  "keywords": [
6
6
  "nanoagent",
@@ -76,8 +76,10 @@ function extractExecutable(zipBuffer, destinationPath) {
76
76
  }
77
77
 
78
78
  // Ensures the platform binary is present in vendor/. Returns the absolute path.
79
+ // `onDownloaded` is awaited only when a fresh (non-update) binary is fetched, so
80
+ // callers can record an anonymous install event exactly once per real install.
79
81
  async function ensureBinary(options = {}) {
80
- const { force = false, log = () => {}, tag } = options;
82
+ const { force = false, log = () => {}, tag, onDownloaded } = options;
81
83
 
82
84
  const binaryPath = platform.installedBinaryPath();
83
85
  if (!force && fs.existsSync(binaryPath)) {
@@ -121,6 +123,17 @@ async function ensureBinary(options = {}) {
121
123
  fs.renameSync(tempPath, binaryPath);
122
124
 
123
125
  log(`Installed NanoAgent CLI to ${binaryPath}.`);
126
+
127
+ // Fire once per genuine install. Updates pass force=true and are intentionally
128
+ // excluded so they are not counted as new installs.
129
+ if (!force && typeof onDownloaded === "function") {
130
+ try {
131
+ await onDownloaded();
132
+ } catch {
133
+ // Telemetry must never affect installation.
134
+ }
135
+ }
136
+
124
137
  return binaryPath;
125
138
  }
126
139
 
@@ -12,8 +12,12 @@ if (process.env.NANOAGENT_SKIP_DOWNLOAD === "1") {
12
12
  }
13
13
 
14
14
  const { ensureBinary } = require("./download");
15
+ const { trackInstall } = require("./telemetry");
15
16
 
16
- ensureBinary({ log: (m) => console.error(`[nanoagent] ${m}`) })
17
+ ensureBinary({
18
+ log: (m) => console.error(`[nanoagent] ${m}`),
19
+ onDownloaded: () => trackInstall(),
20
+ })
17
21
  .then(() => {
18
22
  console.error("[nanoagent] Ready. Run `nanoai` to start.");
19
23
  })
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+
3
+ // Best-effort, anonymous PostHog "installed" event shared by the postinstall step
4
+ // and the runtime first-run download (covers `bun add`, which skips postinstall).
5
+ // Telemetry must never affect installation: every failure is swallowed and the
6
+ // network call is bounded by a short timeout. Opt out with NANOAGENT_TELEMETRY_DISABLED
7
+ // or the cross-tool DO_NOT_TRACK convention.
8
+
9
+ const crypto = require("crypto");
10
+
11
+ const platform = require("./platform");
12
+
13
+ // Mirrors NanoAgent.Infrastructure.Configuration.TelemetryOptions so install and
14
+ // in-product analytics land in the same PostHog project.
15
+ const TELEMETRY_HOST = "https://us.i.posthog.com";
16
+ const TELEMETRY_PROJECT_TOKEN = "phc_AKZFSyU239kkQ5GQ2y4idb8MtFX96kVekgezgnsELHRk";
17
+ const TELEMETRY_EVENT = "nanoagent cli installed";
18
+ const TIMEOUT_MS = 5000;
19
+
20
+ function isTruthy(value) {
21
+ if (!value) return false;
22
+ return ["1", "true", "yes", "on"].includes(String(value).trim().toLowerCase());
23
+ }
24
+
25
+ function telemetryEnabled() {
26
+ if (isTruthy(process.env.NANOAGENT_TELEMETRY_DISABLED) || isTruthy(process.env.DO_NOT_TRACK)) {
27
+ return false;
28
+ }
29
+ return Boolean(TELEMETRY_PROJECT_TOKEN);
30
+ }
31
+
32
+ // Identify the package manager that triggered the install. npm/pnpm/yarn set
33
+ // npm_config_user_agent during their lifecycle; bun is detected from the runtime
34
+ // when the binary is fetched lazily on first launch.
35
+ function detectInstallMethod() {
36
+ const ua = (process.env.npm_config_user_agent || "").toLowerCase();
37
+ if (ua.includes("bun")) return "bun";
38
+ if (ua.includes("pnpm")) return "pnpm";
39
+ if (ua.includes("yarn")) return "yarn";
40
+ if (ua.includes("npm")) return "npm";
41
+ if (process.versions && process.versions.bun) return "bun";
42
+ return "npm";
43
+ }
44
+
45
+ function osFamily() {
46
+ switch (process.platform) {
47
+ case "win32":
48
+ return "windows";
49
+ case "darwin":
50
+ return "macos";
51
+ case "linux":
52
+ return "linux";
53
+ default:
54
+ return "other";
55
+ }
56
+ }
57
+
58
+ function isCi() {
59
+ return Boolean(
60
+ process.env.CI ||
61
+ process.env.GITHUB_ACTIONS ||
62
+ process.env.GITLAB_CI ||
63
+ process.env.BITBUCKET_BUILD_NUMBER
64
+ );
65
+ }
66
+
67
+ function resolveQuietly(resolver, fallback) {
68
+ try {
69
+ const value = resolver();
70
+ return value && String(value).trim() ? value : fallback;
71
+ } catch {
72
+ return fallback;
73
+ }
74
+ }
75
+
76
+ async function trackInstall(options = {}) {
77
+ try {
78
+ if (!telemetryEnabled() || typeof fetch !== "function") {
79
+ return;
80
+ }
81
+
82
+ const ci = isCi();
83
+ const payload = {
84
+ api_key: TELEMETRY_PROJECT_TOKEN,
85
+ event: TELEMETRY_EVENT,
86
+ distinct_id: crypto.randomUUID(),
87
+ properties: {
88
+ $lib: "nanoagent-installer",
89
+ install_method: options.method || detectInstallMethod(),
90
+ nanoagent_version: resolveQuietly(() => platform.resolveTag(), "unknown"),
91
+ os_family: osFamily(),
92
+ platform: resolveQuietly(() => platform.resolveRid(), "unknown"),
93
+ app_surface: "cli",
94
+ execution_environment: ci ? "ci" : "local",
95
+ is_ci: ci,
96
+ },
97
+ };
98
+
99
+ const signal =
100
+ typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function"
101
+ ? AbortSignal.timeout(TIMEOUT_MS)
102
+ : undefined;
103
+
104
+ await fetch(`${TELEMETRY_HOST}/i/v0/e/`, {
105
+ method: "POST",
106
+ headers: { "Content-Type": "application/json" },
107
+ body: JSON.stringify(payload),
108
+ signal,
109
+ });
110
+ } catch {
111
+ // Telemetry must never affect installation.
112
+ }
113
+ }
114
+
115
+ module.exports = { trackInstall, detectInstallMethod };