@tanagram/cli 0.6.8 → 0.6.23

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
@@ -26,6 +26,12 @@ tanagram help # Show available commands
26
26
 
27
27
  Set `TANAGRAM_TOKEN` to skip interactive login (useful for CI/CD).
28
28
 
29
+ For local development, you can override service hosts with environment variables:
30
+
31
+ - `TANAGRAM_WEB_URL` for browser login
32
+ - `TANAGRAM_API_URL` for normal CLI API requests and the LLM proxy
33
+ - `TANAGRAM_LORE_URL` only for Lore upload session create/complete requests
34
+
29
35
  ## Requirements
30
36
 
31
37
  - Node.js >= 14.0.0
@@ -26,6 +26,12 @@ tanagram help # Show available commands
26
26
 
27
27
  Set `TANAGRAM_TOKEN` to skip interactive login (useful for CI/CD).
28
28
 
29
+ For local development, you can override service hosts with environment variables:
30
+
31
+ - `TANAGRAM_WEB_URL` for browser login
32
+ - `TANAGRAM_API_URL` for normal CLI API requests and the LLM proxy
33
+ - `TANAGRAM_LORE_URL` only for Lore upload session create/complete requests
34
+
29
35
  ## Requirements
30
36
 
31
37
  - Node.js >= 14.0.0
Binary file
@@ -26,6 +26,12 @@ tanagram help # Show available commands
26
26
 
27
27
  Set `TANAGRAM_TOKEN` to skip interactive login (useful for CI/CD).
28
28
 
29
+ For local development, you can override service hosts with environment variables:
30
+
31
+ - `TANAGRAM_WEB_URL` for browser login
32
+ - `TANAGRAM_API_URL` for normal CLI API requests and the LLM proxy
33
+ - `TANAGRAM_LORE_URL` only for Lore upload session create/complete requests
34
+
29
35
  ## Requirements
30
36
 
31
37
  - Node.js >= 14.0.0
Binary file
@@ -26,6 +26,12 @@ tanagram help # Show available commands
26
26
 
27
27
  Set `TANAGRAM_TOKEN` to skip interactive login (useful for CI/CD).
28
28
 
29
+ For local development, you can override service hosts with environment variables:
30
+
31
+ - `TANAGRAM_WEB_URL` for browser login
32
+ - `TANAGRAM_API_URL` for normal CLI API requests and the LLM proxy
33
+ - `TANAGRAM_LORE_URL` only for Lore upload session create/complete requests
34
+
29
35
  ## Requirements
30
36
 
31
37
  - Node.js >= 14.0.0
Binary file
@@ -26,6 +26,12 @@ tanagram help # Show available commands
26
26
 
27
27
  Set `TANAGRAM_TOKEN` to skip interactive login (useful for CI/CD).
28
28
 
29
+ For local development, you can override service hosts with environment variables:
30
+
31
+ - `TANAGRAM_WEB_URL` for browser login
32
+ - `TANAGRAM_API_URL` for normal CLI API requests and the LLM proxy
33
+ - `TANAGRAM_LORE_URL` only for Lore upload session create/complete requests
34
+
29
35
  ## Requirements
30
36
 
31
37
  - Node.js >= 14.0.0
Binary file
@@ -26,6 +26,12 @@ tanagram help # Show available commands
26
26
 
27
27
  Set `TANAGRAM_TOKEN` to skip interactive login (useful for CI/CD).
28
28
 
29
+ For local development, you can override service hosts with environment variables:
30
+
31
+ - `TANAGRAM_WEB_URL` for browser login
32
+ - `TANAGRAM_API_URL` for normal CLI API requests and the LLM proxy
33
+ - `TANAGRAM_LORE_URL` only for Lore upload session create/complete requests
34
+
29
35
  ## Requirements
30
36
 
31
37
  - Node.js >= 14.0.0
Binary file
package/install.js CHANGED
@@ -3,56 +3,20 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
- const https = require('https');
7
- const crypto = require('crypto');
8
- const pkg = require('./package.json');
9
-
10
- // Discover skills from the skills/ directory
11
- const SKILLS = fs.readdirSync(path.join(__dirname, 'skills'), { withFileTypes: true })
12
- .filter(d => d.isDirectory())
13
- .map(d => d.name);
14
-
15
- const POSTHOG_KEY = 'phc_sMsUvf0nK50rZdztSlX9rDJqIreLcXj4dyGS0tORQpQ';
16
- const POSTHOG_HOST = 'phe.tanagram.ai';
17
-
18
- // Generate anonymous distinct ID based on machine (must match Go CLI convention in metrics/metrics.go)
19
- function getDistinctId() {
20
- return 'cli_' + os.hostname();
21
- }
22
-
23
- // Track event to PostHog
24
- function track(event, properties = {}) {
25
- if (isCIEnvironment()) return;
26
-
27
- const data = JSON.stringify({
28
- api_key: POSTHOG_KEY,
29
- event: event,
30
- properties: {
31
- distinct_id: getDistinctId(),
32
- version: pkg.version,
33
- platform: os.platform(),
34
- arch: os.arch(),
35
- node_version: process.version,
36
- ...properties
37
- },
38
- timestamp: new Date().toISOString()
39
- });
40
-
41
- const req = https.request({
42
- hostname: POSTHOG_HOST,
43
- port: 443,
44
- path: '/capture/',
45
- method: 'POST',
46
- headers: {
47
- 'Content-Type': 'application/json',
48
- 'Content-Length': data.length
49
- }
50
- });
51
-
52
- req.on('error', () => {}); // Ignore errors
53
- req.write(data);
54
- req.end();
55
- }
6
+ const { SKILLS } = require('./skills.config');
7
+ const {
8
+ createTracker,
9
+ detectInstallPackageManager,
10
+ isCIEnvironment,
11
+ loreDir,
12
+ loreInstallSourcePath,
13
+ loreLaunchAgentPlistPath,
14
+ loreStableBinaryPath,
15
+ tanagramDir,
16
+ LORE_LABEL
17
+ } = require('./npm-shared');
18
+
19
+ const track = createTracker();
56
20
 
57
21
  // Map Node.js platform/arch to directory naming
58
22
  function getPlatformInfo() {
@@ -119,16 +83,79 @@ function installPrebuiltBinary(sourcePath) {
119
83
  return targetPath;
120
84
  }
121
85
 
122
- function isCIEnvironment() {
123
- return (
124
- process.env.CI === 'true' ||
125
- process.env.GITHUB_ACTIONS === 'true' ||
126
- process.env.BUILDKITE === 'true' ||
127
- process.env.GITLAB_CI === 'true' ||
128
- process.env.TF_BUILD === 'True'
86
+ function isLoreEnabled() {
87
+ return fs.existsSync(loreLaunchAgentPlistPath());
88
+ }
89
+
90
+ function copyFileAtomic(sourcePath, targetPath) {
91
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
92
+ const tempPath = `${targetPath}.tmp`;
93
+ fs.copyFileSync(sourcePath, tempPath);
94
+ if (os.platform() !== 'win32') {
95
+ fs.chmodSync(tempPath, '755');
96
+ }
97
+ fs.renameSync(tempPath, targetPath);
98
+ }
99
+
100
+ function writeLoreInstallSource(sourcePath) {
101
+ fs.mkdirSync(loreDir(), { recursive: true });
102
+ fs.writeFileSync(
103
+ loreInstallSourcePath(),
104
+ JSON.stringify(
105
+ {
106
+ package_manager: detectInstallPackageManager(),
107
+ binary_path: sourcePath,
108
+ updated_at: new Date().toISOString()
109
+ },
110
+ null,
111
+ 2
112
+ ) + '\n'
129
113
  );
130
114
  }
131
115
 
116
+ function restartLoreLaunchAgent() {
117
+ if (os.platform() !== 'darwin' || !isLoreEnabled()) {
118
+ return;
119
+ }
120
+
121
+ const { spawnSync } = require('child_process');
122
+ const uid = os.userInfo().uid;
123
+ const launchTarget = `gui/${uid}/${LORE_LABEL}`;
124
+
125
+ let result = spawnSync('launchctl', ['kickstart', '-k', launchTarget], { encoding: 'utf8' });
126
+ if (result.status === 0) {
127
+ return;
128
+ }
129
+
130
+ result = spawnSync('launchctl', ['bootstrap', `gui/${uid}`, loreLaunchAgentPlistPath()], { encoding: 'utf8' });
131
+ const bootstrapOutput = `${result.stdout || ''}${result.stderr || ''}`;
132
+ if (result.status !== 0 && !/already exists/i.test(bootstrapOutput)) {
133
+ throw new Error(bootstrapOutput.trim() || 'launchctl bootstrap failed');
134
+ }
135
+
136
+ result = spawnSync('launchctl', ['kickstart', '-k', launchTarget], { encoding: 'utf8' });
137
+ if (result.status !== 0) {
138
+ throw new Error(`${result.stdout || ''}${result.stderr || ''}`.trim() || 'launchctl kickstart failed');
139
+ }
140
+ }
141
+
142
+ function refreshLoreBinaryIfEnabled(sourcePath) {
143
+ if (os.platform() !== 'darwin' || !isLoreEnabled()) {
144
+ return;
145
+ }
146
+
147
+ try {
148
+ copyFileAtomic(sourcePath, loreStableBinaryPath());
149
+ writeLoreInstallSource(sourcePath);
150
+ restartLoreLaunchAgent();
151
+ console.error('✓ Refreshed Lore background service binary');
152
+ track('cli.lore.refresh.success', { package_manager: detectInstallPackageManager() });
153
+ } catch (err) {
154
+ console.error(`⚠ Failed to refresh Lore background service: ${err.message}`);
155
+ track('cli.lore.refresh.failure', { error: err.message });
156
+ }
157
+ }
158
+
132
159
  function installSkill(skillName) {
133
160
  try {
134
161
  const skillsSourceDir = path.join(__dirname, 'skills', skillName);
@@ -276,8 +303,7 @@ function ensureOpenCode() {
276
303
  // Main installation flow
277
304
  (async () => {
278
305
  // Check if this is a first-time install (no .tanagram directory exists)
279
- const tanagramDir = path.join(os.homedir(), '.tanagram');
280
- const isFirstTimeUser = !fs.existsSync(tanagramDir);
306
+ const isFirstTimeUser = !fs.existsSync(tanagramDir());
281
307
 
282
308
  // Track install start
283
309
  track('cli.install.start', { first_time: isFirstTimeUser, install_method: 'npm' });
@@ -285,7 +311,8 @@ function ensureOpenCode() {
285
311
  try {
286
312
  // Find and install prebuilt binary
287
313
  const prebuiltPath = findPrebuiltBinary();
288
- installPrebuiltBinary(prebuiltPath);
314
+ const installedBinaryPath = installPrebuiltBinary(prebuiltPath);
315
+ refreshLoreBinaryIfEnabled(installedBinaryPath);
289
316
 
290
317
  // Install Claude skills
291
318
  installClaudeSkills();
package/npm-shared.js ADDED
@@ -0,0 +1,110 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const https = require('https');
5
+ const pkg = require('./package.json');
6
+
7
+ const POSTHOG_KEY = 'phc_sMsUvf0nK50rZdztSlX9rDJqIreLcXj4dyGS0tORQpQ';
8
+ const POSTHOG_HOST = 'phe.tanagram.ai';
9
+ const LORE_LABEL = 'com.tanagram.cli.lore';
10
+
11
+ function defaultDistinctId() {
12
+ return `cli_${os.hostname()}`;
13
+ }
14
+
15
+ function createTracker(options = {}) {
16
+ const getDistinctId = options.getDistinctId || defaultDistinctId;
17
+
18
+ return function track(event, properties = {}) {
19
+ if (isCIEnvironment()) return;
20
+
21
+ const data = JSON.stringify({
22
+ api_key: POSTHOG_KEY,
23
+ event,
24
+ properties: {
25
+ distinct_id: getDistinctId(),
26
+ version: pkg.version,
27
+ platform: os.platform(),
28
+ arch: os.arch(),
29
+ node_version: process.version,
30
+ ...properties
31
+ },
32
+ timestamp: new Date().toISOString()
33
+ });
34
+
35
+ const req = https.request({
36
+ hostname: POSTHOG_HOST,
37
+ port: 443,
38
+ path: '/capture/',
39
+ method: 'POST',
40
+ headers: {
41
+ 'Content-Type': 'application/json',
42
+ 'Content-Length': data.length
43
+ }
44
+ });
45
+
46
+ req.on('error', () => {});
47
+ req.write(data);
48
+ req.end();
49
+ };
50
+ }
51
+
52
+ function isCIEnvironment() {
53
+ return (
54
+ process.env.CI === 'true' ||
55
+ process.env.GITHUB_ACTIONS === 'true' ||
56
+ process.env.BUILDKITE === 'true' ||
57
+ process.env.GITLAB_CI === 'true' ||
58
+ process.env.TF_BUILD === 'True'
59
+ );
60
+ }
61
+
62
+ function tanagramDir() {
63
+ return path.join(os.homedir(), '.tanagram');
64
+ }
65
+
66
+ function loreDir() {
67
+ return path.join(tanagramDir(), 'lore');
68
+ }
69
+
70
+ function loreStableBinaryPath() {
71
+ return path.join(tanagramDir(), 'bin', 'tanagram-lore');
72
+ }
73
+
74
+ function loreInstallSourcePath() {
75
+ return path.join(loreDir(), 'install_source.json');
76
+ }
77
+
78
+ function loreStatusPath() {
79
+ return path.join(loreDir(), 'status.json');
80
+ }
81
+
82
+ function loreLaunchAgentPlistPath() {
83
+ return path.join(os.homedir(), 'Library', 'LaunchAgents', `${LORE_LABEL}.plist`);
84
+ }
85
+
86
+ function detectInstallPackageManager() {
87
+ const userAgent = (process.env.npm_config_user_agent || '').toLowerCase();
88
+ const execPath = (process.env.npm_execpath || '').toLowerCase();
89
+
90
+ if (userAgent.includes('pnpm') || execPath.includes('pnpm')) {
91
+ return 'pnpm';
92
+ }
93
+ if (userAgent.includes('yarn') || execPath.includes('yarn')) {
94
+ return 'yarn';
95
+ }
96
+ return 'npm';
97
+ }
98
+
99
+ module.exports = {
100
+ createTracker,
101
+ detectInstallPackageManager,
102
+ isCIEnvironment,
103
+ loreDir,
104
+ loreInstallSourcePath,
105
+ loreLaunchAgentPlistPath,
106
+ loreStableBinaryPath,
107
+ loreStatusPath,
108
+ tanagramDir,
109
+ LORE_LABEL
110
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanagram/cli",
3
- "version": "0.6.8",
3
+ "version": "0.6.23",
4
4
  "description": "Tanagram - Catch sloppy code before it ships",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -34,8 +34,9 @@
34
34
  "bin/tanagram.js",
35
35
  "dist/npm/",
36
36
  "install.js",
37
+ "npm-shared.js",
37
38
  "uninstall.js",
38
- "skills.config",
39
+ "skills.config.js",
39
40
  "skills/",
40
41
  "README.md",
41
42
  "LICENSE"
@@ -0,0 +1,136 @@
1
+ ---
2
+ name: tanagram-codify
3
+ description: Codify an observed repeatable, enforceable code pattern into a Tanagram rule. Use when you observe the opportunity to codify a repeatable rule or when prompted by the user
4
+ allowed-tools: Bash
5
+ ---
6
+
7
+ # Tanagram Codify — Turn Observed Patterns Into Rules
8
+
9
+ Turns a concrete, repeatable code pattern — observed in a diff, a file, or described by the user — into an enforceable Tanagram rule.
10
+
11
+ ## When to Use
12
+
13
+ Use this skill when:
14
+ - The user explicitly asks to "codify", "enforce", or "make a rule" for a pattern
15
+ - You observe a `git diff` that introduces a new utility, decorator, or abstraction that *every* call site should adopt
16
+ - A pattern is clearly intentional (not a one-off), mechanical enough to check programmatically, and worth catching in code review
17
+
18
+ **Do NOT create a rule when:**
19
+ - The pattern is already present in tanagram rules (observed via `tanagram rules list`)
20
+ - The pattern is already caught by a linter or type checker
21
+ - The pattern is too context-specific to apply broadly
22
+ - The "pattern" is just a style preference with no correctness or reliability implication
23
+ - The diff only has one call site and no obvious future surface area
24
+
25
+ ## Prerequisites
26
+
27
+ - `tanagram` CLI installed and authenticated (`tanagram login`)
28
+ - Inside a git repository with relevant code visible
29
+
30
+ ## Procedure
31
+
32
+ ### Step 1: Identify the pattern
33
+
34
+ Study the code — the diff, the file, or the user's description — and extract:
35
+
36
+ 1. **The canonical correct pattern**: What does correct code look like? Include the import, decorator, function call, or structure as concretely as possible.
37
+ 2. **The anti-pattern it replaces**: What would a developer write if they didn't know about this convention? Be specific.
38
+ 3. **The consequence of the anti-pattern**: What goes wrong — silent failure, missing observability, race condition, inconsistency, etc.?
39
+ 4. **The scope**: Which files, layers, or contexts does this rule apply to? (e.g., "all Inngest function handlers", "all API route files", "all database models")
40
+
41
+ A pattern is worth codifying when it meets **all three** of these:
42
+ - **Repeatable** — The same structure will appear again as the codebase grows
43
+ - **Mechanical** — A reviewer (or LLM) can recognize a violation without deep context
44
+ - **Consequential** — Getting it wrong produces a meaningful problem (not just aesthetic)
45
+
46
+ ### Step 2: Check for duplicate rules
47
+
48
+ ```bash
49
+ tanagram rules list --json
50
+ ```
51
+
52
+ Read the `name` and `description` of existing rules. If an existing rule already covers the pattern (even partially), skip creation and report the overlap to the user.
53
+
54
+ ### Step 3: Determine the repo slug
55
+
56
+ Look at the `repos` field in the output from Step 2. Use the same slug (e.g., `tanagram/monorepo`) for the new rule. If no existing rules exist, ask the user for the repo slug.
57
+
58
+ ### Step 4: Draft the rule
59
+
60
+ A well-formed rule has two parts:
61
+
62
+ **Name** — PascalCase, specific, self-explanatory without reading the description. 5–7 words maximum.
63
+ - Good: `InngestFunctionsMustUseTraceDecorator`
64
+ - Bad: `UseDecoratorPattern`, `FixInngest`
65
+
66
+ **Description** — Must include all four of these, in order:
67
+ 1. **Scope**: What kind of code this applies to
68
+ 2. **Anti-pattern**: What incorrect code looks like (quote or paraphrase real code)
69
+ 3. **Consequence**: What goes wrong if the anti-pattern is used
70
+ 4. **Correct pattern**: What correct code looks like, including the import if relevant
71
+
72
+ Example of a well-formed description:
73
+ > Every Inngest function handler and its `on_failure` handler must be decorated with `@trace_inngest_function('function-id')`, using the same ID as `fn_id=` in `create_function(...)`. This decorator opens a root Sentry transaction and sets the `inngest.function` tag. Anti-pattern: manually calling `sentry_sdk.set_tag("inngest.function", "...")` inside the function body — this is ad-hoc, easy to forget, and not scoped to a transaction. Correct: `from src.inngest.functions.utils.tracing import trace_inngest_function` then apply `@trace_inngest_function("my-function-id")` immediately below the `@create_function(...)` decorator.
74
+
75
+ ### Step 5: Ask for permission
76
+ Present your proposal to the user and ask for confirmation to create a Tanagram rule
77
+
78
+ ### Step 6: Create the rule
79
+
80
+ ```bash
81
+ tanagram rules create \
82
+ --name "RuleNameInPascalCase" \
83
+ --description "Scope. Anti-pattern: ... Consequence: ... Correct pattern: ..." \
84
+ --repos "<repo-slug>"
85
+ ```
86
+
87
+ ### Step 7: Confirm and report
88
+
89
+ After the CLI returns a rule ID, tell the user:
90
+
91
+ > **Codified:** "[Rule Name]" (`pol_xxxx`)
92
+ >
93
+ > Catches: [one sentence on what it catches]
94
+ > Scope: [what code it applies to]
95
+
96
+ ## Quality Bar for Descriptions
97
+
98
+ Before submitting, verify the description answers all of these:
99
+ - [ ] What code does this apply to? (scope)
100
+ - [ ] What does the wrong version look like?
101
+ - [ ] Why is the wrong version harmful?
102
+ - [ ] What does the right version look like?
103
+ - [ ] Is there an import or specific symbol the correct pattern requires?
104
+
105
+ ## Examples
106
+
107
+ ### Pattern: a new decorator that all handlers must use
108
+
109
+ User shows a diff introducing `@trace_inngest_function` and applying it to several Inngest handlers.
110
+
111
+ ```bash
112
+ tanagram rules create \
113
+ --name "InngestFunctionsMustUseTraceDecorator" \
114
+ --description "Every Inngest function handler (and its on_failure handler) must be decorated with @trace_inngest_function('function-id') from src.inngest.functions.utils.tracing, using the same function ID string passed to fn_id= in create_function(). Anti-pattern: calling sentry_sdk.set_tag('inngest.function', '...') manually inside the function body — this is ad-hoc and not scoped to a Sentry transaction. Correct: apply @trace_inngest_function('my-fn-id') immediately below the @create_function(...) decorator." \
115
+ --repos "tanagram/monorepo"
116
+ ```
117
+
118
+ ### Pattern: use set_context instead of multiple set_tag calls
119
+
120
+ User shows a diff migrating from several `set_tag` calls to one `set_context` with a dict.
121
+
122
+ ```bash
123
+ tanagram rules create \
124
+ --name "UseSetContextForStructuredSentryData" \
125
+ --description "When attaching multiple related fields to a Sentry event (e.g., fields about a GitHub check run, a repository, or a session), use sentry_sdk.set_context('context-name', {...}) with a single dict rather than multiple sentry_sdk.set_tag() calls. Anti-pattern: set_tag('github.check_run_id', ...) followed by set_tag('github.pull_request_number', ...) — this scatters related data and is harder to query. Correct: sentry_sdk.set_context('github.check_run', {'owner_login': ..., 'check_run_id': ..., 'pull_request_numbers': ...}). Reserve set_tag for single scalar values used as Sentry filter dimensions." \
126
+ --repos "acme/monorepo"
127
+ ```
128
+
129
+ ## Common Mistakes to Avoid
130
+
131
+ - **Too vague**: "Use the new utility" — a rule must describe what violation looks like in code
132
+ - **Bundling**: Don't combine two unrelated patterns into one rule; create separate rules
133
+ - **Over-scoping**: Don't apply a rule to the whole codebase if it only applies to one layer (e.g., Inngest handlers)
134
+ - **Redundant rules**: Always check existing rules first
135
+ - **Missing the anti-pattern**: A rule without a concrete anti-pattern is not checkable
136
+
@@ -0,0 +1,5 @@
1
+ // Shared list of Claude Code skills installed/uninstalled by the CLI.
2
+ // Both install.js and uninstall.js reference this to stay in sync.
3
+ const SKILLS = ['tanagram', 'tanagram-mine', 'tanagram-codify'];
4
+
5
+ module.exports = { SKILLS };
package/uninstall.js CHANGED
@@ -1,99 +1,88 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const fs = require('fs');
4
- const path = require('path');
5
- const os = require('os');
6
- const https = require('https');
7
- const crypto = require('crypto');
8
- const pkg = require('./package.json');
9
-
10
- // Discover skills from the skills/ directory
11
- const SKILLS = fs.readdirSync(path.join(__dirname, 'skills'), { withFileTypes: true })
12
- .filter(d => d.isDirectory())
13
- .map(d => d.name);
14
-
15
- const POSTHOG_KEY = 'phc_sMsUvf0nK50rZdztSlX9rDJqIreLcXj4dyGS0tORQpQ';
16
- const POSTHOG_HOST = 'phe.tanagram.ai';
17
-
18
- function getDistinctId() {
19
- const machineId = os.hostname() + os.userInfo().username;
20
- return crypto.createHash('sha256').update(machineId).digest('hex').slice(0, 16);
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+ const crypto = require("crypto");
7
+ const { SKILLS } = require("./skills.config");
8
+ const {
9
+ createTracker,
10
+ isCIEnvironment,
11
+ loreInstallSourcePath,
12
+ loreLaunchAgentPlistPath,
13
+ loreStableBinaryPath,
14
+ loreStatusPath,
15
+ LORE_LABEL,
16
+ } = require("./npm-shared");
17
+
18
+ const track = createTracker({
19
+ getDistinctId() {
20
+ const machineId = os.hostname() + os.userInfo().username;
21
+ return crypto
22
+ .createHash("sha256")
23
+ .update(machineId)
24
+ .digest("hex")
25
+ .slice(0, 16);
26
+ },
27
+ });
28
+
29
+ function removeIfExists(targetPath) {
30
+ if (fs.existsSync(targetPath)) {
31
+ fs.rmSync(targetPath, { recursive: true, force: true });
32
+ }
21
33
  }
22
34
 
23
- function track(event, properties = {}) {
24
- if (isCIEnvironment()) return;
25
-
26
- const data = JSON.stringify({
27
- api_key: POSTHOG_KEY,
28
- event: event,
29
- properties: {
30
- distinct_id: getDistinctId(),
31
- version: pkg.version,
32
- platform: os.platform(),
33
- arch: os.arch(),
34
- node_version: process.version,
35
- ...properties
36
- },
37
- timestamp: new Date().toISOString()
38
- });
39
-
40
- const req = https.request({
41
- hostname: POSTHOG_HOST,
42
- port: 443,
43
- path: '/capture/',
44
- method: 'POST',
45
- headers: {
46
- 'Content-Type': 'application/json',
47
- 'Content-Length': data.length
48
- }
49
- });
50
-
51
- req.on('error', () => {});
52
- req.write(data);
53
- req.end();
54
- }
35
+ function removeLoreArtifacts() {
36
+ if (os.platform() === "darwin") {
37
+ const { spawnSync } = require("child_process");
38
+ spawnSync(
39
+ "launchctl",
40
+ ["bootout", `gui/${os.userInfo().uid}`, loreLaunchAgentPlistPath()],
41
+ { encoding: "utf8" },
42
+ );
43
+ }
55
44
 
56
- function isCIEnvironment() {
57
- return (
58
- process.env.CI === 'true' ||
59
- process.env.GITHUB_ACTIONS === 'true' ||
60
- process.env.BUILDKITE === 'true' ||
61
- process.env.GITLAB_CI === 'true' ||
62
- process.env.TF_BUILD === 'True'
63
- );
45
+ removeIfExists(loreLaunchAgentPlistPath());
46
+ removeIfExists(loreStableBinaryPath());
47
+ removeIfExists(loreStatusPath());
48
+ removeIfExists(loreInstallSourcePath());
64
49
  }
65
50
 
66
51
  function removeClaudeSkills() {
67
52
  for (const skill of SKILLS) {
68
- const skillsDir = path.join(os.homedir(), '.claude', 'skills', skill);
53
+ const skillsDir = path.join(os.homedir(), ".claude", "skills", skill);
69
54
 
70
55
  try {
71
56
  if (fs.existsSync(skillsDir)) {
72
57
  fs.rmSync(skillsDir, { recursive: true });
73
58
  console.error(`✓ ${skill} skill removed`);
74
- track('cli.skill.uninstall.success', { skill });
59
+ track("cli.skill.uninstall.success", { skill });
75
60
  }
76
61
  } catch (err) {
77
62
  console.error(`Warning: Failed to remove ${skill} skill:`, err.message);
78
- track('cli.skill.uninstall.failure', { skill, error: err.message });
63
+ track("cli.skill.uninstall.failure", { skill, error: err.message });
79
64
  }
80
65
  }
81
66
  }
82
67
 
83
68
  function removeStopHook() {
84
- const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
69
+ const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
85
70
 
86
71
  try {
87
72
  if (!fs.existsSync(settingsPath)) return;
88
73
 
89
- const raw = fs.readFileSync(settingsPath, 'utf8');
74
+ const raw = fs.readFileSync(settingsPath, "utf8");
90
75
  const settings = JSON.parse(raw);
91
76
 
92
77
  if (!settings.hooks || !Array.isArray(settings.hooks.Stop)) return;
93
78
 
94
79
  const before = settings.hooks.Stop.length;
95
- settings.hooks.Stop = settings.hooks.Stop.filter(entry =>
96
- !(Array.isArray(entry.hooks) && entry.hooks.some(h => h.command === 'tanagram stop-hook'))
80
+ settings.hooks.Stop = settings.hooks.Stop.filter(
81
+ (entry) =>
82
+ !(
83
+ Array.isArray(entry.hooks) &&
84
+ entry.hooks.some((h) => h.command === "tanagram stop-hook")
85
+ ),
97
86
  );
98
87
  const after = settings.hooks.Stop.length;
99
88
 
@@ -107,43 +96,44 @@ function removeStopHook() {
107
96
  delete settings.hooks;
108
97
  }
109
98
 
110
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
111
- console.error('✓ Tanagram Stop hook removed');
112
- track('cli.stop_hook.uninstall.success');
99
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
100
+ console.error("✓ Tanagram Stop hook removed");
101
+ track("cli.stop_hook.uninstall.success");
113
102
  } catch (err) {
114
- console.error('Warning: Failed to remove Stop hook:', err.message);
115
- track('cli.stop_hook.uninstall.failure', { error: err.message });
103
+ console.error("Warning: Failed to remove Stop hook:", err.message);
104
+ track("cli.stop_hook.uninstall.failure", { error: err.message });
116
105
  }
117
106
  }
118
107
 
119
108
  function removeOpenCode() {
120
109
  if (isCIEnvironment()) return;
121
110
 
122
- const { execSync } = require('child_process');
111
+ const { execSync } = require("child_process");
123
112
 
124
113
  try {
125
- execSync('opencode --version', { stdio: 'ignore' });
114
+ execSync("opencode --version", { stdio: "ignore" });
126
115
  } catch {
127
116
  // Not installed, nothing to do
128
117
  return;
129
118
  }
130
119
 
131
120
  try {
132
- execSync('npm uninstall -g opencode-ai', { stdio: 'pipe' });
133
- console.error('✓ OpenCode uninstalled');
134
- track('cli.opencode.uninstall.success');
121
+ execSync("npm uninstall -g opencode-ai", { stdio: "pipe" });
122
+ console.error("✓ OpenCode uninstalled");
123
+ track("cli.opencode.uninstall.success");
135
124
  } catch (err) {
136
- console.error('Warning: Failed to uninstall OpenCode:', err.message);
137
- track('cli.opencode.uninstall.failure', { error: err.message });
125
+ console.error("Warning: Failed to uninstall OpenCode:", err.message);
126
+ track("cli.opencode.uninstall.failure", { error: err.message });
138
127
  }
139
128
  }
140
129
 
141
130
  // Main uninstall flow
142
- track('cli.uninstall.start');
131
+ track("cli.uninstall.start");
143
132
 
144
133
  removeClaudeSkills();
145
134
  removeStopHook();
146
135
  removeOpenCode();
136
+ removeLoreArtifacts();
147
137
 
148
- track('cli.uninstall.success');
149
- console.error('✓ Tanagram CLI uninstalled successfully');
138
+ track("cli.uninstall.success");
139
+ console.error("✓ Tanagram CLI uninstalled successfully");