taketomarket 2.3.3 → 2.4.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.
@@ -9,7 +9,7 @@
9
9
  "name": "taketomarket",
10
10
  "source": "./",
11
11
  "description": "Marketing OS for developerneurs + solopreneurs — engineers shipping products with zero marketing experience. Spec-driven campaigns with positioning-invariant quality gates.",
12
- "version": "2.3.3",
12
+ "version": "2.4.0",
13
13
  "homepage": "https://www.npmjs.com/package/taketomarket",
14
14
  "license": "MIT",
15
15
  "keywords": [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "taketomarket",
3
3
  "displayName": "taketomarket",
4
- "version": "2.3.3",
4
+ "version": "2.4.0",
5
5
  "description": "Marketing OS for developerneurs and solopreneurs. Built for engineers shipping products with zero marketing experience required. Spec-driven campaigns with positioning-as-invariant enforcement and quality gate walls.",
6
6
  "author": {
7
7
  "name": "Rishikesh Ranjan",
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ /**
6
+ * check-update.cjs -- SessionStart hook: nudge when takeToMarket is outdated.
7
+ *
8
+ * Fires at session start via two install paths:
9
+ * - plugin install : hooks/hooks.json runs `node "${CLAUDE_PLUGIN_ROOT}/bin/check-update.cjs"`
10
+ * - npm / clone : install.js injects a SessionStart hook into ~/.claude/settings.json
11
+ * pointing at ~/.taketomarket/bin/check-update.cjs
12
+ *
13
+ * Behaviour: read the installed version, compare against the npm registry
14
+ * (throttled to once per CHECK_INTERVAL via an on-disk cache), and -- if a newer
15
+ * version exists -- print a SessionStart `additionalContext` block instructing
16
+ * Claude to surface the update and offer to run /ttm-update.
17
+ *
18
+ * HARD CONTRACT: never break the session. Any failure -> exit 0 with no output.
19
+ * Zero npm dependencies; Node built-ins only.
20
+ */
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const os = require('os');
25
+ const { execFileSync } = require('child_process');
26
+
27
+ const PACKAGE = 'taketomarket';
28
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h throttle between npm registry hits
29
+ const NPM_TIMEOUT_MS = 3000; // fail fast so session start is never blocked
30
+ const NUDGE_COOLDOWN_MS = 30 * 1000; // suppress a duplicate nudge from back-to-back hook fires
31
+
32
+ // ── Pure helpers (unit-tested) ─────────────────────────────────────────────────
33
+
34
+ /**
35
+ * Compare two prerelease identifier strings per semver §11.
36
+ * Numeric identifiers compare numerically and rank below alphanumeric ones;
37
+ * a larger set of identifiers outranks a smaller prefix-equal set.
38
+ * @param {string} a - dot-separated prerelease string (non-empty)
39
+ * @param {string} b - dot-separated prerelease string (non-empty)
40
+ * @returns {number} 1 if a>b, -1 if a<b, 0 if equal
41
+ */
42
+ function comparePrerelease(a, b) {
43
+ const ai = a.split('.');
44
+ const bi = b.split('.');
45
+ const len = Math.max(ai.length, bi.length);
46
+ for (let i = 0; i < len; i++) {
47
+ if (ai[i] === undefined) return -1; // a is a prefix of b -> a is lower
48
+ if (bi[i] === undefined) return 1;
49
+ const aNum = /^\d+$/.test(ai[i]);
50
+ const bNum = /^\d+$/.test(bi[i]);
51
+ if (aNum && bNum) {
52
+ const d = parseInt(ai[i], 10) - parseInt(bi[i], 10);
53
+ if (d !== 0) return d > 0 ? 1 : -1;
54
+ } else if (aNum !== bNum) {
55
+ return aNum ? -1 : 1; // numeric identifiers rank below alphanumeric
56
+ } else if (ai[i] !== bi[i]) {
57
+ return ai[i] > bi[i] ? 1 : -1;
58
+ }
59
+ }
60
+ return 0;
61
+ }
62
+
63
+ /**
64
+ * Compare two semver strings with proper prerelease precedence (semver §11).
65
+ * Numeric major.minor.patch compare first; when those are equal, a version
66
+ * WITHOUT a prerelease tag outranks one WITH a tag (e.g. 2.3.0 > 2.3.0-rc.1),
67
+ * and two prerelease tags compare by identifier.
68
+ * @param {string} a
69
+ * @param {string} b
70
+ * @returns {number} 1 if a>b, -1 if a<b, 0 if equal
71
+ */
72
+ function compareSemver(a, b) {
73
+ const parse = (v) => {
74
+ const s = String(v).trim().replace(/^v/, '');
75
+ const dash = s.indexOf('-');
76
+ const core = dash === -1 ? s : s.slice(0, dash);
77
+ const pre = dash === -1 ? '' : s.slice(dash + 1);
78
+ const parts = core.split('.');
79
+ return { nums: [0, 1, 2].map((i) => parseInt(parts[i], 10) || 0), pre };
80
+ };
81
+ const pa = parse(a);
82
+ const pb = parse(b);
83
+ for (let i = 0; i < 3; i++) {
84
+ if (pa.nums[i] > pb.nums[i]) return 1;
85
+ if (pa.nums[i] < pb.nums[i]) return -1;
86
+ }
87
+ if (pa.pre === pb.pre) return 0;
88
+ if (pa.pre === '') return 1; // no prerelease outranks a prerelease of same core
89
+ if (pb.pre === '') return -1;
90
+ return comparePrerelease(pa.pre, pb.pre);
91
+ }
92
+
93
+ /**
94
+ * @param {string} installed
95
+ * @param {string} latest
96
+ * @returns {boolean} true when `latest` is strictly newer than `installed`
97
+ */
98
+ function isOutdated(installed, latest) {
99
+ if (!installed || !latest) return false;
100
+ return compareSemver(latest, installed) === 1;
101
+ }
102
+
103
+ /**
104
+ * @param {?{checked_at:number}} cache
105
+ * @param {number} now - epoch ms
106
+ * @param {number} intervalMs
107
+ * @returns {boolean} true when the cache exists and is younger than intervalMs
108
+ */
109
+ function isCacheFresh(cache, now, intervalMs) {
110
+ if (!cache || typeof cache.checked_at !== 'number') return false;
111
+ const age = now - cache.checked_at;
112
+ // Negative age = future timestamp (clock moved backward / imported cache):
113
+ // treat as stale so it forces a re-check rather than pinning a stale latest.
114
+ return age >= 0 && age < intervalMs;
115
+ }
116
+
117
+ /**
118
+ * Build the SessionStart context string injected into Claude's context.
119
+ * Deliberately directive: this is the "strong inject-context" nudge.
120
+ * @param {string} installed
121
+ * @param {string} latest
122
+ * @returns {string}
123
+ */
124
+ function buildContext(installed, latest) {
125
+ return [
126
+ `[takeToMarket] Your installed version is OUT OF DATE: v${installed} installed, v${latest} is the latest on npm.`,
127
+ `ACTION: Proactively tell the user their takeToMarket install is outdated (v${installed} -> v${latest})`,
128
+ `and offer to upgrade now. If they agree, invoke the /ttm-update skill -- it detects the install`,
129
+ `method (plugin/npm/clone) and performs the upgrade safely, preserving local edits.`,
130
+ `Do NOT attempt the upgrade by any other means (no manual npm/git commands).`,
131
+ ].join(' ');
132
+ }
133
+
134
+ /**
135
+ * Decide whether update checking is disabled.
136
+ * @param {NodeJS.ProcessEnv} env
137
+ * @returns {boolean}
138
+ */
139
+ function isOptedOut(env) {
140
+ const v = env && env.TTM_NO_UPDATE_CHECK;
141
+ return v === '1' || v === 'true';
142
+ }
143
+
144
+ /**
145
+ * Read the `version` from a package.json, but only if it is the taketomarket package.
146
+ * @param {string} filePath
147
+ * @returns {?string} version string, or null
148
+ */
149
+ function readVersionFromPackageJson(filePath) {
150
+ try {
151
+ const pkg = JSON.parse(fs.readFileSync(filePath, 'utf8'));
152
+ if (pkg && pkg.name === PACKAGE && typeof pkg.version === 'string') return pkg.version;
153
+ } catch (_) {
154
+ // unreadable / not JSON / wrong package -> null
155
+ }
156
+ return null;
157
+ }
158
+
159
+ /**
160
+ * Resolve the installed version. Prefers the package.json adjacent to this
161
+ * script (works for BOTH the plugin-cache layout, where bin/ sits beside
162
+ * package.json at the plugin root, AND the ~/.taketomarket/bin layout written
163
+ * by install.js). Falls back to ~/.taketomarket/package.json.
164
+ * @param {string} scriptDir - directory of this script (__dirname)
165
+ * @param {string} homeDir
166
+ * @returns {?string}
167
+ */
168
+ function resolveInstalledVersion(scriptDir, homeDir) {
169
+ const candidates = [
170
+ path.join(scriptDir, '..', 'package.json'),
171
+ path.join(homeDir, '.taketomarket', 'package.json'),
172
+ ];
173
+ for (const p of candidates) {
174
+ const v = readVersionFromPackageJson(p);
175
+ if (v) return v;
176
+ }
177
+ return null;
178
+ }
179
+
180
+ // ── I/O helpers ─────────────────────────────────────────────────────────────--
181
+
182
+ function readCache(cachePath) {
183
+ try {
184
+ const c = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
185
+ if (c && typeof c.checked_at === 'number') return c;
186
+ } catch (_) {
187
+ // missing / corrupt -> treat as no cache
188
+ }
189
+ return null;
190
+ }
191
+
192
+ function writeCache(cachePath, data) {
193
+ try {
194
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
195
+ fs.writeFileSync(cachePath, JSON.stringify(data, null, 2) + '\n');
196
+ } catch (_) {
197
+ // cache is best-effort; never throw
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Query the npm registry for the latest published version. Short timeout,
203
+ * fail-silent (returns null on any error / offline / timeout).
204
+ * @returns {?string}
205
+ */
206
+ function fetchLatestVersion() {
207
+ try {
208
+ // On Windows the npm launcher is npm.cmd, which execFile cannot run without
209
+ // a shell -> spawn via the shell there. PACKAGE is a fixed constant (no user
210
+ // input interpolated), so shell use carries no injection risk.
211
+ const out = execFileSync('npm', ['show', PACKAGE, 'version'], {
212
+ timeout: NPM_TIMEOUT_MS,
213
+ stdio: ['ignore', 'pipe', 'ignore'],
214
+ encoding: 'utf8',
215
+ shell: process.platform === 'win32',
216
+ });
217
+ const v = String(out).trim();
218
+ return /^\d+\.\d+\.\d+/.test(v) ? v : null;
219
+ } catch (_) {
220
+ return null;
221
+ }
222
+ }
223
+
224
+ // ── Orchestrator ──────────────────────────────────────────────────────────────
225
+
226
+ /**
227
+ * Run the update check. All side-effecting dependencies are injectable for tests.
228
+ * @param {object} [deps]
229
+ * @returns {{action:string, installed?:string, latest?:string}} summary (for tests)
230
+ */
231
+ function run(deps = {}) {
232
+ const {
233
+ now = Date.now(),
234
+ homeDir = os.homedir(),
235
+ scriptDir = __dirname,
236
+ env = process.env,
237
+ fetchLatest = fetchLatestVersion,
238
+ out = (s) => process.stdout.write(s),
239
+ } = deps;
240
+
241
+ if (isOptedOut(env)) return { action: 'opted-out' };
242
+
243
+ const installed = resolveInstalledVersion(scriptDir, homeDir);
244
+ if (!installed) return { action: 'no-version' };
245
+
246
+ const cachePath = path.join(homeDir, '.taketomarket', '.update-check.json');
247
+ const cache = readCache(cachePath);
248
+
249
+ let latest = cache ? cache.latest : null;
250
+ if (!isCacheFresh(cache, now, CHECK_INTERVAL_MS)) {
251
+ const fetched = fetchLatest();
252
+ if (fetched) {
253
+ latest = fetched;
254
+ writeCache(cachePath, { checked_at: now, latest, installed });
255
+ }
256
+ // else: offline/timeout -> keep stale `latest` if we had one, otherwise null
257
+ }
258
+
259
+ if (!latest) return { action: 'no-latest' };
260
+
261
+ if (isOutdated(installed, latest)) {
262
+ // Double-fire guard: a user with BOTH a plugin install (hooks/hooks.json)
263
+ // and an npm/clone install (settings.json) runs this script twice per
264
+ // SessionStart. The two fires happen back-to-back, so suppress a second
265
+ // emission within a short cooldown to avoid a duplicated nudge. (The
266
+ // installer's idempotency is settings.json-scoped and cannot see the
267
+ // plugin-discovered hooks.json, so the dedupe must live here.)
268
+ const nudgedAt = cache && typeof cache.nudged_at === 'number' ? cache.nudged_at : null;
269
+ if (nudgedAt !== null && (now - nudgedAt) >= 0 && (now - nudgedAt) < NUDGE_COOLDOWN_MS) {
270
+ return { action: 'nudge-suppressed', installed, latest };
271
+ }
272
+ out(JSON.stringify({
273
+ hookSpecificOutput: {
274
+ hookEventName: 'SessionStart',
275
+ additionalContext: buildContext(installed, latest),
276
+ },
277
+ }) + '\n');
278
+ writeCache(cachePath, {
279
+ checked_at: cache && typeof cache.checked_at === 'number' ? cache.checked_at : now,
280
+ latest,
281
+ installed,
282
+ nudged_at: now,
283
+ });
284
+ return { action: 'nudged', installed, latest };
285
+ }
286
+
287
+ return { action: 'up-to-date', installed, latest };
288
+ }
289
+
290
+ module.exports = {
291
+ compareSemver,
292
+ comparePrerelease,
293
+ isOutdated,
294
+ isCacheFresh,
295
+ buildContext,
296
+ isOptedOut,
297
+ readVersionFromPackageJson,
298
+ resolveInstalledVersion,
299
+ readCache,
300
+ writeCache,
301
+ fetchLatestVersion,
302
+ run,
303
+ CHECK_INTERVAL_MS,
304
+ };
305
+
306
+ if (require.main === module) {
307
+ try {
308
+ run();
309
+ } catch (_) {
310
+ // HARD CONTRACT: never break the session.
311
+ }
312
+ process.exit(0);
313
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/bin/check-update.cjs\""
9
+ }
10
+ ]
11
+ }
12
+ ]
13
+ }
14
+ }
package/install.js CHANGED
@@ -21,6 +21,7 @@ const DIRS_TO_COPY = [
21
21
  'gates',
22
22
  'bin',
23
23
  'agents',
24
+ 'hooks',
24
25
  ];
25
26
 
26
27
  const FILES_TO_COPY = [
@@ -180,7 +181,7 @@ async function promptRuntimeSelection(args, homeDir = os.homedir()) {
180
181
  const result = [];
181
182
  for (const name of choices) {
182
183
  if (name === 'custom') {
183
- result.push({ label: 'Custom', skillsDir: customPath, parentDir: null });
184
+ result.push(buildCustomTarget(customPath, homeDir));
184
185
  } else {
185
186
  result.push(allTargets[name]);
186
187
  }
@@ -188,6 +189,21 @@ async function promptRuntimeSelection(args, homeDir = os.homedir()) {
188
189
  return result;
189
190
  }
190
191
 
192
+ /**
193
+ * Build an install target for a user-typed custom path. Expands a leading `~`
194
+ * to the home dir (so a literal `~/.claude/skills` does not create a stray `~`
195
+ * directory) and derives parentDir from the path so the Claude-target gate in
196
+ * main() can recognise a custom path that lands under ~/.claude and still wire
197
+ * the SessionStart update-check hook.
198
+ * @param {string} customPath
199
+ * @param {string} [homeDir]
200
+ * @returns {{label: string, skillsDir: string, parentDir: string}}
201
+ */
202
+ function buildCustomTarget(customPath, homeDir = os.homedir()) {
203
+ const expanded = customPath.replace(/^~(?=$|[/\\])/, homeDir);
204
+ return { label: 'Custom', skillsDir: expanded, parentDir: path.dirname(expanded) };
205
+ }
206
+
191
207
  // ── Runtime detection ────────────────────────────────────────────────────────
192
208
 
193
209
  /**
@@ -279,7 +295,7 @@ function copyDirSync(src, dest) {
279
295
 
280
296
  // ── Package Base & Per-Runtime Skill Install ──────────────────────────────────
281
297
 
282
- const PACKAGE_BASE_DIRS = ['workflows', 'templates', 'references', 'playbooks', 'gates', 'bin', 'agents'];
298
+ const PACKAGE_BASE_DIRS = ['workflows', 'templates', 'references', 'playbooks', 'gates', 'bin', 'agents', 'hooks'];
283
299
  const PACKAGE_BASE_FILES = ['settings.json', 'package.json'];
284
300
 
285
301
  /**
@@ -418,6 +434,77 @@ function registerPlugin(installPath, version, homeDir = os.homedir()) {
418
434
  console.log(' Registered in installed_plugins.json');
419
435
  }
420
436
 
437
+ // ── Update-check Hook Injection (Claude Code only) ─────────────────────────────
438
+
439
+ const CHECK_UPDATE_SCRIPT_REL = path.join('.taketomarket', 'bin', 'check-update.cjs');
440
+ const CHECK_UPDATE_MARKER = 'check-update.cjs';
441
+
442
+ /**
443
+ * Build the shell command Claude Code runs at SessionStart. Uses the absolute
444
+ * resolved path (no shell-expansion assumptions) into the shared package base.
445
+ * @param {string} homeDir
446
+ * @returns {string}
447
+ */
448
+ function buildCheckUpdateCommand(homeDir) {
449
+ return `node "${path.join(homeDir, CHECK_UPDATE_SCRIPT_REL)}"`;
450
+ }
451
+
452
+ /**
453
+ * Inject the takeToMarket update-check hook into ~/.claude/settings.json as a
454
+ * SessionStart command hook. This is the npm/clone-path equivalent of the
455
+ * plugin-path hooks/hooks.json (plugin installs auto-discover that file; npm and
456
+ * git-clone installs do not, so we register the hook in the user's settings).
457
+ *
458
+ * Idempotent (skips if any SessionStart command already references
459
+ * check-update.cjs), atomic (tmp -> rename), and preserves all existing hooks
460
+ * and settings. Claude Code only.
461
+ *
462
+ * @param {string} [homeDir]
463
+ * @returns {boolean} true if the hook was newly added, false if already present
464
+ */
465
+ function injectSessionStartHook(homeDir = os.homedir()) {
466
+ const settingsPath = path.join(homeDir, '.claude', 'settings.json');
467
+ const settingsDir = path.dirname(settingsPath);
468
+ const command = buildCheckUpdateCommand(homeDir);
469
+
470
+ let settings = {};
471
+ if (fileExists(settingsPath)) {
472
+ try {
473
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
474
+ if (!settings || typeof settings !== 'object') settings = {};
475
+ } catch {
476
+ fs.renameSync(settingsPath, settingsPath + '.bak');
477
+ console.warn(' Warning: ~/.claude/settings.json was corrupted. Backed up to .bak and recreated.');
478
+ settings = {};
479
+ }
480
+ }
481
+
482
+ if (!settings.hooks || typeof settings.hooks !== 'object') settings.hooks = {};
483
+ if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
484
+
485
+ const already = settings.hooks.SessionStart.some(group =>
486
+ group && Array.isArray(group.hooks) && group.hooks.some(h =>
487
+ h && typeof h.command === 'string' && h.command.includes(CHECK_UPDATE_MARKER)
488
+ )
489
+ );
490
+ if (already) {
491
+ console.log(' Update-check hook already present in ~/.claude/settings.json');
492
+ return false;
493
+ }
494
+
495
+ settings.hooks.SessionStart.push({
496
+ hooks: [{ type: 'command', command }],
497
+ });
498
+
499
+ const tmpPath = settingsPath + '.tmp';
500
+ fs.mkdirSync(settingsDir, { recursive: true });
501
+ fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
502
+ fs.renameSync(tmpPath, settingsPath);
503
+
504
+ console.log(' Installed update-check hook -> ~/.claude/settings.json (SessionStart)');
505
+ return true;
506
+ }
507
+
421
508
  // ── Skill Introspection ───────────────────────────────────────────────────────
422
509
 
423
510
  /**
@@ -723,6 +810,18 @@ Options:
723
810
  }
724
811
  }
725
812
 
813
+ // Register the SessionStart update-check hook (Claude Code only).
814
+ const claudeParent = path.join(os.homedir(), '.claude');
815
+ const claudeTargeted = targets.some(t => t.parentDir === claudeParent);
816
+ if (claudeTargeted) {
817
+ console.log('');
818
+ try {
819
+ injectSessionStartHook();
820
+ } catch (err) {
821
+ console.warn(` Warning: could not install update-check hook: ${err.message}`);
822
+ }
823
+ }
824
+
726
825
  // Summary
727
826
  const successes = results.filter(r => r.success);
728
827
  const failures = results.filter(r => !r.success);
@@ -789,5 +888,8 @@ module.exports = {
789
888
  installSkillsForRuntime,
790
889
  classifyInstallMethod,
791
890
  writeInstallSentinel,
891
+ injectSessionStartHook,
892
+ buildCheckUpdateCommand,
893
+ buildCustomTarget,
792
894
  PACKAGE_ROOT,
793
895
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taketomarket",
3
- "version": "2.3.3",
3
+ "version": "2.4.0",
4
4
  "description": "Marketing OS for developerneurs and solopreneurs. Built for engineers shipping products with zero marketing experience required. Spec-driven campaigns with positioning-as-invariant enforcement and quality gate walls.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,6 +28,7 @@
28
28
  "gates/",
29
29
  "bin/",
30
30
  "agents/",
31
+ "hooks/",
31
32
  "settings.json",
32
33
  "install.js",
33
34
  "LICENSE",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: ttm-aeo-check
3
3
  description: >
4
- [DEPRECATED v2.3.0 -> removed v2.4.0] Merged into /ttm-seo aeo.
4
+ [DEPRECATED v2.3.0 -> removed v3.0.0] Merged into /ttm-seo aeo.
5
5
  disable-model-invocation: true
6
6
  allowed-tools: Read
7
7
  ---
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: ttm-email-preflight
3
3
  description: >
4
- [DEPRECATED v2.3.0 -> removed v2.4.0] Renamed to /ttm-email-check.
4
+ [DEPRECATED v2.3.0 -> removed v3.0.0] Renamed to /ttm-email-check.
5
5
  disable-model-invocation: true
6
6
  allowed-tools: Read
7
7
  ---
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: ttm-keyword-map
3
3
  description: >
4
- [DEPRECATED v2.3.0 -> removed v2.4.0] Merged into /ttm-seo keyword-map.
4
+ [DEPRECATED v2.3.0 -> removed v3.0.0] Merged into /ttm-seo keyword-map.
5
5
  disable-model-invocation: true
6
6
  allowed-tools: Read
7
7
  ---
@@ -1,14 +1,14 @@
1
1
  ---
2
2
  name: ttm-research
3
3
  description: >
4
- [DEPRECATED v2.3.0 -> removed v2.4.0] Renamed to /ttm-discover.
4
+ [DEPRECATED v2.3.0 -> removed v3.0.0] Renamed to /ttm-discover.
5
5
  disable-model-invocation: true
6
6
  allowed-tools: Read
7
7
  ---
8
8
 
9
9
  # /ttm-research (DEPRECATED)
10
10
 
11
- This skill was renamed to `/ttm-discover` in v2.3.0 and will be removed in v2.4.0.
11
+ This skill was renamed to `/ttm-discover` in v2.3.0 and will be removed in v3.0.0.
12
12
 
13
13
  Print to user:
14
14
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: ttm-seo-audit
3
3
  description: >
4
- [DEPRECATED v2.3.0 -> removed v2.4.0] Merged into /ttm-seo audit.
4
+ [DEPRECATED v2.3.0 -> removed v3.0.0] Merged into /ttm-seo audit.
5
5
  disable-model-invocation: true
6
6
  allowed-tools: Read
7
7
  ---