taketomarket 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +419 -0
  4. package/agents/ttm-producer.md +53 -0
  5. package/bin/lib/campaign.cjs +553 -0
  6. package/bin/lib/commit.cjs +105 -0
  7. package/bin/lib/core.cjs +172 -0
  8. package/bin/lib/deviation.cjs +149 -0
  9. package/bin/lib/drift-log.cjs +219 -0
  10. package/bin/lib/health.cjs +438 -0
  11. package/bin/lib/slug.cjs +59 -0
  12. package/bin/lib/state.cjs +96 -0
  13. package/bin/ttm-tools.cjs +157 -0
  14. package/gates/base-gates.md +266 -0
  15. package/gates/discipline/.gitkeep +0 -0
  16. package/gates/gate-evaluation.md +341 -0
  17. package/gates/meta-gates.md +19 -0
  18. package/install.js +307 -0
  19. package/package.json +53 -0
  20. package/playbooks/.gitkeep +0 -0
  21. package/playbooks/aeo.md +223 -0
  22. package/playbooks/affiliate.md +272 -0
  23. package/playbooks/base.md +110 -0
  24. package/playbooks/email.md +306 -0
  25. package/playbooks/events.md +320 -0
  26. package/playbooks/linkedin.md +263 -0
  27. package/playbooks/paid-ads.md +318 -0
  28. package/playbooks/pr-media.md +296 -0
  29. package/playbooks/seo.md +284 -0
  30. package/playbooks/social.md +305 -0
  31. package/playbooks/youtube.md +325 -0
  32. package/references/context-loading.md +107 -0
  33. package/references/learnings-extraction.md +94 -0
  34. package/references/measurement-template.md +48 -0
  35. package/references/meta-gate-evaluation.md +169 -0
  36. package/references/positioning-check-report.md +197 -0
  37. package/references/review-checklist.md +78 -0
  38. package/references/ship-checklist-items.md +94 -0
  39. package/settings.json +4 -0
  40. package/skills/ttm-aeo-check/SKILL.md +20 -0
  41. package/skills/ttm-affiliate-kit/SKILL.md +19 -0
  42. package/skills/ttm-archive/SKILL.md +13 -0
  43. package/skills/ttm-brand-refresh/SKILL.md +19 -0
  44. package/skills/ttm-brief/SKILL.md +14 -0
  45. package/skills/ttm-competitor-scan/SKILL.md +19 -0
  46. package/skills/ttm-email-preflight/SKILL.md +19 -0
  47. package/skills/ttm-fix/SKILL.md +13 -0
  48. package/skills/ttm-health/SKILL.md +12 -0
  49. package/skills/ttm-icp-refresh/SKILL.md +19 -0
  50. package/skills/ttm-init/SKILL.md +12 -0
  51. package/skills/ttm-keyword-map/SKILL.md +19 -0
  52. package/skills/ttm-learn/SKILL.md +14 -0
  53. package/skills/ttm-measure/SKILL.md +14 -0
  54. package/skills/ttm-new-campaign/SKILL.md +13 -0
  55. package/skills/ttm-next/SKILL.md +12 -0
  56. package/skills/ttm-positioning-check/SKILL.md +19 -0
  57. package/skills/ttm-positioning-shift/SKILL.md +19 -0
  58. package/skills/ttm-produce/SKILL.md +14 -0
  59. package/skills/ttm-repurpose/SKILL.md +20 -0
  60. package/skills/ttm-research/SKILL.md +13 -0
  61. package/skills/ttm-resume/SKILL.md +13 -0
  62. package/skills/ttm-review/SKILL.md +13 -0
  63. package/skills/ttm-seo-audit/SKILL.md +20 -0
  64. package/skills/ttm-ship/SKILL.md +13 -0
  65. package/skills/ttm-state/SKILL.md +13 -0
  66. package/skills/ttm-verify/SKILL.md +14 -0
  67. package/templates/agents-md.md +65 -0
  68. package/templates/campaign-brief.md +74 -0
  69. package/templates/campaign-research.md +39 -0
  70. package/templates/campaign-state.md +40 -0
  71. package/templates/claude-md.md +65 -0
  72. package/templates/deviation-log.md +12 -0
  73. package/templates/drift-log.md +17 -0
  74. package/templates/fix-brief.md +59 -0
  75. package/templates/fix-log.md +22 -0
  76. package/templates/measurement-report.md +75 -0
  77. package/templates/migration-plan.md +24 -0
  78. package/templates/production-manifest.json +20 -0
  79. package/templates/reference-files/brand.md +45 -0
  80. package/templates/reference-files/calendar.md +30 -0
  81. package/templates/reference-files/channels.md +40 -0
  82. package/templates/reference-files/competitors.md +40 -0
  83. package/templates/reference-files/icp.md +50 -0
  84. package/templates/reference-files/learnings.md +40 -0
  85. package/templates/reference-files/metrics.md +42 -0
  86. package/templates/reference-files/positioning.md +38 -0
  87. package/templates/reference-files/state.md +27 -0
  88. package/templates/verification-report.md +59 -0
  89. package/workflows/discipline/.gitkeep +0 -0
  90. package/workflows/discipline/aeo-check.md +180 -0
  91. package/workflows/discipline/affiliate-kit.md +147 -0
  92. package/workflows/discipline/email-preflight.md +150 -0
  93. package/workflows/discipline/keyword-map.md +125 -0
  94. package/workflows/discipline/repurpose.md +329 -0
  95. package/workflows/discipline/seo-audit.md +169 -0
  96. package/workflows/lifecycle/.gitkeep +0 -0
  97. package/workflows/lifecycle/brief-positioning-check.md +90 -0
  98. package/workflows/lifecycle/brief.md +355 -0
  99. package/workflows/lifecycle/fix.md +495 -0
  100. package/workflows/lifecycle/learn.md +405 -0
  101. package/workflows/lifecycle/measure.md +379 -0
  102. package/workflows/lifecycle/produce.md +383 -0
  103. package/workflows/lifecycle/research.md +264 -0
  104. package/workflows/lifecycle/review.md +432 -0
  105. package/workflows/lifecycle/ship.md +521 -0
  106. package/workflows/lifecycle/verify.md +507 -0
  107. package/workflows/reference-mgmt/.gitkeep +0 -0
  108. package/workflows/reference-mgmt/brand-refresh.md +193 -0
  109. package/workflows/reference-mgmt/competitor-scan.md +228 -0
  110. package/workflows/reference-mgmt/icp-refresh.md +200 -0
  111. package/workflows/reference-mgmt/positioning-check.md +339 -0
  112. package/workflows/reference-mgmt/positioning-shift.md +368 -0
  113. package/workflows/setup/.gitkeep +0 -0
  114. package/workflows/setup/init-questions.md +225 -0
  115. package/workflows/setup/init-validation.md +155 -0
  116. package/workflows/setup/init.md +449 -0
  117. package/workflows/setup/new-campaign.md +134 -0
  118. package/workflows/utility/.gitkeep +0 -0
  119. package/workflows/utility/archive.md +334 -0
  120. package/workflows/utility/health.md +166 -0
  121. package/workflows/utility/next.md +187 -0
  122. package/workflows/utility/resume.md +249 -0
  123. package/workflows/utility/state.md +207 -0
@@ -0,0 +1,438 @@
1
+ /**
2
+ * Health -- Directory integrity validation for ttm-tools.cjs
3
+ *
4
+ * Zero npm dependencies. Uses only Node.js built-ins: fs, path.
5
+ * Depends on: ./core.cjs for output, error, safeReadFile, parseFrontmatter
6
+ *
7
+ * Exports: cmdHealth, cmdInit
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { output, safeReadFile, parseFrontmatter } = require('./core.cjs');
15
+
16
+ /**
17
+ * Expected reference files in .marketing/ directory.
18
+ */
19
+ const REFERENCE_FILES = [
20
+ 'POSITIONING.md',
21
+ 'BRAND.md',
22
+ 'ICP.md',
23
+ 'CHANNELS.md',
24
+ 'STATE.md',
25
+ 'CALENDAR.md',
26
+ 'COMPETITORS.md',
27
+ 'METRICS.md',
28
+ 'LEARNINGS.md',
29
+ ];
30
+
31
+ /**
32
+ * Valid campaign phases for state consistency checking.
33
+ */
34
+ const VALID_PHASES = new Set([
35
+ 'created', 'researched', 'briefed', 'produced', 'verified',
36
+ 'reviewed', 'fixed', 'shipped', 'measured', 'learned',
37
+ 'archived', 'cancelled',
38
+ ]);
39
+
40
+ /**
41
+ * Check if a path exists and is a directory.
42
+ * @param {string} p - Path to check
43
+ * @returns {boolean}
44
+ */
45
+ function dirExists(p) {
46
+ try {
47
+ return fs.statSync(p).isDirectory();
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Check if a path exists and is a file.
55
+ * @param {string} p - Path to check
56
+ * @returns {boolean}
57
+ */
58
+ function fileExists(p) {
59
+ try {
60
+ return fs.statSync(p).isFile();
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Check campaign STATE.md consistency for all campaigns.
68
+ * @param {string} campaignsDir - Path to CAMPAIGNS/ directory
69
+ * @returns {Array} Array of check objects
70
+ */
71
+ function checkCampaignStateConsistency(campaignsDir) {
72
+ const checks = [];
73
+ let entries;
74
+ try {
75
+ entries = fs.readdirSync(campaignsDir, { withFileTypes: true });
76
+ } catch {
77
+ return checks;
78
+ }
79
+
80
+ for (const entry of entries) {
81
+ if (!entry.isDirectory() || entry.name === 'ARCHIVE') continue;
82
+ const statePath = path.resolve(campaignsDir, entry.name, 'STATE.md');
83
+ const content = safeReadFile(statePath);
84
+ if (content === null) {
85
+ checks.push({
86
+ name: `campaign_state_${entry.name}`,
87
+ status: 'fail',
88
+ path: `.marketing/CAMPAIGNS/${entry.name}/STATE.md`,
89
+ detail: 'STATE.md missing',
90
+ });
91
+ continue;
92
+ }
93
+ const { frontmatter } = parseFrontmatter(content);
94
+ if (!frontmatter.phase || !VALID_PHASES.has(frontmatter.phase)) {
95
+ checks.push({
96
+ name: `campaign_state_${entry.name}`,
97
+ status: 'fail',
98
+ path: `.marketing/CAMPAIGNS/${entry.name}/STATE.md`,
99
+ detail: `invalid phase: ${frontmatter.phase || 'undefined'}`,
100
+ });
101
+ } else {
102
+ checks.push({
103
+ name: `campaign_state_${entry.name}`,
104
+ status: 'pass',
105
+ path: `.marketing/CAMPAIGNS/${entry.name}/STATE.md`,
106
+ });
107
+ }
108
+ }
109
+ return checks;
110
+ }
111
+
112
+ /**
113
+ * Check reference file staleness based on mtime.
114
+ * @param {string} marketingDir - Path to .marketing/ directory
115
+ * @param {number} thresholdDays - Days threshold for staleness warning (default 90)
116
+ * @returns {Array} Array of check objects
117
+ */
118
+ function checkReferenceStaleness(marketingDir, thresholdDays) {
119
+ if (!thresholdDays) thresholdDays = 90;
120
+ const checks = [];
121
+ const now = Date.now();
122
+ const thresholdMs = thresholdDays * 24 * 60 * 60 * 1000;
123
+
124
+ for (const file of REFERENCE_FILES) {
125
+ const filePath = path.resolve(marketingDir, file);
126
+ try {
127
+ const stat = fs.statSync(filePath);
128
+ const ageMs = now - stat.mtimeMs;
129
+ const ageDays = Math.floor(ageMs / (24 * 60 * 60 * 1000));
130
+ if (ageMs > thresholdMs) {
131
+ checks.push({
132
+ name: `staleness_${file.toLowerCase().replace('.md', '')}`,
133
+ status: 'warn',
134
+ path: `.marketing/${file}`,
135
+ detail: `not updated in ${ageDays} days`,
136
+ });
137
+ } else {
138
+ checks.push({
139
+ name: `staleness_${file.toLowerCase().replace('.md', '')}`,
140
+ status: 'pass',
141
+ path: `.marketing/${file}`,
142
+ });
143
+ }
144
+ } catch {
145
+ // File doesn't exist -- skip (already covered by basic health check)
146
+ }
147
+ }
148
+ return checks;
149
+ }
150
+
151
+ /**
152
+ * Check campaign velocity -- detect stuck campaigns.
153
+ * @param {string} campaignsDir - Path to CAMPAIGNS/ directory
154
+ * @param {number} thresholdDays - Days threshold for stuck warning (default 14)
155
+ * @returns {Array} Array of check objects
156
+ */
157
+ function checkCampaignVelocity(campaignsDir, thresholdDays) {
158
+ if (!thresholdDays) thresholdDays = 14;
159
+ const checks = [];
160
+ const now = Date.now();
161
+ const thresholdMs = thresholdDays * 24 * 60 * 60 * 1000;
162
+
163
+ let entries;
164
+ try {
165
+ entries = fs.readdirSync(campaignsDir, { withFileTypes: true });
166
+ } catch {
167
+ return checks;
168
+ }
169
+
170
+ for (const entry of entries) {
171
+ if (!entry.isDirectory() || entry.name === 'ARCHIVE') continue;
172
+ const statePath = path.resolve(campaignsDir, entry.name, 'STATE.md');
173
+ const content = safeReadFile(statePath);
174
+ if (content === null) continue;
175
+ const { frontmatter } = parseFrontmatter(content);
176
+ const lastUpdated = frontmatter['last_updated'];
177
+ if (!lastUpdated || lastUpdated === 'null') continue;
178
+ const lastMs = Date.parse(lastUpdated);
179
+ if (isNaN(lastMs)) continue;
180
+ const ageMs = now - lastMs;
181
+ const ageDays = Math.floor(ageMs / (24 * 60 * 60 * 1000));
182
+ if (ageMs > thresholdMs) {
183
+ checks.push({
184
+ name: `velocity_${entry.name}`,
185
+ status: 'warn',
186
+ path: `.marketing/CAMPAIGNS/${entry.name}/STATE.md`,
187
+ detail: `stuck in ${frontmatter.phase} for ${ageDays} days`,
188
+ });
189
+ } else {
190
+ checks.push({
191
+ name: `velocity_${entry.name}`,
192
+ status: 'pass',
193
+ path: `.marketing/CAMPAIGNS/${entry.name}/STATE.md`,
194
+ });
195
+ }
196
+ }
197
+ return checks;
198
+ }
199
+
200
+ /**
201
+ * Check DRIFT-LOG.md structural integrity.
202
+ * @param {string} marketingDir - Path to .marketing/ directory
203
+ * @returns {Array} Array of check objects
204
+ */
205
+ function checkDriftLogIntegrity(marketingDir) {
206
+ const checks = [];
207
+ const driftLogPath = path.resolve(marketingDir, 'DRIFT-LOG.md');
208
+ const content = safeReadFile(driftLogPath);
209
+
210
+ if (content === null) {
211
+ checks.push({
212
+ name: 'drift_log_integrity',
213
+ status: 'warn',
214
+ path: '.marketing/DRIFT-LOG.md',
215
+ detail: 'no drift log yet',
216
+ });
217
+ return checks;
218
+ }
219
+
220
+ const markerCount = (content.match(/<!-- Audit Trail -->/g) || []).length;
221
+ if (markerCount === 0) {
222
+ checks.push({
223
+ name: 'drift_log_integrity',
224
+ status: 'fail',
225
+ path: '.marketing/DRIFT-LOG.md',
226
+ detail: 'missing audit trail marker',
227
+ });
228
+ } else if (markerCount > 1) {
229
+ checks.push({
230
+ name: 'drift_log_integrity',
231
+ status: 'fail',
232
+ path: '.marketing/DRIFT-LOG.md',
233
+ detail: 'duplicate audit trail markers',
234
+ });
235
+ } else {
236
+ checks.push({
237
+ name: 'drift_log_integrity',
238
+ status: 'pass',
239
+ path: '.marketing/DRIFT-LOG.md',
240
+ });
241
+ }
242
+ return checks;
243
+ }
244
+
245
+ /**
246
+ * Check gate result consistency for campaigns with verification results.
247
+ * @param {string} campaignsDir - Path to CAMPAIGNS/ directory
248
+ * @returns {Array} Array of check objects
249
+ */
250
+ function checkGateConsistency(campaignsDir) {
251
+ const checks = [];
252
+ const validGateValues = new Set(['null', 'pass', 'warn', 'fail', 'fix_needed', 'accepted']);
253
+
254
+ let entries;
255
+ try {
256
+ entries = fs.readdirSync(campaignsDir, { withFileTypes: true });
257
+ } catch {
258
+ return checks;
259
+ }
260
+
261
+ for (const entry of entries) {
262
+ if (!entry.isDirectory() || entry.name === 'ARCHIVE') continue;
263
+ const statePath = path.resolve(campaignsDir, entry.name, 'STATE.md');
264
+ const content = safeReadFile(statePath);
265
+ if (content === null) continue;
266
+ const { frontmatter } = parseFrontmatter(content);
267
+
268
+ const invalidGates = [];
269
+ for (const [key, value] of Object.entries(frontmatter)) {
270
+ if (key.startsWith('gate.') && value && !validGateValues.has(value)) {
271
+ invalidGates.push(`${key}=${value}`);
272
+ }
273
+ }
274
+
275
+ if (invalidGates.length > 0) {
276
+ checks.push({
277
+ name: `gate_consistency_${entry.name}`,
278
+ status: 'fail',
279
+ path: `.marketing/CAMPAIGNS/${entry.name}/STATE.md`,
280
+ detail: `invalid gate values: ${invalidGates.join(', ')}`,
281
+ });
282
+ } else {
283
+ checks.push({
284
+ name: `gate_consistency_${entry.name}`,
285
+ status: 'pass',
286
+ path: `.marketing/CAMPAIGNS/${entry.name}/STATE.md`,
287
+ });
288
+ }
289
+ }
290
+ return checks;
291
+ }
292
+
293
+ /**
294
+ * Validate .marketing/ directory structure.
295
+ *
296
+ * Checks:
297
+ * 1. .marketing/ directory exists
298
+ * 2. .marketing/CAMPAIGNS/ directory exists
299
+ * 3. Each expected reference file exists
300
+ * 4. STATE.md has valid frontmatter (parseable)
301
+ *
302
+ * When full=true, runs extended checks:
303
+ * 5. Campaign state consistency
304
+ * 6. Reference file staleness
305
+ * 7. Campaign velocity
306
+ * 8. DRIFT-LOG integrity
307
+ * 9. Gate result consistency
308
+ *
309
+ * healthy = true when .marketing/ and CAMPAIGNS/ both exist and no 'fail' checks.
310
+ * Reference files use "missing" status (expected before /ttm-init runs).
311
+ *
312
+ * @param {boolean} raw - Whether to output raw summary string
313
+ * @param {boolean} full - Whether to run extended audit checks
314
+ */
315
+ function cmdHealth(raw, full) {
316
+ const marketingDir = path.resolve(process.cwd(), '.marketing');
317
+ const campaignsDir = path.resolve(marketingDir, 'CAMPAIGNS');
318
+ const checks = [];
319
+
320
+ // Check .marketing/ directory
321
+ const marketingExists = dirExists(marketingDir);
322
+ checks.push({
323
+ name: 'marketing_dir',
324
+ status: marketingExists ? 'pass' : 'fail',
325
+ path: '.marketing/',
326
+ });
327
+
328
+ // Check CAMPAIGNS/ directory
329
+ const campaignsExists = dirExists(campaignsDir);
330
+ checks.push({
331
+ name: 'campaigns_dir',
332
+ status: campaignsExists ? 'pass' : 'fail',
333
+ path: '.marketing/CAMPAIGNS/',
334
+ });
335
+
336
+ // Check each reference file
337
+ for (const file of REFERENCE_FILES) {
338
+ const filePath = path.resolve(marketingDir, file);
339
+ const name = file.toLowerCase().replace('.md', '_md');
340
+ if (fileExists(filePath)) {
341
+ // Extra validation for STATE.md -- check frontmatter is parseable
342
+ if (file === 'STATE.md') {
343
+ const content = safeReadFile(filePath);
344
+ const { frontmatter } = parseFrontmatter(content || '');
345
+ const isValid = Object.keys(frontmatter).length > 0;
346
+ checks.push({
347
+ name,
348
+ status: isValid ? 'pass' : 'fail',
349
+ path: `.marketing/${file}`,
350
+ detail: isValid ? undefined : 'frontmatter unparseable',
351
+ });
352
+ } else {
353
+ checks.push({ name, status: 'pass', path: `.marketing/${file}` });
354
+ }
355
+ } else {
356
+ checks.push({ name, status: 'missing', path: `.marketing/${file}` });
357
+ }
358
+ }
359
+
360
+ // Extended checks when --full flag is passed
361
+ if (full && campaignsExists) {
362
+ const stateChecks = checkCampaignStateConsistency(campaignsDir);
363
+ checks.push(...stateChecks);
364
+
365
+ const stalenessChecks = checkReferenceStaleness(marketingDir);
366
+ checks.push(...stalenessChecks);
367
+
368
+ const velocityChecks = checkCampaignVelocity(campaignsDir);
369
+ checks.push(...velocityChecks);
370
+
371
+ const driftChecks = checkDriftLogIntegrity(marketingDir);
372
+ checks.push(...driftChecks);
373
+
374
+ const gateChecks = checkGateConsistency(campaignsDir);
375
+ checks.push(...gateChecks);
376
+ }
377
+
378
+ const passed = checks.filter(c => c.status === 'pass').length;
379
+ const total = checks.length;
380
+ const failures = checks.filter(c => c.status === 'fail').length;
381
+ // healthy = marketing dir + campaigns dir both exist and no failures
382
+ const healthy = marketingExists && campaignsExists && failures === 0;
383
+
384
+ const result = {
385
+ healthy,
386
+ checks,
387
+ summary: `${passed}/${total} checks passed`,
388
+ };
389
+
390
+ if (raw) {
391
+ const label = healthy ? 'HEALTHY' : 'UNHEALTHY';
392
+ const issues = failures;
393
+ if (healthy) {
394
+ output(result, true, `${label}: ${passed}/${total} checks passed`);
395
+ } else {
396
+ output(result, true, `${label}: ${issues} issue(s) found`);
397
+ }
398
+ } else {
399
+ output(result, false);
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Lightweight init check.
405
+ * Returns: { initialized, marketing_dir, reference_files_count, total_expected }
406
+ *
407
+ * @param {boolean} raw - Whether to output raw summary string
408
+ */
409
+ function cmdInit(raw) {
410
+ const marketingDir = path.resolve(process.cwd(), '.marketing');
411
+ const marketingExists = dirExists(marketingDir);
412
+
413
+ let refCount = 0;
414
+ if (marketingExists) {
415
+ for (const file of REFERENCE_FILES) {
416
+ if (fileExists(path.resolve(marketingDir, file))) {
417
+ refCount++;
418
+ }
419
+ }
420
+ }
421
+
422
+ const totalExpected = REFERENCE_FILES.length;
423
+ const initialized = marketingExists && refCount >= totalExpected;
424
+
425
+ const result = {
426
+ initialized,
427
+ marketing_dir: marketingExists,
428
+ reference_files_count: refCount,
429
+ total_expected: totalExpected,
430
+ };
431
+
432
+ output(result, raw, initialized ? 'initialized' : 'not initialized');
433
+ }
434
+
435
+ module.exports = {
436
+ cmdHealth,
437
+ cmdInit,
438
+ };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Slug -- Slug generation and timestamp formatting for ttm-tools.cjs
3
+ *
4
+ * Zero npm dependencies. Uses only Node.js built-ins.
5
+ *
6
+ * Exports: cmdSlug, cmdTimestamp
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const { output, error } = require('./core.cjs');
12
+
13
+ /**
14
+ * Generate a URL-safe slug from text.
15
+ * Security: /[^a-z0-9]+/g strips all non-alphanumeric characters,
16
+ * preventing path traversal or injection via slug values.
17
+ *
18
+ * @param {string} text - Input text to slugify
19
+ * @param {boolean} raw - Whether to output raw string
20
+ */
21
+ function cmdSlug(text, raw) {
22
+ if (!text || !text.trim()) {
23
+ error('text required for slug generation');
24
+ }
25
+ const slug = text
26
+ .toLowerCase()
27
+ .replace(/[^a-z0-9]+/g, '-')
28
+ .replace(/^-+|-+$/g, '')
29
+ .substring(0, 60);
30
+ output({ slug }, raw, slug);
31
+ }
32
+
33
+ /**
34
+ * Output a timestamp in the specified format.
35
+ *
36
+ * @param {string} format - 'full' (default) | 'date' | 'filename'
37
+ * @param {boolean} raw - Whether to output raw string
38
+ */
39
+ function cmdTimestamp(format, raw) {
40
+ const now = new Date();
41
+ let result;
42
+ switch (format) {
43
+ case 'date':
44
+ result = now.toISOString().split('T')[0];
45
+ break;
46
+ case 'filename':
47
+ result = now.toISOString().replace(/:/g, '-').replace(/\..+/, '');
48
+ break;
49
+ default:
50
+ result = now.toISOString();
51
+ break;
52
+ }
53
+ output({ timestamp: result }, raw, result);
54
+ }
55
+
56
+ module.exports = {
57
+ cmdSlug,
58
+ cmdTimestamp,
59
+ };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * State -- STATE.md read/update operations for ttm-tools.cjs
3
+ *
4
+ * Zero npm dependencies. Uses only Node.js built-ins: path.
5
+ * Depends on: ./core.cjs for output, error, safeReadFile, safeWriteFile,
6
+ * parseFrontmatter, serializeFrontmatter
7
+ *
8
+ * Exports: cmdStateRead, cmdStateUpdate
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const path = require('path');
14
+ const {
15
+ output,
16
+ error,
17
+ safeReadFile,
18
+ safeWriteFile,
19
+ parseFrontmatter,
20
+ serializeFrontmatter,
21
+ } = require('./core.cjs');
22
+
23
+ /**
24
+ * Resolve and validate the STATE.md path.
25
+ * Security: uses path.resolve() and rejects paths containing '..' after resolution.
26
+ * @returns {string} Absolute path to .marketing/STATE.md
27
+ */
28
+ function resolveStatePath() {
29
+ const statePath = path.resolve(process.cwd(), '.marketing', 'STATE.md');
30
+ // Reject paths that escape the project directory
31
+ const projectRoot = path.resolve(process.cwd());
32
+ if (!statePath.startsWith(projectRoot)) {
33
+ error('STATE.md path escapes project directory');
34
+ }
35
+ return statePath;
36
+ }
37
+
38
+ /**
39
+ * Read .marketing/STATE.md, parse frontmatter, output as JSON.
40
+ * If STATE.md does not exist, outputs { exists: false, error: "..." }.
41
+ *
42
+ * @param {boolean} raw - Whether to output raw string
43
+ */
44
+ function cmdStateRead(raw) {
45
+ const statePath = resolveStatePath();
46
+ const content = safeReadFile(statePath);
47
+ if (content === null) {
48
+ output({ exists: false, error: 'STATE.md not found' }, raw, 'STATE.md not found');
49
+ return;
50
+ }
51
+ const { frontmatter, body } = parseFrontmatter(content);
52
+ const result = {
53
+ exists: true,
54
+ ...frontmatter,
55
+ body_preview: body.substring(0, 200),
56
+ };
57
+ output(result, raw, JSON.stringify(frontmatter));
58
+ }
59
+
60
+ /**
61
+ * Update a single field in STATE.md frontmatter.
62
+ * Sets last_updated to current ISO timestamp.
63
+ *
64
+ * @param {string} field - Frontmatter field name to update
65
+ * @param {string} value - New value for the field
66
+ * @param {boolean} raw - Whether to output raw string
67
+ */
68
+ function cmdStateUpdate(field, value, raw) {
69
+ if (!field) error('field name required for state update');
70
+ if (value === undefined || value === null) error('value required for state update');
71
+
72
+ const statePath = resolveStatePath();
73
+ const content = safeReadFile(statePath);
74
+ if (content === null) {
75
+ error('STATE.md not found -- run /ttm-init first');
76
+ }
77
+ const { frontmatter, body } = parseFrontmatter(content);
78
+ const timestamp = new Date().toISOString();
79
+
80
+ frontmatter[field] = value;
81
+ frontmatter['last_updated'] = timestamp;
82
+
83
+ const updated = serializeFrontmatter(frontmatter, body);
84
+ safeWriteFile(statePath, updated);
85
+
86
+ output(
87
+ { updated: field, value, last_updated: timestamp },
88
+ raw,
89
+ `${field}=${value}`
90
+ );
91
+ }
92
+
93
+ module.exports = {
94
+ cmdStateRead,
95
+ cmdStateUpdate,
96
+ };