taketomarket 2.3.2 → 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.2",
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.2",
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",
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  **Marketing OS for developerneurs and solopreneurs.** Built for engineers shipping products with zero marketing experience required.
7
7
 
8
- takeToMarket is a Claude Code / Codex skill set that brings spec-driven development to marketing. Every campaign, asset, and channel is a spec-driven unit with a verifiable outcome metric and a positioning-invariant quality gate wall.
8
+ taketomarket is a Claude Code / Codex skill set that brings spec-driven development to marketing. Every campaign, asset, and channel is a spec-driven unit with a verifiable outcome metric and a positioning-invariant quality gate wall.
9
9
 
10
10
  **Core invariant:** Every marketing asset ships with a verifiable outcome metric and passes through a positioning-invariant quality gate wall — no asset ships without both, ever.
11
11
 
@@ -15,19 +15,19 @@ takeToMarket is a Claude Code / Codex skill set that brings spec-driven developm
15
15
  - **Solopreneurs with engineering backgrounds** — founders who code their MVP themselves.
16
16
  - **Indie hackers** — anyone shipping a product who has zero or near-zero marketing/growth experience.
17
17
 
18
- If you can write code but have never built a landing page, written a positioning statement, or thought about ICPs — takeToMarket gives you the operating system. The AI does the marketing work; you stay in control of the decisions.
18
+ If you can write code but have never built a landing page, written a positioning statement, or thought about ICPs — taketomarket gives you the operating system. The AI does the marketing work; you stay in control of the decisions.
19
19
 
20
20
  ## Who this is NOT for
21
21
 
22
- - Full-time marketers who already have a stack — takeToMarket overlaps with what you already do.
22
+ - Full-time marketers who already have a stack — taketomarket overlaps with what you already do.
23
23
  - Agencies serving multiple clients — built for one product per workspace.
24
- - Anyone wanting a one-click blog generator — takeToMarket is opinionated, slower, and quality-gated by design.
24
+ - Anyone wanting a one-click blog generator — taketomarket is opinionated, slower, and quality-gated by design.
25
25
 
26
26
  ## What it is / What it isn't
27
27
 
28
28
  **IS:** A marketing OS that treats every campaign, asset, and channel as a spec-driven unit with a verifiable outcome, a positioning invariant, and a quality gate wall. Persistent state. Compound learnings. Nine-phase lifecycle enforcement.
29
29
 
30
- **IS NOT:** A content generator, one-click blog writer, or social media scheduler. Not a replacement for marketers — it is the operating system a marketer uses to ship more, drift less, and compound learnings. Not a reporting dashboard. Not a scheduler. takeToMarket enforces discipline; it does not generate random content.
30
+ **IS NOT:** A content generator, one-click blog writer, or social media scheduler. Not a replacement for marketers — it is the operating system a marketer uses to ship more, drift less, and compound learnings. Not a reporting dashboard. Not a scheduler. taketomarket enforces discipline; it does not generate random content.
31
31
 
32
32
  ## Requirements
33
33
 
@@ -50,28 +50,29 @@ Flags:
50
50
  - `--check` — show install status without installing
51
51
  - `--runtime <claude|codex>` — skip interactive prompt, install to one runtime
52
52
 
53
- ### Option 2 — Claude Code plugin marketplace
53
+ ### Option 2 — Claude Code plugin marketplace (community)
54
54
 
55
55
  ```
56
- /plugin install taketomarket@claude-plugins-official
56
+ /plugin marketplace add anthropics/claude-plugins-community
57
+ /plugin install taketomarket@claude-community
57
58
  ```
58
59
 
59
- > Status: pending marketplace approval. Check https://github.com/ranjanrishikesh/taketomarket for current status.
60
+ Published to the community marketplace on 2026-05-14. The community catalog syncs nightly, so newly approved updates may take up to 24h to appear. If install fails, confirm `taketomarket` is listed in the [community catalog](https://github.com/anthropics/claude-plugins-community/blob/main/.claude-plugin/marketplace.json).
60
61
 
61
62
  ### Option 3 — Direct from GitHub (Claude Code)
62
63
 
63
64
  ```
64
65
  /plugin marketplace add ranjanrishikesh/taketomarket
65
- /plugin install taketomarket
66
+ /plugin install taketomarket@taketomarket
66
67
  ```
67
68
 
68
- This uses the Claude Code plugin system to install directly from the GitHub repo. Run both commands in Claude Code's chat.
69
+ Installs the latest commit on `main` directly from this repo. Useful for trying unreleased fixes before they reach the community catalog. The `@taketomarket` suffix matches the marketplace name declared in `.claude-plugin/marketplace.json`.
69
70
 
70
71
  ### Option 4 — Manual (advanced)
71
72
 
72
73
  ```bash
73
74
  git clone https://github.com/ranjanrishikesh/taketomarket
74
- cd takeToMarket
75
+ cd taketomarket
75
76
  node install.js
76
77
  ```
77
78
 
@@ -83,33 +84,40 @@ node install.js
83
84
  /ttm-produce # run production wave
84
85
  ```
85
86
 
87
+ > **Plugin install users (Options 2 + 3):** commands are namespaced — use `/taketomarket:ttm-init`, `/taketomarket:ttm-new-campaign`, etc. The bare `/ttm-*` form only works when installed via `npx taketomarket` (Option 1) or manually (Option 4) into the standalone skills directory.
88
+
86
89
  ## Runtime Notes
87
90
 
88
- Commands vary by tool:
91
+ Commands vary by tool and install path:
89
92
 
90
- | Runtime | Install path | Invocation |
91
- |---------|-------------|------------|
92
- | Claude Code | `~/.claude/skills/` | `/ttm-init` |
93
- | Codex | `~/.codex/skills/` or `~/.agents/skills/` | `$ttm-init` or mention by name |
94
- | Cursor | `~/.cursor/skills/` | `/ttm-init` |
95
- | Windsurf | `~/.codeium/windsurf/skills/` | `@ttm-init` |
96
- | Gemini CLI | `~/.gemini/skills/` | automatic or `/skills enable` |
93
+ | Runtime | Install path | Invocation (standalone via npx) | Invocation (plugin install) |
94
+ |---------|--------------|---------------------------------|----------------------------|
95
+ | Claude Code | `~/.claude/skills/` | `/ttm-init` | `/taketomarket:ttm-init` |
96
+ | Codex | `~/.codex/skills/` or `~/.agents/skills/` | `$ttm-init` or mention by name | n/a (plugin install is Claude Code only) |
97
+ | Cursor | `~/.cursor/skills/` | `/ttm-init` | n/a |
98
+ | Windsurf | `~/.codeium/windsurf/skills/` | `@ttm-init` | n/a |
99
+ | Gemini CLI | `~/.gemini/skills/` | automatic or `/skills enable` | n/a |
97
100
 
98
101
  All non-Claude runtimes also support `~/.agents/skills/` as a universal path. Select **option 6** during install to use it.
99
102
 
100
103
  ## Campaign Lifecycle
101
104
 
102
- 1. **Init** set up workspace and reference files
103
- 2. **New Campaign** — create campaign directory with initialized state
104
- 3. **Research** — discover market, audience, and ambient narrative
105
- 4. **Brief** — generate brief with mandatory outcome metrics
106
- 5. **Produce** — generate assets in isolated contexts with full reference loading
107
- 6. **Review** — human quality evaluation with structured checklist
108
- 7. **Fix** — root cause analysis, re-produce, re-verify (capped 3×)
109
- 8. **Verify** — quality gate wall check across all assets
110
- 9. **Ship** — launch checklist confirming tracking, UTMs, funnel testing
111
- 10. **Measure** — analytics vs outcome metrics with attribution models
112
- 11. **Learn** — extract lessons, propose reference file edits, log to LEARNINGS.md
105
+ ### Setup (one-time per workspace + per campaign)
106
+
107
+ - **Init** (`/ttm-init`) set up workspace and reference files
108
+ - **New Campaign** (`/ttm-new-campaign`) create campaign directory with initialized state
109
+
110
+ ### 9-Phase Lifecycle
111
+
112
+ 1. **Discover** — research market, audience, and ambient narrative
113
+ 2. **Brief** — generate brief with mandatory outcome metrics
114
+ 3. **Produce** — generate assets in isolated contexts with full reference loading
115
+ 4. **Review** — human quality evaluation with structured checklist
116
+ 5. **Fix** — root cause analysis, re-produce, re-verify (capped 3×)
117
+ 6. **Verify** — quality gate wall check across all assets
118
+ 7. **Ship** — launch checklist confirming tracking, UTMs, funnel testing
119
+ 8. **Measure** — analytics vs outcome metrics with attribution models
120
+ 9. **Learn** — extract lessons, propose reference file edits, log to LEARNINGS.md
113
121
 
114
122
  ## Command Reference
115
123
 
@@ -164,7 +172,7 @@ MIT — see [LICENSE](LICENSE).
164
172
 
165
173
  ## Privacy & Security
166
174
 
167
- takeToMarket runs entirely on your local filesystem. No data collection, no telemetry, no servers. See [SECURITY.md](SECURITY.md) for the combined privacy and security policy.
175
+ taketomarket runs entirely on your local filesystem. No data collection, no telemetry, no servers. See [SECURITY.md](SECURITY.md) for the combined privacy and security policy.
168
176
 
169
177
  ## Contributing
170
178
 
@@ -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.2",
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
  ---