@meshofgrowth/mogagentic 0.1.1

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 (3) hide show
  1. package/README.md +48 -0
  2. package/mogagentic.mjs +446 -0
  3. package/package.json +17 -0
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # @meshofgrowth/mogagentic
2
+
3
+ MogAgentic is a small CLI for Mesh of Growth / LaunchMesh that:
4
+ - generates a draft post (optional)
5
+ - asks for confirmation/edit
6
+ - publishes the post using your LaunchMesh API key
7
+
8
+ ## Requirements
9
+
10
+ - Node 18+
11
+ - A LaunchMesh API key (`lm_sk_...`)
12
+
13
+ ## Install
14
+
15
+ Run without installing (recommended):
16
+
17
+ ```bash
18
+ npx @meshofgrowth/mogagentic --help
19
+ ```
20
+
21
+ Or install globally:
22
+
23
+ ```bash
24
+ npm i -g @meshofgrowth/mogagentic
25
+ ```
26
+
27
+ ## Configure
28
+
29
+ ```bash
30
+ export LAUNCHMESH_BASE_URL="https://meshofgrowth.com" # or http://localhost:3002
31
+ export LAUNCHMESH_API_KEY="lm_sk_..."
32
+ ```
33
+
34
+ ## Activate (verifies your key)
35
+
36
+ ```bash
37
+ mogagentic init
38
+ ```
39
+
40
+ ## Commands
41
+
42
+ ```bash
43
+ mogagentic status
44
+ mogagentic post:daily
45
+ mogagentic post:feature
46
+ mogagentic product:new
47
+ ```
48
+
package/mogagentic.mjs ADDED
@@ -0,0 +1,446 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console */
3
+
4
+ import fs from 'node:fs'
5
+ import os from 'node:os'
6
+ import path from 'node:path'
7
+ import readline from 'node:readline'
8
+
9
+ function basenameNoExt(p) {
10
+ const b = path.basename(p)
11
+ return b.replace(/\.(mjs|js|cjs|ts)$/, '')
12
+ }
13
+
14
+ function getDefaultCommandFromInvokedName() {
15
+ const invoked = basenameNoExt(process.argv[1] || '')
16
+ if (invoked === 'mogpostdaily') return 'post:daily'
17
+ if (invoked === 'mogpostfeatureupdate') return 'post:feature'
18
+ if (invoked === 'mognewproduct') return 'product:new'
19
+ return null
20
+ }
21
+
22
+ function getConfigDir() {
23
+ const xdg = process.env.XDG_CONFIG_HOME
24
+ if (xdg) return path.join(xdg, 'launchmesh')
25
+ return path.join(os.homedir(), '.config', 'launchmesh')
26
+ }
27
+
28
+ function getConfigPath() {
29
+ return path.join(getConfigDir(), 'mogagentic.json')
30
+ }
31
+
32
+ function readConfig() {
33
+ const p = getConfigPath()
34
+ try {
35
+ const raw = fs.readFileSync(p, 'utf8')
36
+ return JSON.parse(raw)
37
+ } catch {
38
+ return null
39
+ }
40
+ }
41
+
42
+ function ensureDir(dir) {
43
+ fs.mkdirSync(dir, { recursive: true })
44
+ }
45
+
46
+ function writeConfig(cfg) {
47
+ const dir = getConfigDir()
48
+ ensureDir(dir)
49
+ const p = getConfigPath()
50
+ const tmp = `${p}.tmp`
51
+ fs.writeFileSync(tmp, JSON.stringify(cfg, null, 2), { mode: 0o600 })
52
+ fs.renameSync(tmp, p)
53
+ }
54
+
55
+ function maskKey(key) {
56
+ if (!key || typeof key !== 'string') return ''
57
+ if (key.length <= 10) return '***'
58
+ return `${key.slice(0, 10)}…${key.slice(-6)}`
59
+ }
60
+
61
+ function mkRl() {
62
+ return readline.createInterface({
63
+ input: process.stdin,
64
+ output: process.stdout,
65
+ })
66
+ }
67
+
68
+ function question(rl, prompt) {
69
+ return new Promise((resolve) => rl.question(prompt, resolve))
70
+ }
71
+
72
+ async function promptLine(label, { defaultValue = '', secret = false } = {}) {
73
+ const rl = mkRl()
74
+ try {
75
+ if (!secret) {
76
+ const hint = defaultValue ? ` (${defaultValue})` : ''
77
+ const v = String(await question(rl, `${label}${hint}: `)).trim()
78
+ return v || defaultValue
79
+ }
80
+
81
+ // Minimal "secret" prompt: we can't reliably disable echo cross-platform in pure readline,
82
+ // so we warn and suggest setting env var instead.
83
+ console.log(`Note: for safety, prefer setting LAUNCHMESH_API_KEY as an env var instead of typing it.`)
84
+ const v = String(await question(rl, `${label}: `)).trim()
85
+ return v
86
+ } finally {
87
+ rl.close()
88
+ }
89
+ }
90
+
91
+ async function promptConfirm(label, defaultYes = false) {
92
+ const rl = mkRl()
93
+ const suffix = defaultYes ? ' [Y/n]' : ' [y/N]'
94
+ try {
95
+ const ans = String(await question(rl, `${label}${suffix}: `)).trim().toLowerCase()
96
+ if (!ans) return defaultYes
97
+ return ans === 'y' || ans === 'yes'
98
+ } finally {
99
+ rl.close()
100
+ }
101
+ }
102
+
103
+ async function promptChoose(label, items, { toLabel = (x) => String(x), allowSkip = false } = {}) {
104
+ if (!Array.isArray(items) || items.length === 0) return null
105
+ console.log(label)
106
+ items.forEach((it, idx) => {
107
+ console.log(` ${idx + 1}. ${toLabel(it)}`)
108
+ })
109
+ if (allowSkip) console.log(` 0. Skip`)
110
+ const rl = mkRl()
111
+ try {
112
+ // eslint-disable-next-line no-constant-condition
113
+ while (true) {
114
+ const raw = String(await question(rl, `Choose 1-${items.length}${allowSkip ? ' (or 0)' : ''}: `)).trim()
115
+ const n = Number(raw)
116
+ if (allowSkip && n === 0) return null
117
+ if (Number.isInteger(n) && n >= 1 && n <= items.length) return items[n - 1]
118
+ console.log('Invalid choice.')
119
+ }
120
+ } finally {
121
+ rl.close()
122
+ }
123
+ }
124
+
125
+ function resolveRuntimeConfig() {
126
+ const cfg = readConfig() || {}
127
+ const apiKey = process.env.LAUNCHMESH_API_KEY || cfg.api_key || ''
128
+ const baseUrl = process.env.LAUNCHMESH_BASE_URL || cfg.base_url || 'http://localhost:3002'
129
+ return {
130
+ apiKey,
131
+ baseUrl: String(baseUrl).replace(/\/+$/, ''),
132
+ stored: cfg,
133
+ }
134
+ }
135
+
136
+ async function apiFetchJson({ baseUrl, apiKey, method, pathname, body }) {
137
+ const url = `${baseUrl}${pathname}`
138
+ const headers = {
139
+ Authorization: `Bearer ${apiKey}`,
140
+ }
141
+ if (body !== undefined) headers['Content-Type'] = 'application/json'
142
+
143
+ const res = await fetch(url, {
144
+ method,
145
+ headers,
146
+ body: body === undefined ? undefined : JSON.stringify(body),
147
+ })
148
+ const text = await res.text().catch(() => '')
149
+ let json = null
150
+ try {
151
+ json = text ? JSON.parse(text) : null
152
+ } catch {
153
+ json = null
154
+ }
155
+ if (!res.ok) {
156
+ const msg = json?.error || `HTTP ${res.status} from ${pathname}`
157
+ const err = new Error(msg)
158
+ err.status = res.status
159
+ err.payload = json
160
+ throw err
161
+ }
162
+ return json
163
+ }
164
+
165
+ async function cmdInit() {
166
+ const current = resolveRuntimeConfig()
167
+ const baseUrl = await promptLine('LaunchMesh base URL', { defaultValue: current.baseUrl })
168
+ let apiKey = process.env.LAUNCHMESH_API_KEY || ''
169
+ if (!apiKey) {
170
+ apiKey = await promptLine('Paste your LaunchMesh API key (recommended: set LAUNCHMESH_API_KEY env var instead)', { secret: true })
171
+ }
172
+ if (!apiKey) {
173
+ console.error('Missing API key. Set LAUNCHMESH_API_KEY or run init again.')
174
+ process.exit(1)
175
+ }
176
+
177
+ // Verify activation
178
+ const ctx = await apiFetchJson({
179
+ baseUrl,
180
+ apiKey,
181
+ method: 'GET',
182
+ pathname: '/api/skill/bot-launch/context',
183
+ })
184
+
185
+ const cfg = {
186
+ base_url: baseUrl,
187
+ api_key: apiKey,
188
+ activated_at: new Date().toISOString(),
189
+ founder_id: ctx?.founder_id || null,
190
+ key_prefix: typeof apiKey === 'string' ? apiKey.split('_').slice(0, 3).join('_') : null,
191
+ }
192
+ writeConfig(cfg)
193
+
194
+ console.log('Activated mogagentic.')
195
+ console.log(` base_url: ${baseUrl}`)
196
+ console.log(` api_key: ${maskKey(apiKey)}`)
197
+ if (cfg.founder_id) console.log(` founder_id: ${cfg.founder_id}`)
198
+ }
199
+
200
+ async function cmdStatus() {
201
+ const cfg = readConfig()
202
+ const rt = resolveRuntimeConfig()
203
+ if (!cfg) {
204
+ console.log('mogagentic is not initialized yet.')
205
+ console.log('Run: mogagentic init')
206
+ return
207
+ }
208
+ console.log('mogagentic status')
209
+ console.log(` base_url: ${rt.baseUrl}`)
210
+ console.log(` api_key: ${maskKey(rt.apiKey)}`)
211
+ if (cfg.activated_at) console.log(` activated_at: ${cfg.activated_at}`)
212
+ if (cfg.founder_id) console.log(` founder_id: ${cfg.founder_id}`)
213
+ }
214
+
215
+ async function loadContextOrDie() {
216
+ const rt = resolveRuntimeConfig()
217
+ if (!rt.apiKey) {
218
+ console.error('Missing API key. Set LAUNCHMESH_API_KEY or run: mogagentic init')
219
+ process.exit(1)
220
+ }
221
+ const ctx = await apiFetchJson({
222
+ baseUrl: rt.baseUrl,
223
+ apiKey: rt.apiKey,
224
+ method: 'GET',
225
+ pathname: '/api/skill/bot-launch/context',
226
+ })
227
+ return { rt, ctx }
228
+ }
229
+
230
+ async function cmdPostDaily() {
231
+ const { rt, ctx } = await loadContextOrDie()
232
+ const products = ctx.products || []
233
+ if (!products.length) {
234
+ console.error('No products found. Create a product first.')
235
+ process.exit(1)
236
+ }
237
+
238
+ const product = await promptChoose('Select a product:', products, {
239
+ toLabel: (p) => `${p.name} (${p.id})${p.posting_status?.posted_today ? ' [posted today]' : ''}`,
240
+ })
241
+ if (!product) process.exit(1)
242
+
243
+ const wantGenerate = await promptConfirm('Generate a draft automatically?', true)
244
+ let content = ''
245
+ if (wantGenerate) {
246
+ const gen = await apiFetchJson({
247
+ baseUrl: rt.baseUrl,
248
+ apiKey: rt.apiKey,
249
+ method: 'POST',
250
+ pathname: '/api/skill/bot-launch/generate',
251
+ body: { mode: 'general_daily', product_id: product.id, hints: {} },
252
+ })
253
+ content = String(gen?.generated?.content || '').trim()
254
+ }
255
+ if (!content) {
256
+ content = await promptLine('Write your daily post content (tweet-style)')
257
+ } else {
258
+ console.log('\nDraft:\n')
259
+ console.log(content)
260
+ const edit = await promptConfirm('\nEdit this draft before posting?', false)
261
+ if (edit) content = await promptLine('Updated content')
262
+ }
263
+
264
+ if (!content) {
265
+ console.error('Missing content.')
266
+ process.exit(1)
267
+ }
268
+
269
+ const ok = await promptConfirm('Post now?', false)
270
+ if (!ok) {
271
+ console.log('Cancelled.')
272
+ return
273
+ }
274
+
275
+ const res = await apiFetchJson({
276
+ baseUrl: rt.baseUrl,
277
+ apiKey: rt.apiKey,
278
+ method: 'POST',
279
+ pathname: '/api/skill/bot-launch/publish-general-post',
280
+ body: { product_id: product.id, content },
281
+ })
282
+ console.log(`Posted. post_id=${res?.post?.id || 'unknown'}`)
283
+ }
284
+
285
+ async function cmdPostFeature() {
286
+ const { rt, ctx } = await loadContextOrDie()
287
+ const products = ctx.products || []
288
+ if (!products.length) {
289
+ console.error('No products found. Create a product first.')
290
+ process.exit(1)
291
+ }
292
+
293
+ const product = await promptChoose('Select a product:', products, {
294
+ toLabel: (p) => `${p.name} (${p.id})`,
295
+ })
296
+ if (!product) process.exit(1)
297
+
298
+ const hint = await promptLine('What changed? (short hint for the generator)', { defaultValue: '' })
299
+
300
+ const wantGenerate = await promptConfirm('Generate a feature update draft automatically?', true)
301
+ let content = ''
302
+ if (wantGenerate) {
303
+ const gen = await apiFetchJson({
304
+ baseUrl: rt.baseUrl,
305
+ apiKey: rt.apiKey,
306
+ method: 'POST',
307
+ pathname: '/api/skill/bot-launch/generate',
308
+ body: {
309
+ mode: 'feature_update',
310
+ product_id: product.id,
311
+ hints: hint ? { what_changed: hint } : {},
312
+ },
313
+ })
314
+ content = String(gen?.generated?.content || '').trim()
315
+ if (!content) {
316
+ // If generator returns structured fields but not content, fallback to assembling.
317
+ const title = gen?.generated?.feature_title ? `Feature: ${gen.generated.feature_title}\n\n` : ''
318
+ const wc = gen?.generated?.what_changed ? `What changed: ${gen.generated.what_changed}\n` : ''
319
+ const why = gen?.generated?.why_it_matters ? `Why it matters: ${gen.generated.why_it_matters}\n` : ''
320
+ content = `${title}${wc}${why}`.trim()
321
+ }
322
+ }
323
+
324
+ if (!content) {
325
+ content = await promptLine('Write your feature update content')
326
+ } else {
327
+ console.log('\nDraft:\n')
328
+ console.log(content)
329
+ const edit = await promptConfirm('\nEdit this draft before posting?', true)
330
+ if (edit) content = await promptLine('Updated content')
331
+ }
332
+
333
+ if (!content) {
334
+ console.error('Missing content.')
335
+ process.exit(1)
336
+ }
337
+
338
+ const ok = await promptConfirm('Post now?', false)
339
+ if (!ok) {
340
+ console.log('Cancelled.')
341
+ return
342
+ }
343
+
344
+ const res = await apiFetchJson({
345
+ baseUrl: rt.baseUrl,
346
+ apiKey: rt.apiKey,
347
+ method: 'POST',
348
+ pathname: '/api/skill/bot-launch/publish-feature-update',
349
+ body: { product_id: product.id, content, media_urls: [] },
350
+ })
351
+ console.log(`Posted. post_id=${res?.post?.id || 'unknown'}`)
352
+ }
353
+
354
+ async function cmdNewProduct() {
355
+ const { rt, ctx } = await loadContextOrDie()
356
+ const startups = (ctx.startups || []).filter((s) => !s.is_product)
357
+ if (!startups.length) {
358
+ console.error('No startups (orgs) found. Create a startup first in the app.')
359
+ process.exit(1)
360
+ }
361
+
362
+ const org = await promptChoose('Select a startup (org) to add a product under:', startups, {
363
+ toLabel: (s) => `${s.name} (${s.id})`,
364
+ })
365
+ if (!org) process.exit(1)
366
+
367
+ const name = await promptLine('Product name')
368
+ const short_description = await promptLine('Short description (one line)')
369
+ const mini_blog = await promptLine('Mini blog/description (2-5 sentences)')
370
+ const website_url = await promptLine('Website URL (optional)', { defaultValue: '' })
371
+
372
+ if (!name || !short_description || !mini_blog) {
373
+ console.error('Missing required fields: name, short description, mini blog.')
374
+ process.exit(1)
375
+ }
376
+
377
+ const ok = await promptConfirm('Create product now?', false)
378
+ if (!ok) {
379
+ console.log('Cancelled.')
380
+ return
381
+ }
382
+
383
+ const res = await apiFetchJson({
384
+ baseUrl: rt.baseUrl,
385
+ apiKey: rt.apiKey,
386
+ method: 'POST',
387
+ pathname: '/api/skill/bot-launch/create-product',
388
+ body: {
389
+ startup_id: org.id,
390
+ name,
391
+ short_description,
392
+ mini_blog,
393
+ category: null,
394
+ website_url: website_url || null,
395
+ },
396
+ })
397
+ console.log(`Created product. product_id=${res?.product?.id || 'unknown'}`)
398
+ }
399
+
400
+ function printHelp() {
401
+ console.log(`mogagentic (Bot Launch skill)
402
+
403
+ Setup:
404
+ mogagentic init
405
+ mogagentic status
406
+
407
+ Commands:
408
+ mogpostdaily Generate (optional) + confirm + post daily update
409
+ mogpostfeatureupdate Generate (optional) + confirm + post feature update
410
+ mognewproduct Create a new product under an existing startup
411
+
412
+ Config:
413
+ LAUNCHMESH_API_KEY API key (recommended)
414
+ LAUNCHMESH_BASE_URL Base URL (default: http://localhost:3002)
415
+ `)
416
+ }
417
+
418
+ async function main() {
419
+ const defaultCmd = getDefaultCommandFromInvokedName()
420
+ const args = process.argv.slice(2)
421
+
422
+ const cmd = defaultCmd || args[0] || 'help'
423
+
424
+ try {
425
+ if (cmd === 'help' || cmd === '--help' || cmd === '-h') return printHelp()
426
+ if (cmd === 'init') return await cmdInit()
427
+ if (cmd === 'status') return await cmdStatus()
428
+ if (cmd === 'post:daily' || cmd === 'postdaily') return await cmdPostDaily()
429
+ if (cmd === 'post:feature' || cmd === 'postfeature') return await cmdPostFeature()
430
+ if (cmd === 'product:new' || cmd === 'newproduct') return await cmdNewProduct()
431
+
432
+ // Support: mogagentic mogpostdaily (people may type the alias as a subcommand)
433
+ if (cmd === 'mogpostdaily') return await cmdPostDaily()
434
+ if (cmd === 'mogpostfeatureupdate') return await cmdPostFeature()
435
+ if (cmd === 'mognewproduct') return await cmdNewProduct()
436
+
437
+ printHelp()
438
+ process.exitCode = 1
439
+ } catch (err) {
440
+ console.error(err?.message || String(err))
441
+ process.exitCode = 1
442
+ }
443
+ }
444
+
445
+ await main()
446
+
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@meshofgrowth/mogagentic",
3
+ "version": "0.1.1",
4
+ "description": "MogAgentic CLI for Mesh of Growth (generate + confirm + post)",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "mogagentic": "mogagentic.mjs"
9
+ },
10
+ "files": [
11
+ "mogagentic.mjs",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ }
17
+ }