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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +39 -31
- package/bin/check-update.cjs +313 -0
- package/hooks/hooks.json +14 -0
- package/install.js +104 -2
- package/package.json +2 -1
- package/skills/ttm-aeo-check/SKILL.md +1 -1
- package/skills/ttm-email-preflight/SKILL.md +1 -1
- package/skills/ttm-keyword-map/SKILL.md +1 -1
- package/skills/ttm-research/SKILL.md +2 -2
- package/skills/ttm-seo-audit/SKILL.md +1 -1
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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 —
|
|
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 —
|
|
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 —
|
|
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.
|
|
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
|
|
56
|
+
/plugin marketplace add anthropics/claude-plugins-community
|
|
57
|
+
/plugin install taketomarket@claude-community
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/hooks/hooks.json
ADDED
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(
|
|
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
|
+
"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,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ttm-research
|
|
3
3
|
description: >
|
|
4
|
-
[DEPRECATED v2.3.0 -> removed
|
|
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
|
|
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
|
|