@dev-adoption/cli 0.1.8
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/README.md +190 -0
- package/bin/dai.mjs +7 -0
- package/package.json +48 -0
- package/src/index.mjs +2753 -0
package/src/index.mjs
ADDED
|
@@ -0,0 +1,2753 @@
|
|
|
1
|
+
// dai — Developer Adoption Intelligence CLI.
|
|
2
|
+
//
|
|
3
|
+
// Commands:
|
|
4
|
+
// dai login - OAuth device-code login
|
|
5
|
+
// dai logout - Clear credentials
|
|
6
|
+
// dai stack add <tool> - Add a tool to your public stack
|
|
7
|
+
// dai stack remove <tool> - Remove a tool from your stack
|
|
8
|
+
// dai stack sync - Sync local package.json into your stack
|
|
9
|
+
// dai submit <challenge> [--file] - Submit a challenge entry
|
|
10
|
+
// dai proof <tool> - Print the public adoption proof JSON
|
|
11
|
+
// dai rank [tool] - Show live AI Adoption Score rankings
|
|
12
|
+
// dai notifications - List unread notifications
|
|
13
|
+
// dai digest - Fetch this week's personalized digest
|
|
14
|
+
// dai mcp serve - Start the MCP stdio server for Cursor/Codex
|
|
15
|
+
// dai project draft - Draft a project from the current repo
|
|
16
|
+
// dai project publish --yes - Publish the current repo to tokens&
|
|
17
|
+
// dai whoami - Show the signed-in account
|
|
18
|
+
// dai version - Print CLI version
|
|
19
|
+
//
|
|
20
|
+
// The CLI keeps zero runtime dependencies so it works on a locked-down
|
|
21
|
+
// developer box without pulling a tree on first use.
|
|
22
|
+
|
|
23
|
+
import { readFile } from 'node:fs/promises'
|
|
24
|
+
import { homedir } from 'node:os'
|
|
25
|
+
import { basename, join } from 'node:path'
|
|
26
|
+
|
|
27
|
+
const VERSION = '0.1.8'
|
|
28
|
+
const CLI_PACKAGE_SPEC = `@dev-adoption/cli@${VERSION}`
|
|
29
|
+
const ENTERPRISE_GROWTH_PROMISE = 'Create a tracked developer session, invite the right builders, onboard partner companies, and prove adoption.'
|
|
30
|
+
const ENTERPRISE_ADOPTION_BOUNDARY =
|
|
31
|
+
'Tokens& creates tracking, partner intake, ICP preview, and proof. It does not create Luma/Zoom/Eventbrite pages or send external invites without approval.'
|
|
32
|
+
const DEFAULT_API = process.env.DAI_API_URL || 'https://tokensand.com'
|
|
33
|
+
const CONFIG_PATH = join(homedir(), '.dai', 'config.json')
|
|
34
|
+
const TRACKING_TIMEOUT_MS = 900
|
|
35
|
+
const MODE_VALUES = new Set(['auto', 'public', 'developer', 'enterprise'])
|
|
36
|
+
|
|
37
|
+
const ENTERPRISE_PROFILE_PRESETS = {
|
|
38
|
+
redis: {
|
|
39
|
+
company: 'Redis',
|
|
40
|
+
segment: 'data infrastructure and backend platform teams',
|
|
41
|
+
sessionLabel: 'AI app performance workshop',
|
|
42
|
+
buyerProblem: 'Convert cache/search/vector interest into production workload proof.',
|
|
43
|
+
defaultBudgetUsd: 45000,
|
|
44
|
+
benchmark: { registrations: 180, attended: 102, builders: 54, activated: 31, retained: 14, proofCount: 9 },
|
|
45
|
+
events: ['workshop_registered', 'lab_completed', 'database_connected', 'production_workload_proof'],
|
|
46
|
+
icpSegments: [
|
|
47
|
+
{
|
|
48
|
+
name: 'AI app teams adding semantic cache or vector retrieval to a live backend',
|
|
49
|
+
score: 94,
|
|
50
|
+
why: 'They have an urgent latency/cost pain and can produce repo, demo, and workload proof quickly.',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'Platform/data infrastructure teams modernizing cache, search, or queue layers',
|
|
54
|
+
score: 86,
|
|
55
|
+
why: 'They can expand across multiple workloads, but proof cycles need account-owner handoff.',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'B2B SaaS teams shipping retrieval-heavy agent features',
|
|
59
|
+
score: 81,
|
|
60
|
+
why: 'They are easier to activate through Codex/Cursor but need a retained-usage reward to avoid one-off demos.',
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
sessionFormats: [
|
|
64
|
+
{
|
|
65
|
+
format: 'hands-on performance lab',
|
|
66
|
+
cadence: '2 sessions in 10 days, then one retained-usage proof sprint',
|
|
67
|
+
why: 'Best match when proof rate is the bottleneck: every attendee leaves with a repo/demo and a measured Redis event.',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
format: 'cache/vector schema clinic',
|
|
71
|
+
cadence: 'weekly office-hours queue for activated projects',
|
|
72
|
+
why: 'Best follow-up for builders who reached first success but have not retained production-like usage.',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
format: 'partner proof sprint',
|
|
76
|
+
cadence: 'one focused challenge with 7-day submission window',
|
|
77
|
+
why: 'Best when the offer is credits plus featured proof, not broad awareness.',
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
offers: [
|
|
81
|
+
'Redis Cloud credits tied to a working AI app demo',
|
|
82
|
+
'30-minute cache/vector schema review for projects that ship proof',
|
|
83
|
+
'Featured project badge for retained production usage',
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
assemblyai: {
|
|
87
|
+
company: 'AssemblyAI',
|
|
88
|
+
segment: 'voice AI builders and product engineering teams',
|
|
89
|
+
sessionLabel: 'voice agent build session',
|
|
90
|
+
buyerProblem: 'Move transcript demos into retained voice/agent workflows.',
|
|
91
|
+
defaultBudgetUsd: 38000,
|
|
92
|
+
benchmark: { registrations: 140, attended: 84, builders: 46, activated: 29, retained: 13, proofCount: 8 },
|
|
93
|
+
events: ['session_attended', 'api_key_created', 'first_transcript_completed', 'voice_agent_demo_published'],
|
|
94
|
+
icpSegments: [
|
|
95
|
+
{ name: 'Voice agent product teams', score: 93, why: 'They can turn a demo into retained transcript, streaming, or agent workflow evidence.' },
|
|
96
|
+
{ name: 'Support automation teams', score: 84, why: 'They have measurable time-saved outcomes but need privacy-safe proof boundaries.' },
|
|
97
|
+
{ name: 'Creator/audio app builders', score: 76, why: 'High activation rate, but lower enterprise expansion unless account use is instrumented.' },
|
|
98
|
+
],
|
|
99
|
+
sessionFormats: [
|
|
100
|
+
{ format: 'voice agent build lab', cadence: 'one build lab plus 48-hour review queue', why: 'Optimizes for first transcript and published demo proof.' },
|
|
101
|
+
{ format: 'streaming API clinic', cadence: 'weekly retained-workflow office hours', why: 'Best after activation when quality, latency, and cost need tuning.' },
|
|
102
|
+
{ format: 'customer-workflow challenge', cadence: '7-day proof sprint', why: 'Forces a real workflow instead of a toy audio demo.' },
|
|
103
|
+
],
|
|
104
|
+
offers: [
|
|
105
|
+
'API credits for a published voice agent demo',
|
|
106
|
+
'Solution review for teams that process retained production audio',
|
|
107
|
+
'Founder office hours for proofs with customer evidence',
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
clickhouse: {
|
|
111
|
+
company: 'ClickHouse',
|
|
112
|
+
segment: 'analytics engineering and data-intensive AI teams',
|
|
113
|
+
sessionLabel: 'real-time analytics build lab',
|
|
114
|
+
buyerProblem: 'Turn analytics curiosity into instrumented retained workloads.',
|
|
115
|
+
defaultBudgetUsd: 42000,
|
|
116
|
+
benchmark: { registrations: 165, attended: 91, builders: 48, activated: 27, retained: 16, proofCount: 10 },
|
|
117
|
+
events: ['event_pipeline_started', 'query_dashboard_created', 'latency_claim_verified', 'production_dataset_connected'],
|
|
118
|
+
icpSegments: [
|
|
119
|
+
{ name: 'Analytics engineering teams with real-time workloads', score: 95, why: 'They can produce latency and retained-query proof that maps to account expansion.' },
|
|
120
|
+
{ name: 'AI observability and event-pipeline builders', score: 87, why: 'They need fast analytical paths and can demonstrate production-like data volume.' },
|
|
121
|
+
{ name: 'Data-intensive founder teams', score: 79, why: 'Fast to activate, but needs dataset and cost guardrails before procurement claims.' },
|
|
122
|
+
],
|
|
123
|
+
sessionFormats: [
|
|
124
|
+
{ format: 'real-time analytics build lab', cadence: 'two technical labs plus benchmark review', why: 'Best for showing latency and cost deltas with source-labeled evidence.' },
|
|
125
|
+
{ format: 'dataset performance clinic', cadence: 'weekly review for projects with live datasets', why: 'Converts first query into retained production-like usage.' },
|
|
126
|
+
{ format: 'benchmark proof sprint', cadence: 'one 7-day challenge', why: 'Best when public proof needs latency and dataset evidence.' },
|
|
127
|
+
],
|
|
128
|
+
offers: [
|
|
129
|
+
'ClickHouse Cloud credits for a live analytics benchmark',
|
|
130
|
+
'Performance review for projects with public query latency proof',
|
|
131
|
+
'Co-marketing slot for retained analytics workloads',
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
supabase: {
|
|
135
|
+
company: 'Supabase',
|
|
136
|
+
segment: 'full-stack builders and AI app teams',
|
|
137
|
+
sessionLabel: 'agent app shipping sprint',
|
|
138
|
+
buyerProblem: 'Move quickstarts into retained projects with auth, data, and usage proof.',
|
|
139
|
+
defaultBudgetUsd: 32000,
|
|
140
|
+
benchmark: { registrations: 220, attended: 132, builders: 82, activated: 54, retained: 28, proofCount: 18 },
|
|
141
|
+
events: ['project_created', 'auth_enabled', 'database_event_tracked', 'retained_app_usage'],
|
|
142
|
+
icpSegments: [
|
|
143
|
+
{ name: 'Full-stack AI app founders', score: 92, why: 'They can ship quickly and publish repo/demo proof with auth, data, and usage events.' },
|
|
144
|
+
{ name: 'Internal tool builders', score: 82, why: 'Strong retained usage potential, but public proof may need privacy-safe summaries.' },
|
|
145
|
+
{ name: 'Hackathon and launch-week builders', score: 77, why: 'High attendance and activation, lower retention unless follow-up proof is rewarded.' },
|
|
146
|
+
],
|
|
147
|
+
sessionFormats: [
|
|
148
|
+
{ format: 'agent app shipping sprint', cadence: 'weekly build session plus 7-day retained-usage check', why: 'Best for converting quickstarts into working projects.' },
|
|
149
|
+
{ format: 'auth/data production clinic', cadence: 'weekly after first success', why: 'Best when retention or security readiness blocks proof.' },
|
|
150
|
+
{ format: 'launch proof challenge', cadence: 'one short challenge per cohort', why: 'Best for turning shipped demos into public proof.' },
|
|
151
|
+
],
|
|
152
|
+
offers: [
|
|
153
|
+
'Startup credits for apps that ship proof',
|
|
154
|
+
'Launch review for projects with retained usage events',
|
|
155
|
+
'Featured builder directory placement for public proof',
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
default: {
|
|
159
|
+
company: 'Enterprise workspace',
|
|
160
|
+
segment: 'developer-tool buyers and builder-facing GTM teams',
|
|
161
|
+
sessionLabel: 'builder activation session',
|
|
162
|
+
buyerProblem: 'Convert session attendance into shipped projects, retained usage, and account-qualified proof.',
|
|
163
|
+
defaultBudgetUsd: 25000,
|
|
164
|
+
benchmark: { registrations: 120, attended: 72, builders: 36, activated: 20, retained: 9, proofCount: 6 },
|
|
165
|
+
events: ['session_attended', 'perk_claimed', 'first_success', 'project_proof_published'],
|
|
166
|
+
icpSegments: [
|
|
167
|
+
{ name: 'Builders with an urgent production workflow', score: 88, why: 'They can prove value through retained usage instead of attendance.' },
|
|
168
|
+
{ name: 'Founder-led technical teams', score: 81, why: 'Fast shipping loop and visible proof, but smaller account expansion initially.' },
|
|
169
|
+
{ name: 'Enterprise developer champions', score: 76, why: 'High contract value, slower proof path unless account workflow data is connected.' },
|
|
170
|
+
],
|
|
171
|
+
sessionFormats: [
|
|
172
|
+
{ format: 'builder activation lab', cadence: 'weekly session plus proof sprint', why: 'Best default format for first-success and proof conversion.' },
|
|
173
|
+
{ format: 'technical office hours', cadence: 'twice weekly for active builders', why: 'Best when activation blockers are setup/debugging problems.' },
|
|
174
|
+
{ format: 'proof challenge', cadence: '7-day submission window', why: 'Best when public proof and retained usage are the constraint.' },
|
|
175
|
+
],
|
|
176
|
+
offers: [
|
|
177
|
+
'Credits that unlock only after a working build is submitted',
|
|
178
|
+
'Expert review for projects that reach first success',
|
|
179
|
+
'Featured placement for retained usage and public proof',
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function loadConfig() {
|
|
185
|
+
try {
|
|
186
|
+
const raw = await readFile(CONFIG_PATH, 'utf8')
|
|
187
|
+
return JSON.parse(raw)
|
|
188
|
+
} catch {
|
|
189
|
+
return {}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function api(path, { method = 'GET', body, token, baseUrl = DEFAULT_API } = {}) {
|
|
194
|
+
const headers = { accept: 'application/json' }
|
|
195
|
+
if (token) headers.authorization = `Bearer ${token}`
|
|
196
|
+
if (body) headers['content-type'] = 'application/json'
|
|
197
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
198
|
+
method,
|
|
199
|
+
headers,
|
|
200
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
201
|
+
})
|
|
202
|
+
if (!res.ok) {
|
|
203
|
+
const text = await res.text().catch(() => '')
|
|
204
|
+
throw new Error(`HTTP ${res.status}: ${text || res.statusText}`)
|
|
205
|
+
}
|
|
206
|
+
const ctype = res.headers.get('content-type') || ''
|
|
207
|
+
return ctype.includes('application/json') ? res.json() : res.text()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function parseGlobals(argv) {
|
|
211
|
+
const clean = []
|
|
212
|
+
let apiBase = DEFAULT_API
|
|
213
|
+
let json = false
|
|
214
|
+
let mode = ''
|
|
215
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
216
|
+
const arg = argv[i]
|
|
217
|
+
if (arg === '--json') {
|
|
218
|
+
json = true
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
if (arg === '--api') {
|
|
222
|
+
apiBase = argv[i + 1] || apiBase
|
|
223
|
+
i += 1
|
|
224
|
+
continue
|
|
225
|
+
}
|
|
226
|
+
if (arg === '--mode') {
|
|
227
|
+
mode = String(argv[i + 1] || '').trim().toLowerCase()
|
|
228
|
+
i += 1
|
|
229
|
+
continue
|
|
230
|
+
}
|
|
231
|
+
clean.push(arg)
|
|
232
|
+
}
|
|
233
|
+
return { clean, apiBase: apiBase.replace(/\/$/, ''), json, mode }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function formatAdoptionRow(row) {
|
|
237
|
+
const name = row?.tool?.name ?? row?.toolName ?? 'Unknown tool'
|
|
238
|
+
const score = row?.adoptionScore ?? row?.adoptionIndex ?? 'pending'
|
|
239
|
+
const rank = row?.categoryRank ? `#${row.categoryRank}` : '#?'
|
|
240
|
+
const trust = row?.trustState ?? row?.confidenceLabel ?? 'observed'
|
|
241
|
+
const verified = row?.verifiedAdoptions ?? 0
|
|
242
|
+
const cohort = row?.cohortSize ?? 0
|
|
243
|
+
const suppressed = row?.suppressed ? ' · suppressed' : ''
|
|
244
|
+
return `${name}: score ${score} · ${rank} · ${trust} · ${verified} verified · N=${cohort}${suppressed}`
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function mcpText(text) {
|
|
248
|
+
return { content: [{ type: 'text', text: typeof text === 'string' ? text : JSON.stringify(text, null, 2) }] }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function authRequiredPayload(state, action) {
|
|
252
|
+
return {
|
|
253
|
+
authRequired: true,
|
|
254
|
+
action,
|
|
255
|
+
message:
|
|
256
|
+
'Tokens& is connected. Public tool search works without auth; private saved stack, Build Packet, and project proof context require a scoped workflow token.',
|
|
257
|
+
nextSteps: [
|
|
258
|
+
`Open ${state.baseUrl}/projects/new?source=agent_pack`,
|
|
259
|
+
'Create or select a project.',
|
|
260
|
+
'Generate a scoped workflow token.',
|
|
261
|
+
'Add it as DAI_TOKEN in your MCP config.',
|
|
262
|
+
],
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function enterpriseAuthRequiredPayload(state, action) {
|
|
267
|
+
return {
|
|
268
|
+
authRequired: true,
|
|
269
|
+
enterpriseAuthRequired: true,
|
|
270
|
+
action,
|
|
271
|
+
message:
|
|
272
|
+
'Private enterprise session, account, and tenant metrics require an enterprise-scoped MCP credential. Codex/Cursor do not automatically inherit a Tokens& browser login.',
|
|
273
|
+
currentMode: state.mode,
|
|
274
|
+
configuredContext: {
|
|
275
|
+
organizationId: state.organizationId || null,
|
|
276
|
+
enterpriseProfile: state.enterpriseProfile || null,
|
|
277
|
+
hasEnterpriseToken: Boolean(state.enterpriseToken),
|
|
278
|
+
},
|
|
279
|
+
nextSteps: [
|
|
280
|
+
`Open ${state.baseUrl}/dashboard?mode=enterprise`,
|
|
281
|
+
'Select the enterprise workspace and verify the session/adoption data there.',
|
|
282
|
+
'Use enterprise_session_brief with explicit session metrics now, or configure an enterprise-scoped MCP token when enabled.',
|
|
283
|
+
'Keep raw developer identity and account-level exports inside an authorized enterprise workspace.',
|
|
284
|
+
],
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function authRequiredContent(state, uri) {
|
|
289
|
+
const payload = authRequiredPayload(state, `read ${uri}`)
|
|
290
|
+
return {
|
|
291
|
+
uri,
|
|
292
|
+
mimeType: uri.endsWith('.md') ? 'text/markdown' : 'application/json',
|
|
293
|
+
text: uri.endsWith('.md')
|
|
294
|
+
? [
|
|
295
|
+
'# Tokens& workflow token required',
|
|
296
|
+
'',
|
|
297
|
+
payload.message,
|
|
298
|
+
'',
|
|
299
|
+
...payload.nextSteps.map((step, index) => `${index + 1}. ${step}`),
|
|
300
|
+
].join('\n')
|
|
301
|
+
: JSON.stringify(payload, null, 2),
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function envValue(...names) {
|
|
306
|
+
for (const name of names) {
|
|
307
|
+
const value = process.env[name]
|
|
308
|
+
if (typeof value === 'string' && value.trim()) return value.trim()
|
|
309
|
+
}
|
|
310
|
+
return ''
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function normalizeMode(value = '') {
|
|
314
|
+
const mode = String(value || '').trim().toLowerCase()
|
|
315
|
+
return MODE_VALUES.has(mode) ? mode : 'auto'
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function enterpriseToken(config = {}) {
|
|
319
|
+
return config.enterpriseToken || config.enterprise_token || envValue('DAI_ENTERPRISE_TOKEN', 'TOKENSAND_ENTERPRISE_TOKEN')
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function enterpriseOrganizationId(config = {}) {
|
|
323
|
+
return config.organizationId || config.organization_id || envValue('DAI_ORGANIZATION_ID', 'TOKENSAND_ORGANIZATION_ID')
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function enterpriseProfileKey(value = '') {
|
|
327
|
+
const normalized = String(value || '').trim().toLowerCase()
|
|
328
|
+
if (!normalized) return ''
|
|
329
|
+
if (ENTERPRISE_PROFILE_PRESETS[normalized]) return normalized
|
|
330
|
+
for (const [key, profile] of Object.entries(ENTERPRISE_PROFILE_PRESETS)) {
|
|
331
|
+
if (key === 'default') continue
|
|
332
|
+
const haystack = `${profile.company} ${profile.segment}`.toLowerCase()
|
|
333
|
+
if (haystack.includes(normalized) || normalized.includes(key)) return key
|
|
334
|
+
}
|
|
335
|
+
return 'default'
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function resolveEnterpriseProfile(config = {}, args = {}) {
|
|
339
|
+
return enterpriseProfileKey(
|
|
340
|
+
args.profile ||
|
|
341
|
+
args.company ||
|
|
342
|
+
config.enterpriseProfile ||
|
|
343
|
+
config.enterprise_profile ||
|
|
344
|
+
envValue('DAI_ENTERPRISE_PROFILE', 'TOKENSAND_ENTERPRISE_PROFILE'),
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function resolveMcpMode({ requestedMode, token, enterpriseToken: privateEnterpriseToken, organizationId, enterpriseProfile }) {
|
|
349
|
+
const normalized = normalizeMode(requestedMode || envValue('DAI_MODE', 'TOKENSAND_MODE'))
|
|
350
|
+
if (normalized !== 'auto') return normalized
|
|
351
|
+
if (privateEnterpriseToken || organizationId || enterpriseProfile) return 'enterprise'
|
|
352
|
+
if (token) return 'developer'
|
|
353
|
+
return 'public'
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function accountModePayload(state) {
|
|
357
|
+
const developerPrivateTools = state.token
|
|
358
|
+
? ['get_context', 'get_build_packet', 'draft_project', 'publish_project']
|
|
359
|
+
: []
|
|
360
|
+
const enterprisePrivateReady = Boolean(state.enterpriseToken)
|
|
361
|
+
return {
|
|
362
|
+
product: 'Tokens& MCP',
|
|
363
|
+
detectedMode: state.mode,
|
|
364
|
+
authBoundary:
|
|
365
|
+
'Codex/Cursor MCP servers are configured by env/config. A Tokens& browser login is not automatically visible inside the agent host.',
|
|
366
|
+
credentials: {
|
|
367
|
+
hasDeveloperWorkflowToken: Boolean(state.token),
|
|
368
|
+
developerTokenEnv: 'DAI_TOKEN or DAI_WORKFLOW_TOKEN',
|
|
369
|
+
hasEnterpriseToken: enterprisePrivateReady,
|
|
370
|
+
enterpriseTokenEnv: 'DAI_ENTERPRISE_TOKEN',
|
|
371
|
+
organizationId: state.organizationId || null,
|
|
372
|
+
enterpriseProfile: state.enterpriseProfile || null,
|
|
373
|
+
},
|
|
374
|
+
availableNow: {
|
|
375
|
+
builderPublic: ['search_tools', 'find_perks', 'get_adoption_rank', 'build_brief'],
|
|
376
|
+
builderPrivate: developerPrivateTools.length ? developerPrivateTools : 'locked until DAI_TOKEN is configured',
|
|
377
|
+
enterprisePublic: [
|
|
378
|
+
'enterprise_session_brief with pasted metrics, budget, ICP segments, or sample profile context',
|
|
379
|
+
'approval-only motion drafts: adoption session, ICP invite batch, Invite partner company, and adoption proof',
|
|
380
|
+
],
|
|
381
|
+
enterprisePrivate: enterprisePrivateReady
|
|
382
|
+
? 'enterprise credential configured; use only authorized organization data'
|
|
383
|
+
: 'locked until enterprise-scoped MCP credentials are issued/configured',
|
|
384
|
+
},
|
|
385
|
+
recommendedFirstCalls:
|
|
386
|
+
state.mode === 'enterprise'
|
|
387
|
+
? ['enterprise_session_brief', 'find_perks', 'get_adoption_rank']
|
|
388
|
+
: ['build_brief', 'find_perks', 'get_adoption_rank'],
|
|
389
|
+
setup: {
|
|
390
|
+
builder: [
|
|
391
|
+
`Open ${state.baseUrl}/projects/new?source=agent_pack`,
|
|
392
|
+
'Generate a scoped workflow token.',
|
|
393
|
+
'Set DAI_TOKEN in the MCP config for private saved stack, Build Packet, and proof workflow context.',
|
|
394
|
+
],
|
|
395
|
+
enterprise: [
|
|
396
|
+
`Open ${state.baseUrl}/dashboard?mode=enterprise`,
|
|
397
|
+
'Use enterprise_session_brief with explicit session metrics, budget, and ICP segment splits for immediate analysis.',
|
|
398
|
+
'Use draft_adoption_session, draft_icp_invite_batch, draft_partner_invite, and draft_adoption_proof_motion to prepare the motion for approval.',
|
|
399
|
+
'Set DAI_MODE=enterprise and DAI_ORGANIZATION_ID or DAI_ENTERPRISE_PROFILE so the agent uses enterprise language and offer strategy.',
|
|
400
|
+
'Add DAI_ENTERPRISE_TOKEN only when a scoped enterprise MCP credential is available.',
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function takeItems(value, key) {
|
|
407
|
+
return Array.isArray(value?.[key]) ? value[key] : []
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function absoluteOrSiteUrl(value, baseUrl) {
|
|
411
|
+
if (!value) return undefined
|
|
412
|
+
const text = String(value)
|
|
413
|
+
if (/^https?:\/\//i.test(text)) return text
|
|
414
|
+
return `${baseUrl}${text.startsWith('/') ? text : `/${text}`}`
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function compactToolResult(result, baseUrl) {
|
|
418
|
+
const tool = result?.tool ?? {}
|
|
419
|
+
return {
|
|
420
|
+
name: tool.name,
|
|
421
|
+
slug: tool.slug,
|
|
422
|
+
category: tool.categoryName ?? tool.categorySlug,
|
|
423
|
+
href: absoluteOrSiteUrl(tool.href, baseUrl),
|
|
424
|
+
docsUrl: tool.docsUrl,
|
|
425
|
+
websiteUrl: tool.websiteUrl,
|
|
426
|
+
pricingModel: tool.pricingModel,
|
|
427
|
+
openSource: Boolean(tool.openSource),
|
|
428
|
+
adoptionIndex: tool.adoptionIndex,
|
|
429
|
+
verifiedAdoptions: tool.verifiedAdoptions,
|
|
430
|
+
why: tool.why,
|
|
431
|
+
tradeoff: tool.tradeoff,
|
|
432
|
+
score: result?.score,
|
|
433
|
+
reasons: Array.isArray(result?.reasons) ? result.reasons.slice(0, 4) : [],
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function compactPerk(perk, baseUrl) {
|
|
438
|
+
return {
|
|
439
|
+
name: perk?.name,
|
|
440
|
+
description: perk?.description,
|
|
441
|
+
type: perk?.perkType,
|
|
442
|
+
tool: perk?.tool?.name,
|
|
443
|
+
category: perk?.tool?.category?.name ?? perk?.tool?.category?.slug,
|
|
444
|
+
organization: perk?.organization?.name,
|
|
445
|
+
valueCents: perk?.unitValueCents ?? perk?.pricingSummary?.perkValueCents,
|
|
446
|
+
scarcity: perk?.scarcityText,
|
|
447
|
+
eligibility: perk?.eligibility?.reason,
|
|
448
|
+
claimUrl: perk?.externalClaimUrl ?? (perk?.id ? `${baseUrl}/perks?perk=${encodeURIComponent(perk.id)}` : undefined),
|
|
449
|
+
source: perk?.source,
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function compactAdoptionRow(row, baseUrl) {
|
|
454
|
+
return {
|
|
455
|
+
tool: row?.tool?.name,
|
|
456
|
+
slug: row?.tool?.slug,
|
|
457
|
+
category: row?.tool?.category,
|
|
458
|
+
adoptionScore: row?.adoptionScore,
|
|
459
|
+
agentRankScore: row?.agentRankScore,
|
|
460
|
+
trustScore: row?.trustScore,
|
|
461
|
+
verifiedAdoptions: row?.verifiedAdoptions,
|
|
462
|
+
uniqueDevelopers: row?.uniqueDevelopers,
|
|
463
|
+
retentionRate: row?.retentionRate,
|
|
464
|
+
trustState: row?.trustState,
|
|
465
|
+
categoryRank: row?.categoryRank,
|
|
466
|
+
profileUrl: row?.badgeUrls?.profile ? `${baseUrl}${row.badgeUrls.profile}` : undefined,
|
|
467
|
+
methodologyUrl: row?.methodologyUrl ? `${baseUrl}${row.methodologyUrl}` : undefined,
|
|
468
|
+
rankingPolicy: row?.rankingPolicy,
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function uniqueBySlug(items = []) {
|
|
473
|
+
const seen = new Set()
|
|
474
|
+
const unique = []
|
|
475
|
+
for (const item of items) {
|
|
476
|
+
const key = item.slug || item.name
|
|
477
|
+
if (!key || seen.has(key)) continue
|
|
478
|
+
seen.add(key)
|
|
479
|
+
unique.push(item)
|
|
480
|
+
}
|
|
481
|
+
return unique
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function normalizedText(value = '') {
|
|
485
|
+
return String(value || '').trim().toLowerCase()
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function matchesAny(text, patterns = []) {
|
|
489
|
+
const haystack = normalizedText(text)
|
|
490
|
+
return patterns.some((pattern) => haystack.includes(pattern))
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function isCustomerAgentIntent(intent, useCase = '') {
|
|
494
|
+
return /customer|support|ticket|helpdesk|intercom|zendesk|agent|assistant|chatbot/i.test(`${intent || ''} ${useCase || ''}`)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function firstCandidate(candidates, patterns) {
|
|
498
|
+
for (const pattern of patterns) {
|
|
499
|
+
const candidate = candidates.find((item) => {
|
|
500
|
+
const haystack = `${item.name || ''} ${item.slug || ''} ${item.category || ''}`
|
|
501
|
+
return matchesAny(haystack, [pattern])
|
|
502
|
+
})
|
|
503
|
+
if (candidate) return candidate
|
|
504
|
+
}
|
|
505
|
+
return null
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function compactApiChoice(role, candidate, fallback) {
|
|
509
|
+
const source = candidate ? 'tokensand_search' : 'playbook_fallback'
|
|
510
|
+
return {
|
|
511
|
+
role,
|
|
512
|
+
name: candidate?.name || fallback.name,
|
|
513
|
+
slug: candidate?.slug || fallback.slug,
|
|
514
|
+
category: candidate?.category || fallback.category,
|
|
515
|
+
pricingModel: candidate?.pricingModel || fallback.pricingModel,
|
|
516
|
+
docsUrl: candidate?.docsUrl || fallback.docsUrl,
|
|
517
|
+
why: candidate?.why || fallback.why,
|
|
518
|
+
tradeoff: candidate?.tradeoff || fallback.tradeoff,
|
|
519
|
+
source,
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function buildApiPlan(intent, stackCandidates, useCase = '') {
|
|
524
|
+
const customerAgent = isCustomerAgentIntent(intent, useCase)
|
|
525
|
+
const supportSystem = firstCandidate(stackCandidates, ['intercom', 'zendesk', 'freshdesk'])
|
|
526
|
+
const channel = firstCandidate(stackCandidates, ['slack', 'discord', 'teams'])
|
|
527
|
+
const runtime = firstCandidate(stackCandidates, ['mastra', 'fastmcp', 'langchain', 'llamaindex', 'cline', 'openai'])
|
|
528
|
+
const memory = firstCandidate(stackCandidates, ['mem0', 'pinecone', 'redis', 'neon', 'postgres'])
|
|
529
|
+
|
|
530
|
+
const customerAgentChoices = [
|
|
531
|
+
compactApiChoice('support system of record', supportSystem, {
|
|
532
|
+
name: 'Intercom or Zendesk API',
|
|
533
|
+
slug: 'support-system-api',
|
|
534
|
+
category: 'Support Channels',
|
|
535
|
+
pricingModel: 'PAID',
|
|
536
|
+
docsUrl: 'Use the provider API docs for the support system already used by the customer.',
|
|
537
|
+
why: 'A customer agent needs tickets, conversations, contacts, and escalation state from the existing support system.',
|
|
538
|
+
tradeoff: 'Do not add a paid support-suite dependency unless it is already the system of record.',
|
|
539
|
+
}),
|
|
540
|
+
compactApiChoice('handoff and internal escalation channel', channel, {
|
|
541
|
+
name: 'Slack API',
|
|
542
|
+
slug: 'slack-api',
|
|
543
|
+
category: 'Support Channels',
|
|
544
|
+
pricingModel: 'FREE',
|
|
545
|
+
docsUrl: 'https://api.slack.com/docs',
|
|
546
|
+
why: 'Most B2B support agents need human handoff, triage, and account-team escalation.',
|
|
547
|
+
tradeoff: 'OAuth scopes and workspace permissions need review before production.',
|
|
548
|
+
}),
|
|
549
|
+
compactApiChoice('agent runtime and tool orchestration', runtime, {
|
|
550
|
+
name: 'Mastra or FastMCP',
|
|
551
|
+
slug: 'agent-runtime',
|
|
552
|
+
category: 'AI Agents',
|
|
553
|
+
pricingModel: 'OPEN_SOURCE',
|
|
554
|
+
docsUrl: 'Use the selected runtime docs.',
|
|
555
|
+
why: 'Keep the customer-agent loop, tools, memory, evals, and handoff logic explicit in code.',
|
|
556
|
+
tradeoff: 'Open-source runtimes reduce vendor cost but add deployment and maintenance ownership.',
|
|
557
|
+
}),
|
|
558
|
+
compactApiChoice('memory, retrieval, or customer context store', memory, {
|
|
559
|
+
name: 'Mem0, Redis, Pinecone, or Postgres',
|
|
560
|
+
slug: 'agent-memory-retrieval',
|
|
561
|
+
category: 'AI Agents / Databases',
|
|
562
|
+
pricingModel: 'FREEMIUM',
|
|
563
|
+
docsUrl: 'Use the selected provider docs.',
|
|
564
|
+
why: 'Customer agents need durable user/account context and retrieval over support knowledge.',
|
|
565
|
+
tradeoff: 'Keep PII boundaries explicit; do not store raw customer data without retention and deletion rules.',
|
|
566
|
+
}),
|
|
567
|
+
]
|
|
568
|
+
|
|
569
|
+
const generalChoices = stackCandidates.slice(0, 4).map((candidate, index) => compactApiChoice(
|
|
570
|
+
index === 0 ? 'primary API' : `supporting API ${index + 1}`,
|
|
571
|
+
candidate,
|
|
572
|
+
{
|
|
573
|
+
name: candidate?.name || 'Selected API',
|
|
574
|
+
slug: candidate?.slug || 'selected-api',
|
|
575
|
+
category: candidate?.category || 'Developer tool',
|
|
576
|
+
pricingModel: candidate?.pricingModel || 'UNKNOWN',
|
|
577
|
+
docsUrl: candidate?.docsUrl,
|
|
578
|
+
why: candidate?.why || 'Selected from Tokens& stack search.',
|
|
579
|
+
tradeoff: candidate?.tradeoff || 'Validate cost, auth, and operational fit before implementation.',
|
|
580
|
+
},
|
|
581
|
+
))
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
useCase: customerAgent ? 'customer_agent' : 'general_builder',
|
|
585
|
+
bestApis: customerAgent ? customerAgentChoices : generalChoices,
|
|
586
|
+
implementationOrder: customerAgent
|
|
587
|
+
? [
|
|
588
|
+
'Pick the support system of record first: Intercom/Zendesk only if the customer already lives there.',
|
|
589
|
+
'Add Slack or an internal handoff channel before autonomous resolution.',
|
|
590
|
+
'Choose the runtime/orchestration layer and define tools, auth scopes, and evals.',
|
|
591
|
+
'Add memory/retrieval only after PII, retention, and deletion boundaries are explicit.',
|
|
592
|
+
]
|
|
593
|
+
: [
|
|
594
|
+
'Start with the primary API that owns the core workflow.',
|
|
595
|
+
'Add the cheapest support API needed for the first demo.',
|
|
596
|
+
'Defer extra integrations until the first user proof exists.',
|
|
597
|
+
],
|
|
598
|
+
missingData:
|
|
599
|
+
customerAgent && !supportSystem
|
|
600
|
+
? 'Tokens& did not find the customer support system in the top candidates; ask the user whether the target customer uses Intercom, Zendesk, HubSpot, Salesforce, or a custom inbox.'
|
|
601
|
+
: null,
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function creditValueLabel(perk) {
|
|
606
|
+
if (perk.valueCents) return `$${Math.round(perk.valueCents / 100).toLocaleString()}`
|
|
607
|
+
return 'amount varies or not listed'
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function buildCreditPlan(matchedPerks, intent, useCase = '') {
|
|
611
|
+
const creditLike = matchedPerks.filter((perk) => {
|
|
612
|
+
const haystack = `${perk.name || ''} ${perk.type || ''} ${perk.scarcity || ''}`.toLowerCase()
|
|
613
|
+
return /credit|free|quota|startup|starter|bundle|discount|perk/.test(haystack)
|
|
614
|
+
})
|
|
615
|
+
const customerAgent = isCustomerAgentIntent(intent, useCase)
|
|
616
|
+
const preferred = creditLike.filter((perk) => {
|
|
617
|
+
const haystack = `${perk.tool || ''} ${perk.category || ''} ${perk.name || ''}`.toLowerCase()
|
|
618
|
+
return customerAgent
|
|
619
|
+
? /vercel|cloudflare|neon|pinecone|redis|tinyfish|mongodb|database|vector|agent|infrastructure|developer-platform/.test(haystack)
|
|
620
|
+
: true
|
|
621
|
+
})
|
|
622
|
+
const availableCredits = (preferred.length ? preferred : creditLike).slice(0, 6).map((perk) => ({
|
|
623
|
+
name: perk.name,
|
|
624
|
+
tool: perk.tool,
|
|
625
|
+
type: perk.type,
|
|
626
|
+
category: perk.category,
|
|
627
|
+
value: creditValueLabel(perk),
|
|
628
|
+
claimUrl: perk.claimUrl,
|
|
629
|
+
source: perk.source,
|
|
630
|
+
caveat: perk.scarcity || 'Verify current provider terms before relying on the offer.',
|
|
631
|
+
}))
|
|
632
|
+
|
|
633
|
+
return {
|
|
634
|
+
source: 'Tokens& public perks API',
|
|
635
|
+
availableCredits,
|
|
636
|
+
recommendation: availableCredits.length
|
|
637
|
+
? `Claim or verify ${availableCredits[0].name} first; use credits to reduce hosting/database/vector or agent-runtime cost before paying for traffic.`
|
|
638
|
+
: 'No matching public credits were found in Tokens& for this intent; treat this as a supply gap to seed.',
|
|
639
|
+
caveat: 'Perks are opportunities, not guaranteed entitlements. Provider eligibility, terms, and current availability must be verified before budgeting.',
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function buildCostPlan(intent, stackCandidates, creditPlan, useCase = '') {
|
|
644
|
+
const customerAgent = isCustomerAgentIntent(intent, useCase)
|
|
645
|
+
const paid = stackCandidates.filter((candidate) => /PAID|ENTERPRISE/i.test(String(candidate.pricingModel || '')))
|
|
646
|
+
const freeish = stackCandidates.filter((candidate) => /FREE|OPEN_SOURCE|FREEMIUM/i.test(String(candidate.pricingModel || '')))
|
|
647
|
+
return {
|
|
648
|
+
source: 'Tokens& pricing labels plus selected provider docs; live metered API pricing is not pulled into this public MCP call.',
|
|
649
|
+
costDrivers: customerAgent
|
|
650
|
+
? [
|
|
651
|
+
'LLM/model calls per conversation and tool call.',
|
|
652
|
+
'Support-system seats or API access if using Intercom/Zendesk.',
|
|
653
|
+
'Vector/database storage and retrieval for knowledge/customer context.',
|
|
654
|
+
'Hosting, background jobs, observability, and human handoff volume.',
|
|
655
|
+
]
|
|
656
|
+
: [
|
|
657
|
+
'Paid APIs in the selected stack.',
|
|
658
|
+
'Hosting, database, storage, and background jobs.',
|
|
659
|
+
'Usage-based model, search, or workflow execution costs.',
|
|
660
|
+
],
|
|
661
|
+
lowCostPath: customerAgent
|
|
662
|
+
? 'Start with an open-source runtime, Slack handoff, existing docs/FAQ data, and credits for hosting/database/vector storage. Add Intercom/Zendesk only when the customer already uses that system.'
|
|
663
|
+
: 'Start with the free/open-source or freemium candidates, claim credits, and defer paid APIs until the first proof path is working.',
|
|
664
|
+
paidSurfaces: paid.map((candidate) => ({
|
|
665
|
+
name: candidate.name,
|
|
666
|
+
pricingModel: candidate.pricingModel,
|
|
667
|
+
tradeoff: candidate.tradeoff,
|
|
668
|
+
docsUrl: candidate.docsUrl,
|
|
669
|
+
})),
|
|
670
|
+
freeOrCreditSurfaces: freeish.map((candidate) => ({
|
|
671
|
+
name: candidate.name,
|
|
672
|
+
pricingModel: candidate.pricingModel,
|
|
673
|
+
docsUrl: candidate.docsUrl,
|
|
674
|
+
})).slice(0, 6),
|
|
675
|
+
creditsAvailable: creditPlan.availableCredits.length,
|
|
676
|
+
budgetGuardrail:
|
|
677
|
+
'Before production, set per-user and per-account usage caps, log model/tool-call cost by workflow, and define a kill condition for cost per resolved ticket or completed task.',
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function buildDeveloperDashboardSync(state) {
|
|
682
|
+
return {
|
|
683
|
+
persistedToDashboard: false,
|
|
684
|
+
reason:
|
|
685
|
+
'build_brief is public decision support. It does not create or update Tokens& project records by itself.',
|
|
686
|
+
currentMode: state.mode,
|
|
687
|
+
hasDeveloperWorkflowToken: Boolean(state.token),
|
|
688
|
+
productAppearsInDashboardWhen:
|
|
689
|
+
'A signed-in builder configures DAI_TOKEN, calls draft_project from the repo, reviews the inferred project, then calls publish_project with explicit confirmation.',
|
|
690
|
+
dashboardLinks: {
|
|
691
|
+
install: `${state.baseUrl}/agents/install`,
|
|
692
|
+
projects: `${state.baseUrl}/dashboard/projects`,
|
|
693
|
+
newProject: `${state.baseUrl}/projects/new?source=agent_pack`,
|
|
694
|
+
publicPerks: `${state.baseUrl}/perks?public=1`,
|
|
695
|
+
},
|
|
696
|
+
nextSyncAction: state.token
|
|
697
|
+
? 'Run draft_project from the repo, review the draft, then run publish_project with confirm: true after human approval.'
|
|
698
|
+
: 'Generate a scoped workflow token from Tokens&, set DAI_TOKEN in Codex/Cursor, then run draft_project and publish_project after approval.',
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async function publicBuildBrief(args, state) {
|
|
703
|
+
const intent = String(args.intent ?? args.query ?? '').trim()
|
|
704
|
+
if (!intent) throw new Error('build_brief requires intent')
|
|
705
|
+
const useCase = String(args.useCase ?? '').trim()
|
|
706
|
+
const customerAgentIntent = isCustomerAgentIntent(intent, useCase)
|
|
707
|
+
const searchIntent = customerAgentIntent && !/intercom|zendesk|slack|support channel|support api/i.test(intent)
|
|
708
|
+
? `${intent} support channel api Intercom Zendesk Slack`
|
|
709
|
+
: intent
|
|
710
|
+
const limit = Math.max(1, Math.min(Number(args.limit ?? 5) || 5, 10))
|
|
711
|
+
const period = ['7d', '30d', '90d'].includes(args.period) ? args.period : '30d'
|
|
712
|
+
|
|
713
|
+
const searchParams = new URLSearchParams({ q: searchIntent, limit: String(limit) })
|
|
714
|
+
if (args.category || args.categorySlug) searchParams.set('category', String(args.category ?? args.categorySlug))
|
|
715
|
+
if (args.pricing) searchParams.set('pricing', String(args.pricing))
|
|
716
|
+
if (args.openSource != null) searchParams.set('openSource', String(Boolean(args.openSource)))
|
|
717
|
+
if (args.enterpriseReady != null) searchParams.set('enterpriseReady', String(Boolean(args.enterpriseReady)))
|
|
718
|
+
|
|
719
|
+
const perkLookupLimit = Math.max(limit, 10)
|
|
720
|
+
const perkParams = new URLSearchParams({ limit: String(perkLookupLimit) })
|
|
721
|
+
if (args.categorySlug) perkParams.set('categorySlug', String(args.categorySlug))
|
|
722
|
+
|
|
723
|
+
const rankParams = new URLSearchParams({ period, limit: String(limit) })
|
|
724
|
+
if (args.categorySlug) rankParams.set('category', String(args.categorySlug))
|
|
725
|
+
|
|
726
|
+
const supportSearchParams = new URLSearchParams({ q: 'customer support agent api', limit: String(limit) })
|
|
727
|
+
const [search, supportSearch, perks, adoption] = await Promise.all([
|
|
728
|
+
workflowApi(`/api/build/search?${searchParams.toString()}`, { baseUrl: state.baseUrl }),
|
|
729
|
+
customerAgentIntent
|
|
730
|
+
? workflowApi(`/api/build/search?${supportSearchParams.toString()}`, { baseUrl: state.baseUrl }).catch(() => ({ results: [] }))
|
|
731
|
+
: Promise.resolve({ results: [] }),
|
|
732
|
+
workflowApi(`/api/perks?${perkParams.toString()}`, { baseUrl: state.baseUrl }).catch((error) => ({
|
|
733
|
+
error: error instanceof Error ? error.message : 'perk lookup failed',
|
|
734
|
+
perks: [],
|
|
735
|
+
})),
|
|
736
|
+
workflowApi(`/api/v1/adoption?${rankParams.toString()}`, { baseUrl: state.baseUrl }).catch((error) => ({
|
|
737
|
+
error: error instanceof Error ? error.message : 'adoption lookup failed',
|
|
738
|
+
rows: [],
|
|
739
|
+
})),
|
|
740
|
+
])
|
|
741
|
+
|
|
742
|
+
const supportCandidates = takeItems(supportSearch, 'results').map((item) => compactToolResult(item, state.baseUrl))
|
|
743
|
+
const generalCandidates = takeItems(search, 'results').map((item) => compactToolResult(item, state.baseUrl))
|
|
744
|
+
const stackCandidates = uniqueBySlug([...supportCandidates, ...generalCandidates]).slice(0, limit)
|
|
745
|
+
const rawPerks = takeItems(perks, 'perks')
|
|
746
|
+
const requestedCategorySlug = args.categorySlug ? String(args.categorySlug) : ''
|
|
747
|
+
const categoryFilteredPerks = requestedCategorySlug
|
|
748
|
+
? rawPerks.filter((perk) => {
|
|
749
|
+
const categorySlug = perk?.tool?.category?.slug
|
|
750
|
+
return categorySlug === requestedCategorySlug || categorySlug === 'developer-program'
|
|
751
|
+
})
|
|
752
|
+
: rawPerks
|
|
753
|
+
const matchedPerks = categoryFilteredPerks.slice(0, perkLookupLimit).map((item) => compactPerk(item, state.baseUrl))
|
|
754
|
+
const adoptionEvidence = takeItems(adoption, 'rows').slice(0, limit).map((item) => compactAdoptionRow(item, state.baseUrl))
|
|
755
|
+
const apiPlan = buildApiPlan(intent, stackCandidates, useCase)
|
|
756
|
+
const creditPlan = buildCreditPlan(matchedPerks, intent, useCase)
|
|
757
|
+
const costPlan = buildCostPlan(intent, stackCandidates, creditPlan, useCase)
|
|
758
|
+
const dashboardSync = buildDeveloperDashboardSync(state)
|
|
759
|
+
|
|
760
|
+
return {
|
|
761
|
+
intent,
|
|
762
|
+
useCase: useCase || apiPlan.useCase,
|
|
763
|
+
stackCandidates,
|
|
764
|
+
matchedPerks,
|
|
765
|
+
adoptionEvidence,
|
|
766
|
+
apiPlan,
|
|
767
|
+
creditPlan,
|
|
768
|
+
costPlan,
|
|
769
|
+
dashboardSync,
|
|
770
|
+
recommendation:
|
|
771
|
+
stackCandidates.length > 0
|
|
772
|
+
? `Start with ${stackCandidates[0].name}; compare against ${stackCandidates.slice(1, 3).map((tool) => tool.name).filter(Boolean).join(' and ') || 'one focused alternative'} before implementation.`
|
|
773
|
+
: 'No stack candidate found yet. Ask a narrower build intent or use Tokens& workbench.',
|
|
774
|
+
nextActions: [
|
|
775
|
+
apiPlan.bestApis?.length ? `Use ${apiPlan.bestApis[0].name} for ${apiPlan.bestApis[0].role}; then follow the implementation order in apiPlan.` : 'Pick the smallest API set that reaches first user proof.',
|
|
776
|
+
creditPlan.availableCredits.length ? `Verify ${creditPlan.availableCredits[0].name} before spending cash.` : 'Seed or find a relevant credit/perk before scaling the build.',
|
|
777
|
+
costPlan.budgetGuardrail,
|
|
778
|
+
'Pick one stack candidate and ask the coding agent for the smallest shippable implementation plan.',
|
|
779
|
+
matchedPerks.length ? 'Claim or verify any matching perk before relying on the credit/value.' : 'No matching perk supply found; this is a supply gap for Tokens& to fill.',
|
|
780
|
+
'Use get_build_packet after adding DAI_TOKEN for private saved-stack context.',
|
|
781
|
+
'Use draft_project after shipping to prepare public proof; publish only after explicit human approval.',
|
|
782
|
+
],
|
|
783
|
+
safety: {
|
|
784
|
+
publicOnly: !state.token,
|
|
785
|
+
privateContextRequires: 'DAI_TOKEN',
|
|
786
|
+
approvalBoundary: 'Public proof publishing, offer claims, and external writes remain human-approved.',
|
|
787
|
+
},
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function numberValue(value) {
|
|
792
|
+
const number = Number(value)
|
|
793
|
+
return Number.isFinite(number) && number > 0 ? number : 0
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function optionalNumber(value) {
|
|
797
|
+
const number = Number(value)
|
|
798
|
+
return Number.isFinite(number) && number > 0 ? number : null
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function moneyPer(total, count) {
|
|
802
|
+
if (!total || !count) return null
|
|
803
|
+
return Math.round((total / count) * 100) / 100
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function percentage(numerator, denominator) {
|
|
807
|
+
if (!denominator) return 0
|
|
808
|
+
return Math.round((numerator / denominator) * 100)
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function compactSession(input = {}, fallbackName = 'session') {
|
|
812
|
+
const registrations = numberValue(input.registrations ?? input.signups ?? input.registered)
|
|
813
|
+
const attended = numberValue(input.attended ?? input.attendees ?? input.checkedIn)
|
|
814
|
+
const builders = numberValue(input.builders ?? input.builderCount ?? input.started)
|
|
815
|
+
const activated = numberValue(
|
|
816
|
+
input.activated ?? input.firstSuccess ?? input.firstSuccesses ?? input.first_success ?? input.first_successes,
|
|
817
|
+
)
|
|
818
|
+
const retained = numberValue(input.retained ?? input.retainedBuilders ?? input.returned ?? input.retainedUsage)
|
|
819
|
+
const proofCount = numberValue(
|
|
820
|
+
input.proofCount ?? input.proofProjects ?? input.proofProjectCount ?? input.proofs ?? input.projects,
|
|
821
|
+
)
|
|
822
|
+
return {
|
|
823
|
+
name: String(input.name || input.title || fallbackName),
|
|
824
|
+
date: input.date ? String(input.date) : undefined,
|
|
825
|
+
offer: input.offer ? String(input.offer) : undefined,
|
|
826
|
+
notes: input.notes ? String(input.notes) : undefined,
|
|
827
|
+
costUsd: optionalNumber(input.costUsd ?? input.budgetUsd ?? input.spendUsd ?? input.cost ?? input.spend),
|
|
828
|
+
icpSegment: input.icpSegment ? String(input.icpSegment) : undefined,
|
|
829
|
+
channel: input.channel ? String(input.channel) : undefined,
|
|
830
|
+
metrics: {
|
|
831
|
+
registrations,
|
|
832
|
+
attended,
|
|
833
|
+
builders,
|
|
834
|
+
activated,
|
|
835
|
+
retained,
|
|
836
|
+
proofCount,
|
|
837
|
+
},
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function summarizeSessions(sessions = []) {
|
|
842
|
+
const totals = sessions.reduce(
|
|
843
|
+
(acc, session) => {
|
|
844
|
+
const metrics = session.metrics ?? {}
|
|
845
|
+
acc.registrations += numberValue(metrics.registrations)
|
|
846
|
+
acc.attended += numberValue(metrics.attended)
|
|
847
|
+
acc.builders += numberValue(metrics.builders)
|
|
848
|
+
acc.activated += numberValue(metrics.activated)
|
|
849
|
+
acc.retained += numberValue(metrics.retained)
|
|
850
|
+
acc.proofCount += numberValue(metrics.proofCount)
|
|
851
|
+
return acc
|
|
852
|
+
},
|
|
853
|
+
{ registrations: 0, attended: 0, builders: 0, activated: 0, retained: 0, proofCount: 0 },
|
|
854
|
+
)
|
|
855
|
+
return {
|
|
856
|
+
totals,
|
|
857
|
+
rates: {
|
|
858
|
+
attendanceRate: percentage(totals.attended, totals.registrations),
|
|
859
|
+
builderStartRate: percentage(totals.builders, totals.attended),
|
|
860
|
+
activationRate: percentage(totals.activated, totals.builders),
|
|
861
|
+
retentionRate: percentage(totals.retained, totals.activated),
|
|
862
|
+
proofRate: percentage(totals.proofCount, totals.activated),
|
|
863
|
+
},
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function sessionBottleneck(summary) {
|
|
868
|
+
const rates = summary.rates
|
|
869
|
+
const candidates = [
|
|
870
|
+
{
|
|
871
|
+
key: 'attendance',
|
|
872
|
+
rate: rates.attendanceRate,
|
|
873
|
+
threshold: 55,
|
|
874
|
+
label: 'registration to attendance',
|
|
875
|
+
action: 'Add calendar holds, reminder proof, and a concrete build outcome before the session starts.',
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
key: 'builder_start',
|
|
879
|
+
rate: rates.builderStartRate,
|
|
880
|
+
threshold: 45,
|
|
881
|
+
label: 'attendance to builder start',
|
|
882
|
+
action: 'Reduce setup friction: one command starter, visible API key path, and a 15-minute first-success script.',
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
key: 'activation',
|
|
886
|
+
rate: rates.activationRate,
|
|
887
|
+
threshold: 50,
|
|
888
|
+
label: 'builder start to first success',
|
|
889
|
+
action: 'Put agents on the happy path: Build Packet, working template, env checklist, and immediate debugging help.',
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
key: 'retention',
|
|
893
|
+
rate: rates.retentionRate,
|
|
894
|
+
threshold: 40,
|
|
895
|
+
label: 'first success to retained usage',
|
|
896
|
+
action: 'Turn the build into an operating workflow: follow-up challenge, production checklist, and account-owner handoff.',
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
key: 'proof',
|
|
900
|
+
rate: rates.proofRate,
|
|
901
|
+
threshold: 30,
|
|
902
|
+
label: 'first success to public proof',
|
|
903
|
+
action: 'Make proof publishing cheap: repo/demo checklist, prefilled project card, and approval-gated claim text.',
|
|
904
|
+
},
|
|
905
|
+
]
|
|
906
|
+
return candidates
|
|
907
|
+
.filter((candidate) => candidate.rate < candidate.threshold)
|
|
908
|
+
.sort((a, b) => a.rate - b.rate)[0] || candidates.sort((a, b) => a.rate - b.rate)[0]
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function resolveBudgetUsd(args, profile, sessions) {
|
|
912
|
+
const explicitBudget = optionalNumber(
|
|
913
|
+
args.budgetUsd ?? args.sessionBudgetUsd ?? args.campaignBudgetUsd ?? args.spendUsd ?? args.costUsd,
|
|
914
|
+
)
|
|
915
|
+
if (explicitBudget) {
|
|
916
|
+
return {
|
|
917
|
+
budgetUsd: explicitBudget,
|
|
918
|
+
source: 'user_supplied_budget',
|
|
919
|
+
boardReady: true,
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const sessionCosts = sessions
|
|
923
|
+
.map((session) => optionalNumber(session.costUsd))
|
|
924
|
+
.filter((value) => value != null)
|
|
925
|
+
if (sessionCosts.length) {
|
|
926
|
+
return {
|
|
927
|
+
budgetUsd: sessionCosts.reduce((sum, value) => sum + value, 0),
|
|
928
|
+
source: 'user_supplied_session_costs',
|
|
929
|
+
boardReady: true,
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return {
|
|
933
|
+
budgetUsd: optionalNumber(profile.defaultBudgetUsd) ?? ENTERPRISE_PROFILE_PRESETS.default.defaultBudgetUsd,
|
|
934
|
+
source: 'profile_modelled_budget',
|
|
935
|
+
boardReady: false,
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function buildCostEconomics(args, profile, sessions, summary, bottleneck) {
|
|
940
|
+
const budget = resolveBudgetUsd(args, profile, sessions)
|
|
941
|
+
const totals = summary.totals
|
|
942
|
+
const costs = {
|
|
943
|
+
perRegistration: moneyPer(budget.budgetUsd, totals.registrations),
|
|
944
|
+
perAttendee: moneyPer(budget.budgetUsd, totals.attended),
|
|
945
|
+
perBuilderStart: moneyPer(budget.budgetUsd, totals.builders),
|
|
946
|
+
perActivation: moneyPer(budget.budgetUsd, totals.activated),
|
|
947
|
+
perRetainedBuilder: moneyPer(budget.budgetUsd, totals.retained),
|
|
948
|
+
perProofProject: moneyPer(budget.budgetUsd, totals.proofCount),
|
|
949
|
+
}
|
|
950
|
+
const spendAdviceByBottleneck = {
|
|
951
|
+
attendance: 'Spend less on broad registration volume; spend more on calendar commitment, pre-work, and reminders tied to a visible build outcome.',
|
|
952
|
+
builder_start: 'Shift budget from audience acquisition to one-command setup, starter repos, live debugging, and small credits unlocked at builder start.',
|
|
953
|
+
activation: 'Fund hands-on technical help, Build Packet templates, env setup, and first-success debugging before buying more traffic.',
|
|
954
|
+
retention: 'Fund follow-up clinics and retained-usage rewards; do not scale the event until a second qualifying usage event clears.',
|
|
955
|
+
proof: 'Fund proof packaging: repo/demo checklist, prefilled project cards, featured proof rewards, and account-owner handoff.',
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
return {
|
|
959
|
+
source: budget.source,
|
|
960
|
+
boardReady: budget.boardReady,
|
|
961
|
+
budgetUsd: budget.budgetUsd,
|
|
962
|
+
costs,
|
|
963
|
+
primaryCostMetric:
|
|
964
|
+
bottleneck.key === 'proof'
|
|
965
|
+
? { label: 'cost per proof project', valueUsd: costs.perProofProject }
|
|
966
|
+
: bottleneck.key === 'retention'
|
|
967
|
+
? { label: 'cost per retained builder', valueUsd: costs.perRetainedBuilder }
|
|
968
|
+
: bottleneck.key === 'activation'
|
|
969
|
+
? { label: 'cost per activation', valueUsd: costs.perActivation }
|
|
970
|
+
: { label: 'cost per builder start', valueUsd: costs.perBuilderStart },
|
|
971
|
+
nextBudgetMove: spendAdviceByBottleneck[bottleneck.key] ?? spendAdviceByBottleneck.proof,
|
|
972
|
+
financeBoundary: budget.boardReady
|
|
973
|
+
? 'Budget was supplied in the MCP request; still reconcile against finance-approved campaign spend before board or procurement claims.'
|
|
974
|
+
: 'Budget is modelled from the profile preset; connect finance-approved spend before calling ROI board-ready.',
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function normalizeSegment(row = {}, index = 0) {
|
|
979
|
+
const name = String(row.name || row.label || row.segment || row.icp || row.icpSegment || `segment ${index + 1}`)
|
|
980
|
+
const builders = numberValue(row.builders ?? row.started)
|
|
981
|
+
const activated = numberValue(row.activated ?? row.firstSuccess ?? row.firstSuccesses ?? row.first_success)
|
|
982
|
+
const retained = numberValue(row.retained ?? row.retainedBuilders ?? row.returned)
|
|
983
|
+
const proofCount = numberValue(row.proofCount ?? row.proofProjects ?? row.proofProjectCount ?? row.proofs ?? row.projects)
|
|
984
|
+
const pipelineUsd = numberValue(row.pipelineUsd ?? row.pipeline ?? row.estimatedPipelineUsd)
|
|
985
|
+
const suppliedScore = optionalNumber(row.score)
|
|
986
|
+
const evidenceScore = suppliedScore ?? Math.min(99, Math.round(
|
|
987
|
+
builders * 0.5 +
|
|
988
|
+
activated * 1.5 +
|
|
989
|
+
retained * 3 +
|
|
990
|
+
proofCount * 4 +
|
|
991
|
+
(pipelineUsd ? Math.log10(pipelineUsd + 10) * 6 : 0),
|
|
992
|
+
))
|
|
993
|
+
return {
|
|
994
|
+
name,
|
|
995
|
+
score: evidenceScore,
|
|
996
|
+
builders,
|
|
997
|
+
activated,
|
|
998
|
+
retained,
|
|
999
|
+
proofCount,
|
|
1000
|
+
pipelineUsd: pipelineUsd || null,
|
|
1001
|
+
reason: row.reason || row.why || `${retained} retained builder(s), ${proofCount} proof project(s), ${activated} activation(s).`,
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function buildIcpAnalysis(args, profile, summary) {
|
|
1006
|
+
const suppliedSegments = Array.isArray(args.segments) ? args.segments : Array.isArray(args.icpSegments) ? args.icpSegments : []
|
|
1007
|
+
const rankedSegments = suppliedSegments.length
|
|
1008
|
+
? suppliedSegments.map(normalizeSegment).sort((a, b) => b.score - a.score).slice(0, 5)
|
|
1009
|
+
: (profile.icpSegments ?? ENTERPRISE_PROFILE_PRESETS.default.icpSegments).map((segment) => ({
|
|
1010
|
+
name: segment.name,
|
|
1011
|
+
score: segment.score,
|
|
1012
|
+
reason: segment.why,
|
|
1013
|
+
builders: null,
|
|
1014
|
+
activated: null,
|
|
1015
|
+
retained: null,
|
|
1016
|
+
proofCount: null,
|
|
1017
|
+
pipelineUsd: null,
|
|
1018
|
+
}))
|
|
1019
|
+
const best = rankedSegments[0]
|
|
1020
|
+
return {
|
|
1021
|
+
source: suppliedSegments.length ? 'user_supplied_segment_metrics' : 'profile_modelled_icp',
|
|
1022
|
+
bestSegment: best?.name ?? profile.segment,
|
|
1023
|
+
rankedSegments,
|
|
1024
|
+
test:
|
|
1025
|
+
suppliedSegments.length
|
|
1026
|
+
? 'Double down only if this segment keeps the lead across the next 100 builders with retained usage or proof, not just registrations.'
|
|
1027
|
+
: `Run the next ${profile.company} session with segment tags so the dashboard can compare retained usage and proof by ICP.`,
|
|
1028
|
+
antiIcp:
|
|
1029
|
+
summary.rates.builderStartRate < 45
|
|
1030
|
+
? 'Broad awareness attendees who do not start a build in-session.'
|
|
1031
|
+
: 'Curiosity-only builders without repo/demo evidence or a second usage event.',
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function buildSessionStrategy(profile, summary, bottleneck) {
|
|
1036
|
+
const formats = profile.sessionFormats ?? ENTERPRISE_PROFILE_PRESETS.default.sessionFormats
|
|
1037
|
+
const preferred = bottleneck.key === 'proof'
|
|
1038
|
+
? formats.find((item) => /proof|performance|build/i.test(item.format)) ?? formats[0]
|
|
1039
|
+
: bottleneck.key === 'retention'
|
|
1040
|
+
? formats.find((item) => /clinic|office|retained/i.test(item.format)) ?? formats[1] ?? formats[0]
|
|
1041
|
+
: formats[0]
|
|
1042
|
+
return {
|
|
1043
|
+
recommendedFormat: preferred.format,
|
|
1044
|
+
cadence: preferred.cadence,
|
|
1045
|
+
why: preferred.why,
|
|
1046
|
+
agenda: [
|
|
1047
|
+
'5 min: show the exact proof artifact builders will leave with.',
|
|
1048
|
+
'15 min: one-command setup and API/key path.',
|
|
1049
|
+
'25 min: build the core workflow with Codex/Cursor and Tokens& Build Packet context.',
|
|
1050
|
+
'10 min: submit repo/demo evidence or schedule a follow-up proof review.',
|
|
1051
|
+
],
|
|
1052
|
+
avoid:
|
|
1053
|
+
summary.rates.proofRate < 30
|
|
1054
|
+
? 'Do not buy a bigger conference booth until proof publishing clears 30% of activations.'
|
|
1055
|
+
: 'Do not optimize for attendance without retained-usage instrumentation.',
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function buildDashboardHandoff(state, profileKey) {
|
|
1060
|
+
const profileQuery = profileKey && profileKey !== 'default' ? `profile=${encodeURIComponent(profileKey)}&sample=1` : 'mode=enterprise'
|
|
1061
|
+
const companyProfileQuery = profileKey && profileKey !== 'default'
|
|
1062
|
+
? `mode=company&profile=${encodeURIComponent(profileKey)}`
|
|
1063
|
+
: 'mode=company'
|
|
1064
|
+
const proofPilotQuery = profileKey && profileKey !== 'default'
|
|
1065
|
+
? `profile=${encodeURIComponent(profileKey)}`
|
|
1066
|
+
: 'mode=company'
|
|
1067
|
+
return {
|
|
1068
|
+
syncStatus: {
|
|
1069
|
+
persistedToDashboard: false,
|
|
1070
|
+
reason:
|
|
1071
|
+
'enterprise_session_brief is read-only decision support. It tracks aggregate MCP funnel events when telemetry is enabled, but it does not create projects, offers, exports, or public proof.',
|
|
1072
|
+
projectAppearsInDashboardWhen:
|
|
1073
|
+
'A signed-in builder configures DAI_TOKEN and calls draft_project, then publish_project with explicit confirmation; the project then appears in /dashboard/projects and public proof surfaces.',
|
|
1074
|
+
enterpriseMetricsAppearWhen:
|
|
1075
|
+
'The enterprise workspace connects tracking keys/events or a scoped enterprise MCP credential is configured; browser login is not inherited by Codex/Cursor.',
|
|
1076
|
+
},
|
|
1077
|
+
dashboardLinks: {
|
|
1078
|
+
enterpriseCommandCenter: `${state.baseUrl}/dashboard/adoption?${profileQuery}`,
|
|
1079
|
+
aiOperators: `${state.baseUrl}/dashboard/agents?${companyProfileQuery}`,
|
|
1080
|
+
activationExchange: `${state.baseUrl}/dashboard/campaigns?${profileQuery}`,
|
|
1081
|
+
proofPilot: `${state.baseUrl}/dashboard/proof-pilot?${proofPilotQuery}`,
|
|
1082
|
+
trackingSetup: `${state.baseUrl}/dashboard/integrations/tracking?template=${encodeURIComponent(profileKey || 'default')}`,
|
|
1083
|
+
builderProjects: `${state.baseUrl}/dashboard/projects`,
|
|
1084
|
+
installPage: `${state.baseUrl}/agents/install`,
|
|
1085
|
+
},
|
|
1086
|
+
codexConfig: {
|
|
1087
|
+
publicEnterprisePreview: `DAI_MODE=enterprise DAI_ENTERPRISE_PROFILE=${profileKey || 'default'} npx -y ${CLI_PACKAGE_SPEC} mcp serve --api ${state.baseUrl} --mode enterprise`,
|
|
1088
|
+
privateBuilderSyncRequires: 'DAI_TOKEN from /api/workflow/mcp-config after sign-in.',
|
|
1089
|
+
liveEnterpriseSyncRequires: 'DAI_ENTERPRISE_TOKEN plus an authorized organization scope when enterprise MCP credentials are enabled.',
|
|
1090
|
+
},
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function buildSampleSessions(profile, args = {}) {
|
|
1095
|
+
const benchmark = profile.benchmark ?? ENTERPRISE_PROFILE_PRESETS.default.benchmark
|
|
1096
|
+
return [
|
|
1097
|
+
compactSession(
|
|
1098
|
+
{
|
|
1099
|
+
name: args.sessionName || profile.sessionLabel,
|
|
1100
|
+
registrations: benchmark.registrations,
|
|
1101
|
+
attended: benchmark.attended,
|
|
1102
|
+
builders: benchmark.builders,
|
|
1103
|
+
activated: benchmark.activated,
|
|
1104
|
+
retained: benchmark.retained,
|
|
1105
|
+
proofCount: benchmark.proofCount,
|
|
1106
|
+
offer: profile.offers?.[0],
|
|
1107
|
+
notes: profile.buyerProblem,
|
|
1108
|
+
},
|
|
1109
|
+
profile.sessionLabel,
|
|
1110
|
+
),
|
|
1111
|
+
]
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function hasTopLevelSessionMetrics(args = {}) {
|
|
1115
|
+
return [
|
|
1116
|
+
'registrations',
|
|
1117
|
+
'signups',
|
|
1118
|
+
'registered',
|
|
1119
|
+
'attended',
|
|
1120
|
+
'attendees',
|
|
1121
|
+
'checkedIn',
|
|
1122
|
+
'builders',
|
|
1123
|
+
'builderCount',
|
|
1124
|
+
'started',
|
|
1125
|
+
'activated',
|
|
1126
|
+
'firstSuccess',
|
|
1127
|
+
'firstSuccesses',
|
|
1128
|
+
'first_success',
|
|
1129
|
+
'retained',
|
|
1130
|
+
'retainedBuilders',
|
|
1131
|
+
'retainedUsage',
|
|
1132
|
+
'proofCount',
|
|
1133
|
+
'proofProjects',
|
|
1134
|
+
'proofProjectCount',
|
|
1135
|
+
'proofs',
|
|
1136
|
+
'projects',
|
|
1137
|
+
].some((key) => args[key] != null)
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function normalizeSuppliedSessions(args = {}, profile) {
|
|
1141
|
+
if (Array.isArray(args.sessions) && args.sessions.length) {
|
|
1142
|
+
return args.sessions.map((session, index) => compactSession(session, `session ${index + 1}`))
|
|
1143
|
+
}
|
|
1144
|
+
if (args.session && typeof args.session === 'object' && !Array.isArray(args.session)) {
|
|
1145
|
+
return [compactSession(args.session, args.sessionName || profile.sessionLabel)]
|
|
1146
|
+
}
|
|
1147
|
+
if (hasTopLevelSessionMetrics(args)) {
|
|
1148
|
+
return [compactSession(args, args.sessionName || profile.sessionLabel)]
|
|
1149
|
+
}
|
|
1150
|
+
return []
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function enterpriseSessionBrief(args, state) {
|
|
1154
|
+
const wantsLiveData = args.live === true || args.liveData === true
|
|
1155
|
+
const organizationId = String(args.organizationId || state.organizationId || '').trim()
|
|
1156
|
+
if (wantsLiveData && !state.enterpriseToken) return enterpriseAuthRequiredPayload(state, 'enterprise_session_brief')
|
|
1157
|
+
|
|
1158
|
+
const profileKey = resolveEnterpriseProfile({}, args) || state.enterpriseProfile || 'default'
|
|
1159
|
+
const profile = ENTERPRISE_PROFILE_PRESETS[profileKey] ?? ENTERPRISE_PROFILE_PRESETS.default
|
|
1160
|
+
const suppliedSessions = normalizeSuppliedSessions(args, profile)
|
|
1161
|
+
const sessions = suppliedSessions.length ? suppliedSessions : buildSampleSessions(profile, args)
|
|
1162
|
+
const summary = summarizeSessions(sessions)
|
|
1163
|
+
const bottleneck = sessionBottleneck(summary)
|
|
1164
|
+
const costEconomics = buildCostEconomics(args, profile, sessions, summary, bottleneck)
|
|
1165
|
+
const icpAnalysis = buildIcpAnalysis(args, profile, summary)
|
|
1166
|
+
const sessionStrategy = buildSessionStrategy(profile, summary, bottleneck)
|
|
1167
|
+
const dashboardHandoff = buildDashboardHandoff(state, profileKey)
|
|
1168
|
+
const dataSource = suppliedSessions.length ? 'user_supplied_session_metrics' : 'profile_sample_model'
|
|
1169
|
+
const question = String(args.question || args.prompt || '').trim()
|
|
1170
|
+
const targetBuilder = String(args.targetBuilder || args.audience || profile.segment).trim()
|
|
1171
|
+
|
|
1172
|
+
return {
|
|
1173
|
+
mode: state.mode === 'enterprise' ? 'enterprise' : 'enterprise_preview',
|
|
1174
|
+
dataSource,
|
|
1175
|
+
liveData: state.enterpriseToken
|
|
1176
|
+
? 'enterprise credential configured; this local brief still uses supplied metrics unless a live endpoint is explicitly called'
|
|
1177
|
+
: 'not connected; browser login is not available inside Codex/Cursor MCP',
|
|
1178
|
+
organizationId: organizationId || null,
|
|
1179
|
+
profile: {
|
|
1180
|
+
key: profileKey,
|
|
1181
|
+
company: profile.company,
|
|
1182
|
+
segment: targetBuilder,
|
|
1183
|
+
buyerProblem: profile.buyerProblem,
|
|
1184
|
+
},
|
|
1185
|
+
question: question || 'How should this enterprise convert builder sessions into retained usage and proof?',
|
|
1186
|
+
sessionSummary: summary,
|
|
1187
|
+
sessions,
|
|
1188
|
+
primaryInsight: `${profile.company}: the current constraint is ${bottleneck.label} at ${bottleneck.rate}%. ${bottleneck.action}`,
|
|
1189
|
+
costEconomics,
|
|
1190
|
+
icpAnalysis,
|
|
1191
|
+
sessionStrategy,
|
|
1192
|
+
recommendedOffer: {
|
|
1193
|
+
headline: profile.offers?.[0] || ENTERPRISE_PROFILE_PRESETS.default.offers[0],
|
|
1194
|
+
whyItAttractsBuilders:
|
|
1195
|
+
'It pays builders only when they create a working artifact, which aligns enterprise spend with proof instead of vanity attendance.',
|
|
1196
|
+
claimMechanic:
|
|
1197
|
+
'Gate the perk on a submitted repo/demo plus one first-success event; upgrade to retained-usage rewards after a second qualifying event.',
|
|
1198
|
+
alternatives: (profile.offers ?? ENTERPRISE_PROFILE_PRESETS.default.offers).slice(1, 3),
|
|
1199
|
+
},
|
|
1200
|
+
eventPlan: {
|
|
1201
|
+
minimumEvents: profile.events ?? ENTERPRISE_PROFILE_PRESETS.default.events,
|
|
1202
|
+
proofDefinition:
|
|
1203
|
+
'A project counts as proof only after repo/demo evidence and first-success evidence are source-labeled; public publication remains human-approved.',
|
|
1204
|
+
accountHandoff:
|
|
1205
|
+
'Export or route account-level actions only from an authorized enterprise workspace, not from unauthenticated MCP output.',
|
|
1206
|
+
},
|
|
1207
|
+
dashboardHandoff,
|
|
1208
|
+
nextActions: [
|
|
1209
|
+
bottleneck.action,
|
|
1210
|
+
costEconomics.nextBudgetMove,
|
|
1211
|
+
`Target ${icpAnalysis.bestSegment} first; run the next cohort with explicit segment tags.`,
|
|
1212
|
+
`Run a ${sessionStrategy.recommendedFormat} next; cadence: ${sessionStrategy.cadence}.`,
|
|
1213
|
+
'Seed one concrete builder offer before the next session; remove generic swag or non-build rewards.',
|
|
1214
|
+
'Ask the agent to call find_perks and build_brief from the builder side so the offer appears where developers already work.',
|
|
1215
|
+
'Use dashboardHandoff links to inspect the same profile in Tokens&; use DAI_TOKEN plus draft_project/publish_project when the built product should appear in the builder dashboard.',
|
|
1216
|
+
'Track install page view -> config copied -> MCP first tool call -> Build Packet read -> perk shown -> project proof.',
|
|
1217
|
+
],
|
|
1218
|
+
killConditions: [
|
|
1219
|
+
'Under 55% registration-to-attendance after 100 registrations: positioning or reminder loop is weak.',
|
|
1220
|
+
'Under 45% attendee-to-builder-start after 50 attendees: setup path is too slow.',
|
|
1221
|
+
'Under 30% first-success-to-proof after 30 activations: proof workflow or reward is not compelling.',
|
|
1222
|
+
'Under 40% retained usage after 20 activations: session attracted curiosity, not a real workflow.',
|
|
1223
|
+
],
|
|
1224
|
+
safety: {
|
|
1225
|
+
tenantDataBoundary:
|
|
1226
|
+
'This response does not claim live private enterprise data unless explicit session metrics were supplied by the user or a scoped enterprise credential is used.',
|
|
1227
|
+
approvalBoundary: 'Offer changes, account exports, and public proof publishing stay human-approved.',
|
|
1228
|
+
},
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function listValue(value, fallback = []) {
|
|
1233
|
+
if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean)
|
|
1234
|
+
if (typeof value === 'string' && value.trim()) {
|
|
1235
|
+
return value.split(',').map((item) => item.trim()).filter(Boolean)
|
|
1236
|
+
}
|
|
1237
|
+
return fallback
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function enterpriseMotionProfile(args, state) {
|
|
1241
|
+
const profileKey = resolveEnterpriseProfile({}, args) || state.enterpriseProfile || 'default'
|
|
1242
|
+
const profile = ENTERPRISE_PROFILE_PRESETS[profileKey] ?? ENTERPRISE_PROFILE_PRESETS.default
|
|
1243
|
+
return { profileKey, profile }
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function enterpriseMotionBase(kind, args, state) {
|
|
1247
|
+
const { profileKey, profile } = enterpriseMotionProfile(args, state)
|
|
1248
|
+
const dashboardHandoff = buildDashboardHandoff(state, profileKey)
|
|
1249
|
+
const dashboardConfirmationUrl = enterpriseMotionConfirmationUrl(state, profileKey, kind)
|
|
1250
|
+
return {
|
|
1251
|
+
approvalRequired: true,
|
|
1252
|
+
persisted: false,
|
|
1253
|
+
externalWrites: [],
|
|
1254
|
+
dashboardConfirmationUrl,
|
|
1255
|
+
promise: ENTERPRISE_GROWTH_PROMISE,
|
|
1256
|
+
motionType: kind,
|
|
1257
|
+
detectedMode: state.mode,
|
|
1258
|
+
profile: {
|
|
1259
|
+
key: profileKey,
|
|
1260
|
+
company: String(args.company || profile.company),
|
|
1261
|
+
segment: String(args.icp || args.targetBuilder || args.audience || profile.segment),
|
|
1262
|
+
},
|
|
1263
|
+
dashboardHandoff,
|
|
1264
|
+
boundary: ENTERPRISE_ADOPTION_BOUNDARY,
|
|
1265
|
+
approvalBoundary: ENTERPRISE_ADOPTION_BOUNDARY,
|
|
1266
|
+
nextApprovalActions: [
|
|
1267
|
+
'Review the draft with the enterprise owner.',
|
|
1268
|
+
'Confirm tracking events, ICP filters, partner terms, and proof definition.',
|
|
1269
|
+
`Open ${dashboardConfirmationUrl} to confirm the action in Tokens& before any invite, partner, or proof workflow runs.`,
|
|
1270
|
+
],
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function enterpriseMotionConfirmationUrl(state, profileKey, kind) {
|
|
1275
|
+
const params = new URLSearchParams()
|
|
1276
|
+
params.set('mode', 'enterprise')
|
|
1277
|
+
if (profileKey && profileKey !== 'default') params.set('profile', profileKey)
|
|
1278
|
+
const pathByKind = {
|
|
1279
|
+
tracked_developer_session: '/dashboard/adoption',
|
|
1280
|
+
adoption_session: '/dashboard/adoption',
|
|
1281
|
+
right_builder_invites: '/dashboard/campaigns',
|
|
1282
|
+
icp_invite_batch: '/dashboard/campaigns',
|
|
1283
|
+
partner_company_onboarding: '/dashboard/campaigns',
|
|
1284
|
+
partner_company_invite: '/dashboard/campaigns',
|
|
1285
|
+
adoption_proof: '/dashboard/reports',
|
|
1286
|
+
}
|
|
1287
|
+
const intentByKind = {
|
|
1288
|
+
tracked_developer_session: 'create-motion',
|
|
1289
|
+
adoption_session: 'create-motion',
|
|
1290
|
+
right_builder_invites: 'preview-icp-builders',
|
|
1291
|
+
icp_invite_batch: 'preview-icp-builders',
|
|
1292
|
+
partner_company_onboarding: 'invite-partner-company',
|
|
1293
|
+
partner_company_invite: 'invite-partner-company',
|
|
1294
|
+
adoption_proof: 'approve-proof-tracking',
|
|
1295
|
+
}
|
|
1296
|
+
params.set('intent', intentByKind[kind] ?? 'create-motion')
|
|
1297
|
+
return `${state.baseUrl}${pathByKind[kind] ?? '/dashboard/adoption'}?${params.toString()}`
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function draftEnterpriseSessionMotion(args, state) {
|
|
1301
|
+
const base = enterpriseMotionBase('tracked_developer_session', args, state)
|
|
1302
|
+
const { profile } = enterpriseMotionProfile(args, state)
|
|
1303
|
+
const events = listValue(args.trackingEvents || args.events, profile.events ?? ENTERPRISE_PROFILE_PRESETS.default.events)
|
|
1304
|
+
const sessionFormat = String(args.format || args.sessionFormat || profile.sessionFormats?.[0]?.format || 'builder activation lab')
|
|
1305
|
+
const sessionName = String(args.sessionName || args.title || profile.sessionLabel)
|
|
1306
|
+
const offer = String(args.offer || profile.offers?.[0] || ENTERPRISE_PROFILE_PRESETS.default.offers[0])
|
|
1307
|
+
const sourceProvider = String(args.sourceProvider || args.externalSourceProvider || 'manual').toLowerCase()
|
|
1308
|
+
const partnerCompany = String(args.partnerCompany || args.partner || '')
|
|
1309
|
+
return {
|
|
1310
|
+
...base,
|
|
1311
|
+
draft: {
|
|
1312
|
+
sessionName,
|
|
1313
|
+
format: sessionFormat,
|
|
1314
|
+
audience: base.profile.segment,
|
|
1315
|
+
offer,
|
|
1316
|
+
externalSource: {
|
|
1317
|
+
provider: ['luma', 'partiful', 'eventbrite', 'zoom', 'manual'].includes(sourceProvider) ? sourceProvider : 'manual',
|
|
1318
|
+
sourceUrl: args.sourceUrl ? String(args.sourceUrl) : null,
|
|
1319
|
+
noExternalEventPageCreated: true,
|
|
1320
|
+
noExternalInvitesSent: true,
|
|
1321
|
+
},
|
|
1322
|
+
partnerIntake: {
|
|
1323
|
+
label: 'Invite partner company',
|
|
1324
|
+
partnerCompany: partnerCompany || null,
|
|
1325
|
+
status: 'draft_link_only',
|
|
1326
|
+
},
|
|
1327
|
+
trackingPlan: events.map((eventName) => ({
|
|
1328
|
+
eventName,
|
|
1329
|
+
why: 'Required to connect attendance, first success, retention, and proof back to the motion.',
|
|
1330
|
+
})),
|
|
1331
|
+
successCriteria: {
|
|
1332
|
+
attendanceRate: '>= 55% registration-to-attendance',
|
|
1333
|
+
builderStartRate: '>= 45% attendee-to-builder-start',
|
|
1334
|
+
proofRate: '>= 30% first-success-to-proof',
|
|
1335
|
+
retainedUsageRate: '>= 40% retained usage after activation',
|
|
1336
|
+
},
|
|
1337
|
+
approvalChecklist: [
|
|
1338
|
+
'Confirm the ICP and exclusion rules.',
|
|
1339
|
+
'Confirm the offer can be fulfilled.',
|
|
1340
|
+
'Confirm tracking links/events before invites go out.',
|
|
1341
|
+
'Confirm proof publication stays opt-in.',
|
|
1342
|
+
],
|
|
1343
|
+
},
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function draftBuilderInviteMotion(args, state, motionType = 'right_builder_invites') {
|
|
1348
|
+
const base = enterpriseMotionBase(motionType, args, state)
|
|
1349
|
+
const { profile } = enterpriseMotionProfile(args, state)
|
|
1350
|
+
const segments = listValue(
|
|
1351
|
+
args.segments || args.icpSegments,
|
|
1352
|
+
(profile.icpSegments ?? ENTERPRISE_PROFILE_PRESETS.default.icpSegments).slice(0, 3).map((segment) => segment.name),
|
|
1353
|
+
)
|
|
1354
|
+
const channels = listValue(args.channels, ['Tokens& builder graph', 'Codex/Cursor agent touchpoints', 'project proof history', 'partner/community lists approved by the customer'])
|
|
1355
|
+
const offer = String(args.offer || profile.offers?.[0] || ENTERPRISE_PROFILE_PRESETS.default.offers[0])
|
|
1356
|
+
return {
|
|
1357
|
+
...base,
|
|
1358
|
+
draft: {
|
|
1359
|
+
targetSegments: segments,
|
|
1360
|
+
channels,
|
|
1361
|
+
offer,
|
|
1362
|
+
qualificationFilters: [
|
|
1363
|
+
'Recent build, repo, docs, API, or project-proof signal.',
|
|
1364
|
+
'ICP match based on product workflow, stack, company domain, or role.',
|
|
1365
|
+
'No raw private developer identity export unless the enterprise workspace has lawful basis and approval.',
|
|
1366
|
+
],
|
|
1367
|
+
inviteCopy: {
|
|
1368
|
+
subject: `${base.profile.company}: build with ${offer}`,
|
|
1369
|
+
body:
|
|
1370
|
+
`Invite builders who can produce a working artifact. The ask is one concrete build, one tracked first-success event, and optional proof review.`,
|
|
1371
|
+
},
|
|
1372
|
+
approvalChecklist: [
|
|
1373
|
+
'Approve the source list and suppression rules.',
|
|
1374
|
+
'Approve the offer copy.',
|
|
1375
|
+
'Approve send channel and rate limits.',
|
|
1376
|
+
'Confirm tracking URLs before launch.',
|
|
1377
|
+
],
|
|
1378
|
+
},
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function draftPartnerOnboardingMotion(args, state) {
|
|
1383
|
+
const base = enterpriseMotionBase('partner_company_onboarding', args, state)
|
|
1384
|
+
const partnerCompany = String(args.partnerCompany || args.partner || 'partner company')
|
|
1385
|
+
const sharedAudience = String(args.sharedAudience || args.audience || base.profile.segment)
|
|
1386
|
+
const offer = String(args.offer || `co-build session for ${sharedAudience}`)
|
|
1387
|
+
return {
|
|
1388
|
+
...base,
|
|
1389
|
+
draft: {
|
|
1390
|
+
label: 'Invite partner company',
|
|
1391
|
+
partnerCompany,
|
|
1392
|
+
sharedAudience,
|
|
1393
|
+
partnerPromise:
|
|
1394
|
+
`Co-run one tracked developer session with ${partnerCompany}, invite the overlapping builder segment, and prove which partner surface created activation.`,
|
|
1395
|
+
offer,
|
|
1396
|
+
inviteChecklist: [
|
|
1397
|
+
'Confirm partner owner, logo/name usage, and approval contact.',
|
|
1398
|
+
'Agree on shared ICP and excluded audiences.',
|
|
1399
|
+
'Agree on source tags, UTM naming, and proof definition.',
|
|
1400
|
+
'Queue partner-facing copy for human approval before publishing.',
|
|
1401
|
+
],
|
|
1402
|
+
trackingPlan: [
|
|
1403
|
+
'partner_invited',
|
|
1404
|
+
'partner_session_registered',
|
|
1405
|
+
'partner_session_attended',
|
|
1406
|
+
'partner_tool_used',
|
|
1407
|
+
'partner_proof_submitted',
|
|
1408
|
+
],
|
|
1409
|
+
dataBoundary:
|
|
1410
|
+
'Share aggregate-safe proof and approved account/company context only; do not expose raw private builder identities through MCP output.',
|
|
1411
|
+
},
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function draftAdoptionProofMotion(args, state) {
|
|
1416
|
+
const base = enterpriseMotionBase('adoption_proof', args, state)
|
|
1417
|
+
const evidenceSources = listValue(args.evidenceSources, ['tracked session events', 'repo/demo proof', 'first-success event', 'retained usage event', 'account-domain rollup'])
|
|
1418
|
+
const proofDefinition = String(
|
|
1419
|
+
args.proofDefinition ||
|
|
1420
|
+
'A builder counts as proof after a repo/demo or product artifact is tied to one first-success event and one source-labeled product usage signal.',
|
|
1421
|
+
)
|
|
1422
|
+
return {
|
|
1423
|
+
...base,
|
|
1424
|
+
draft: {
|
|
1425
|
+
proofDefinition,
|
|
1426
|
+
evidenceSources,
|
|
1427
|
+
readoutAudience: String(args.readoutAudience || 'enterprise operator, product lead, revenue owner, and executive sponsor'),
|
|
1428
|
+
metrics: [
|
|
1429
|
+
'registrations',
|
|
1430
|
+
'attended',
|
|
1431
|
+
'builder starts',
|
|
1432
|
+
'first successes',
|
|
1433
|
+
'retained builders',
|
|
1434
|
+
'proof projects',
|
|
1435
|
+
'qualified accounts',
|
|
1436
|
+
'cost per proof project',
|
|
1437
|
+
],
|
|
1438
|
+
publicationBoundary:
|
|
1439
|
+
'Private proof stays inside the enterprise workspace. Public proof requires builder/customer approval and a separate publish action.',
|
|
1440
|
+
approvalChecklist: [
|
|
1441
|
+
'Approve proof definition.',
|
|
1442
|
+
'Approve evidence sources and privacy scope.',
|
|
1443
|
+
'Approve readout audience.',
|
|
1444
|
+
'Approve any public proof or account export separately.',
|
|
1445
|
+
],
|
|
1446
|
+
},
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
function writeMcpMessage(message) {
|
|
1451
|
+
process.stdout.write(`${JSON.stringify(message)}\n`)
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
function parseMcpBuffer(buffer, onMessage) {
|
|
1455
|
+
let rest = buffer
|
|
1456
|
+
while (rest.length > 0) {
|
|
1457
|
+
if (rest.startsWith('Content-Length:')) {
|
|
1458
|
+
const headerEnd = rest.indexOf('\r\n\r\n')
|
|
1459
|
+
if (headerEnd === -1) break
|
|
1460
|
+
const header = rest.slice(0, headerEnd)
|
|
1461
|
+
const match = header.match(/Content-Length:\s*(\d+)/i)
|
|
1462
|
+
if (!match) break
|
|
1463
|
+
const length = Number(match[1])
|
|
1464
|
+
const bodyStart = headerEnd + 4
|
|
1465
|
+
const bodyEnd = bodyStart + length
|
|
1466
|
+
if (rest.length < bodyEnd) break
|
|
1467
|
+
onMessage(JSON.parse(rest.slice(bodyStart, bodyEnd)))
|
|
1468
|
+
rest = rest.slice(bodyEnd)
|
|
1469
|
+
continue
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
const newline = rest.indexOf('\n')
|
|
1473
|
+
if (newline === -1) break
|
|
1474
|
+
const line = rest.slice(0, newline).trim()
|
|
1475
|
+
rest = rest.slice(newline + 1)
|
|
1476
|
+
if (line) onMessage(JSON.parse(line))
|
|
1477
|
+
}
|
|
1478
|
+
return rest
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
async function workflowApi(path, { method = 'GET', body, token, baseUrl }) {
|
|
1482
|
+
return api(path, { method, body, token, baseUrl })
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
async function trackAgentInstallEvent(state, event, metadata = {}) {
|
|
1486
|
+
if (process.env.DAI_DISABLE_TELEMETRY === '1') return
|
|
1487
|
+
const controller = new AbortController()
|
|
1488
|
+
const timeout = setTimeout(() => controller.abort(), TRACKING_TIMEOUT_MS)
|
|
1489
|
+
try {
|
|
1490
|
+
const headers = { 'content-type': 'application/json', accept: 'application/json' }
|
|
1491
|
+
if (state.token) headers.authorization = `Bearer ${state.token}`
|
|
1492
|
+
await fetch(`${state.baseUrl}/api/agent-install/events`, {
|
|
1493
|
+
method: 'POST',
|
|
1494
|
+
headers,
|
|
1495
|
+
signal: controller.signal,
|
|
1496
|
+
body: JSON.stringify({
|
|
1497
|
+
event,
|
|
1498
|
+
source: 'mcp',
|
|
1499
|
+
metadata: {
|
|
1500
|
+
...metadata,
|
|
1501
|
+
cliVersion: VERSION,
|
|
1502
|
+
mcpServer: 'tokensand',
|
|
1503
|
+
mcpMode: state.mode,
|
|
1504
|
+
publicOnly: !state.token,
|
|
1505
|
+
hasEnterpriseContext: Boolean(state.enterpriseToken || state.organizationId || state.enterpriseProfile),
|
|
1506
|
+
},
|
|
1507
|
+
}),
|
|
1508
|
+
})
|
|
1509
|
+
} catch {
|
|
1510
|
+
// Telemetry must never block MCP utility.
|
|
1511
|
+
} finally {
|
|
1512
|
+
clearTimeout(timeout)
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
async function trackMcpToolCall(state, toolName, args = {}) {
|
|
1517
|
+
const metadata = {
|
|
1518
|
+
tool: toolName,
|
|
1519
|
+
hasToken: Boolean(state.token),
|
|
1520
|
+
mcpMode: state.mode,
|
|
1521
|
+
categorySlug: typeof args.categorySlug === 'string' ? args.categorySlug : undefined,
|
|
1522
|
+
category: typeof args.category === 'string' ? args.category : undefined,
|
|
1523
|
+
format: typeof args.format === 'string' ? args.format : undefined,
|
|
1524
|
+
confirmed: Boolean(args.confirm),
|
|
1525
|
+
}
|
|
1526
|
+
if (!state.firstToolCallTracked) {
|
|
1527
|
+
state.firstToolCallTracked = true
|
|
1528
|
+
await trackAgentInstallEvent(state, 'agent_install_mcp_first_tool_call', metadata)
|
|
1529
|
+
}
|
|
1530
|
+
await trackAgentInstallEvent(state, 'agent_install_mcp_tool_call', metadata)
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function workflowToken(config = {}) {
|
|
1534
|
+
return config.token || process.env.DAI_TOKEN || process.env.DAI_WORKFLOW_TOKEN || ''
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
async function readText(path) {
|
|
1538
|
+
return readFile(path, 'utf8').catch(() => '')
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
async function readFirstExisting(paths) {
|
|
1542
|
+
for (const path of paths) {
|
|
1543
|
+
const value = await readText(path)
|
|
1544
|
+
if (value) return value
|
|
1545
|
+
}
|
|
1546
|
+
return ''
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function safeJson(raw) {
|
|
1550
|
+
if (!raw) return null
|
|
1551
|
+
try {
|
|
1552
|
+
return JSON.parse(raw)
|
|
1553
|
+
} catch {
|
|
1554
|
+
return null
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
function dependencyNames(packageJson) {
|
|
1559
|
+
if (!packageJson) return []
|
|
1560
|
+
return Object.keys({
|
|
1561
|
+
...(packageJson.dependencies ?? {}),
|
|
1562
|
+
...(packageJson.devDependencies ?? {}),
|
|
1563
|
+
...(packageJson.peerDependencies ?? {}),
|
|
1564
|
+
...(packageJson.optionalDependencies ?? {}),
|
|
1565
|
+
}).sort()
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
function gitRemoteFromConfig(configText) {
|
|
1569
|
+
const lines = configText.split('\n')
|
|
1570
|
+
let inOrigin = false
|
|
1571
|
+
for (const line of lines) {
|
|
1572
|
+
const trimmed = line.trim()
|
|
1573
|
+
if (/^\[remote\s+"origin"\]/.test(trimmed)) {
|
|
1574
|
+
inOrigin = true
|
|
1575
|
+
continue
|
|
1576
|
+
}
|
|
1577
|
+
if (trimmed.startsWith('[')) inOrigin = false
|
|
1578
|
+
if (inOrigin && trimmed.startsWith('url =')) return trimmed.replace(/^url =\s*/, '').trim()
|
|
1579
|
+
}
|
|
1580
|
+
return ''
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
function normalizeRemoteUrl(value = '') {
|
|
1584
|
+
const trimmed = value.trim()
|
|
1585
|
+
if (!trimmed) return ''
|
|
1586
|
+
const ssh = trimmed.match(/^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/i)
|
|
1587
|
+
if (ssh) return `https://github.com/${ssh[1]}/${ssh[2].replace(/\.git$/i, '')}`
|
|
1588
|
+
const github = trimmed.match(/^https?:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?(?:\/)?$/i)
|
|
1589
|
+
if (github) return `https://github.com/${github[1]}/${github[2].replace(/\.git$/i, '')}`
|
|
1590
|
+
return trimmed
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
function parseProjectFlags(args = []) {
|
|
1594
|
+
const flags = { yes: false }
|
|
1595
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1596
|
+
const arg = args[i]
|
|
1597
|
+
if (arg === '--yes' || arg === '-y') {
|
|
1598
|
+
flags.yes = true
|
|
1599
|
+
continue
|
|
1600
|
+
}
|
|
1601
|
+
if (arg === '--repo') {
|
|
1602
|
+
flags.repoUrl = args[i + 1] || ''
|
|
1603
|
+
i += 1
|
|
1604
|
+
continue
|
|
1605
|
+
}
|
|
1606
|
+
if (arg === '--demo') {
|
|
1607
|
+
flags.demoUrl = args[i + 1] || ''
|
|
1608
|
+
i += 1
|
|
1609
|
+
continue
|
|
1610
|
+
}
|
|
1611
|
+
if (arg === '--image') {
|
|
1612
|
+
flags.imageUrl = args[i + 1] || ''
|
|
1613
|
+
i += 1
|
|
1614
|
+
continue
|
|
1615
|
+
}
|
|
1616
|
+
if (arg === '--title') {
|
|
1617
|
+
flags.title = args[i + 1] || ''
|
|
1618
|
+
i += 1
|
|
1619
|
+
continue
|
|
1620
|
+
}
|
|
1621
|
+
if (arg === '--summary') {
|
|
1622
|
+
flags.summary = args[i + 1] || ''
|
|
1623
|
+
i += 1
|
|
1624
|
+
continue
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
return flags
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
function projectArgsToFlags(args = {}) {
|
|
1631
|
+
return {
|
|
1632
|
+
repoUrl: args.repoUrl || args.repo,
|
|
1633
|
+
demoUrl: args.demoUrl || args.demo,
|
|
1634
|
+
imageUrl: args.imageUrl || args.image,
|
|
1635
|
+
title: args.title,
|
|
1636
|
+
summary: args.summary || args.localSummary,
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
async function localProjectPayload(flags = {}) {
|
|
1641
|
+
const cwd = process.cwd()
|
|
1642
|
+
const packageRaw = await readText('package.json')
|
|
1643
|
+
const packageJson = safeJson(packageRaw)
|
|
1644
|
+
const readme = await readFirstExisting(['README.md', 'readme.md', 'README.mdx', 'README'])
|
|
1645
|
+
const gitConfig = await readText('.git/config')
|
|
1646
|
+
const remoteUrl = normalizeRemoteUrl(flags.repoUrl || gitRemoteFromConfig(gitConfig))
|
|
1647
|
+
const dependencies = dependencyNames(packageJson)
|
|
1648
|
+
const demoUrl = flags.demoUrl || (typeof packageJson?.homepage === 'string' ? packageJson.homepage : '')
|
|
1649
|
+
|
|
1650
|
+
return {
|
|
1651
|
+
repoUrl: remoteUrl,
|
|
1652
|
+
readme,
|
|
1653
|
+
packageJson: packageRaw || undefined,
|
|
1654
|
+
demoUrl,
|
|
1655
|
+
imageUrl: flags.imageUrl || '',
|
|
1656
|
+
localMetadata: {
|
|
1657
|
+
cwdName: basename(cwd),
|
|
1658
|
+
packageName: typeof packageJson?.name === 'string' ? packageJson.name : '',
|
|
1659
|
+
localSummary: flags.summary || (typeof packageJson?.description === 'string' ? packageJson.description : ''),
|
|
1660
|
+
remoteUrl,
|
|
1661
|
+
detectedDependencies: dependencies,
|
|
1662
|
+
},
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function projectBodyFromDraft(draftResponse, overrides = {}) {
|
|
1667
|
+
const draft = draftResponse?.draft ?? {}
|
|
1668
|
+
return {
|
|
1669
|
+
title: overrides.title || draft.title,
|
|
1670
|
+
summary: overrides.summary || draft.summary,
|
|
1671
|
+
description: overrides.description || draft.description,
|
|
1672
|
+
repoUrl: overrides.repoUrl || draft.repoUrl,
|
|
1673
|
+
demoUrl: overrides.demoUrl || draft.demoUrl,
|
|
1674
|
+
imageUrl: overrides.imageUrl || draft.imageUrl,
|
|
1675
|
+
tools: overrides.tools || draft.tools || [],
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
function printDraft(draftResponse) {
|
|
1680
|
+
const draft = draftResponse?.draft ?? {}
|
|
1681
|
+
console.log(`Draft: ${draft.title ?? 'Untitled project'}`)
|
|
1682
|
+
if (draft.repoUrl) console.log(`Repo: ${draft.repoUrl}`)
|
|
1683
|
+
if (draft.demoUrl) console.log(`Demo: ${draft.demoUrl}`)
|
|
1684
|
+
console.log(`Confidence: ${draftResponse?.confidence ?? 0}%`)
|
|
1685
|
+
if (draftResponse?.duplicateProjectHref) {
|
|
1686
|
+
console.log(`Already published: ${draftResponse.duplicateProjectHref}`)
|
|
1687
|
+
return
|
|
1688
|
+
}
|
|
1689
|
+
const tools = draftResponse?.detectedTools ?? []
|
|
1690
|
+
if (tools.length) {
|
|
1691
|
+
console.log('Detected tools:')
|
|
1692
|
+
for (const item of tools.slice(0, 8)) console.log(`- ${item.tool.name} (${item.role.toLowerCase()})`)
|
|
1693
|
+
} else {
|
|
1694
|
+
console.log('Detected tools: none yet')
|
|
1695
|
+
}
|
|
1696
|
+
const missing = draftResponse?.missingFields ?? []
|
|
1697
|
+
if (missing.length) console.log(`Missing/recommended: ${missing.join(', ')}`)
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
async function handleMcpRequest(message, state) {
|
|
1701
|
+
const { id, method, params } = message
|
|
1702
|
+
if (!method || method.startsWith('notifications/')) return null
|
|
1703
|
+
|
|
1704
|
+
if (method === 'initialize') {
|
|
1705
|
+
return {
|
|
1706
|
+
jsonrpc: '2.0',
|
|
1707
|
+
id,
|
|
1708
|
+
result: {
|
|
1709
|
+
protocolVersion: params?.protocolVersion ?? '2024-11-05',
|
|
1710
|
+
capabilities: {
|
|
1711
|
+
resources: {},
|
|
1712
|
+
tools: {},
|
|
1713
|
+
},
|
|
1714
|
+
serverInfo: { name: 'directory-ai-tools', version: VERSION },
|
|
1715
|
+
},
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
if (method === 'resources/list') {
|
|
1720
|
+
return {
|
|
1721
|
+
jsonrpc: '2.0',
|
|
1722
|
+
id,
|
|
1723
|
+
result: {
|
|
1724
|
+
resources: [
|
|
1725
|
+
{
|
|
1726
|
+
uri: 'dai://developer/context',
|
|
1727
|
+
name: 'Developer context',
|
|
1728
|
+
description: 'Profile, project, saved stack, recommendations, and tool metadata.',
|
|
1729
|
+
mimeType: 'application/json',
|
|
1730
|
+
},
|
|
1731
|
+
{
|
|
1732
|
+
uri: 'dai://developer/context.md',
|
|
1733
|
+
name: 'Developer context markdown',
|
|
1734
|
+
description: 'Markdown context pack for coding agents.',
|
|
1735
|
+
mimeType: 'text/markdown',
|
|
1736
|
+
},
|
|
1737
|
+
{
|
|
1738
|
+
uri: 'dai://developer/build-packet',
|
|
1739
|
+
name: 'Latest Build Packet',
|
|
1740
|
+
description: 'Latest Stack Advisor build packet with stack, costs, safety boundary, evals, env vars, and proof loop.',
|
|
1741
|
+
mimeType: 'application/json',
|
|
1742
|
+
},
|
|
1743
|
+
{
|
|
1744
|
+
uri: 'dai://developer/build-packet.md',
|
|
1745
|
+
name: 'Latest Build Packet markdown',
|
|
1746
|
+
description: 'Markdown build handoff packet for coding agents.',
|
|
1747
|
+
mimeType: 'text/markdown',
|
|
1748
|
+
},
|
|
1749
|
+
{
|
|
1750
|
+
uri: 'dai://account/mode',
|
|
1751
|
+
name: 'Tokens& MCP account mode',
|
|
1752
|
+
description: 'Explains whether this MCP server is running as public, developer, or enterprise context.',
|
|
1753
|
+
mimeType: 'application/json',
|
|
1754
|
+
},
|
|
1755
|
+
],
|
|
1756
|
+
},
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
if (method === 'resources/read') {
|
|
1761
|
+
const uri = params?.uri ?? 'dai://developer/context'
|
|
1762
|
+
if (uri === 'dai://account/mode') {
|
|
1763
|
+
return {
|
|
1764
|
+
jsonrpc: '2.0',
|
|
1765
|
+
id,
|
|
1766
|
+
result: {
|
|
1767
|
+
contents: [
|
|
1768
|
+
{
|
|
1769
|
+
uri,
|
|
1770
|
+
mimeType: 'application/json',
|
|
1771
|
+
text: JSON.stringify(accountModePayload(state), null, 2),
|
|
1772
|
+
},
|
|
1773
|
+
],
|
|
1774
|
+
},
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
if (!state.token) {
|
|
1778
|
+
return {
|
|
1779
|
+
jsonrpc: '2.0',
|
|
1780
|
+
id,
|
|
1781
|
+
result: {
|
|
1782
|
+
contents: [authRequiredContent(state, uri)],
|
|
1783
|
+
},
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
const markdown = uri.endsWith('.md')
|
|
1787
|
+
const isBuildPacket = uri.includes('/build-packet')
|
|
1788
|
+
const path = isBuildPacket
|
|
1789
|
+
? `/api/workflow/build-packet${markdown ? '?format=markdown' : ''}`
|
|
1790
|
+
: markdown ? '/api/workflow/context?format=markdown' : '/api/workflow/context'
|
|
1791
|
+
const content = await workflowApi(path, { token: state.token, baseUrl: state.baseUrl })
|
|
1792
|
+
return {
|
|
1793
|
+
jsonrpc: '2.0',
|
|
1794
|
+
id,
|
|
1795
|
+
result: {
|
|
1796
|
+
contents: [
|
|
1797
|
+
{
|
|
1798
|
+
uri,
|
|
1799
|
+
mimeType: markdown ? 'text/markdown' : 'application/json',
|
|
1800
|
+
text: typeof content === 'string' ? content : JSON.stringify(content, null, 2),
|
|
1801
|
+
},
|
|
1802
|
+
],
|
|
1803
|
+
},
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
if (method === 'tools/list') {
|
|
1808
|
+
return {
|
|
1809
|
+
jsonrpc: '2.0',
|
|
1810
|
+
id,
|
|
1811
|
+
result: {
|
|
1812
|
+
tools: [
|
|
1813
|
+
{
|
|
1814
|
+
name: 'get_account_mode',
|
|
1815
|
+
description: 'Explain whether Tokens& MCP is running in public, developer, or enterprise mode and which private surfaces are unlocked.',
|
|
1816
|
+
inputSchema: {
|
|
1817
|
+
type: 'object',
|
|
1818
|
+
properties: {},
|
|
1819
|
+
},
|
|
1820
|
+
},
|
|
1821
|
+
{
|
|
1822
|
+
name: 'get_context',
|
|
1823
|
+
description: 'Read developer profile, saved stack, latest recommendations, and active project context.',
|
|
1824
|
+
inputSchema: {
|
|
1825
|
+
type: 'object',
|
|
1826
|
+
properties: { format: { type: 'string', enum: ['json', 'markdown'] } },
|
|
1827
|
+
},
|
|
1828
|
+
},
|
|
1829
|
+
{
|
|
1830
|
+
name: 'get_build_packet',
|
|
1831
|
+
description: 'Read the latest Tokens& Build Packet for the selected Stack Advisor recommendation before coding.',
|
|
1832
|
+
inputSchema: {
|
|
1833
|
+
type: 'object',
|
|
1834
|
+
properties: {
|
|
1835
|
+
recommendationId: { type: 'string' },
|
|
1836
|
+
variant: { type: 'string', enum: ['best', 'cheapest', 'fastest', 'open_source', 'enterprise'] },
|
|
1837
|
+
format: { type: 'string', enum: ['json', 'markdown'] },
|
|
1838
|
+
},
|
|
1839
|
+
},
|
|
1840
|
+
},
|
|
1841
|
+
{
|
|
1842
|
+
name: 'search_tools',
|
|
1843
|
+
description: 'Search stack/tool recommendations by developer intent.',
|
|
1844
|
+
inputSchema: {
|
|
1845
|
+
type: 'object',
|
|
1846
|
+
required: ['query'],
|
|
1847
|
+
properties: {
|
|
1848
|
+
query: { type: 'string' },
|
|
1849
|
+
limit: { type: 'number', minimum: 1, maximum: 30 },
|
|
1850
|
+
},
|
|
1851
|
+
},
|
|
1852
|
+
},
|
|
1853
|
+
{
|
|
1854
|
+
name: 'find_perks',
|
|
1855
|
+
description: 'Find public builder credits, offers, startup programs, events, and Tokens& starter-kit perks.',
|
|
1856
|
+
inputSchema: {
|
|
1857
|
+
type: 'object',
|
|
1858
|
+
properties: {
|
|
1859
|
+
categorySlug: { type: 'string' },
|
|
1860
|
+
toolId: { type: 'string' },
|
|
1861
|
+
limit: { type: 'number', minimum: 1, maximum: 50 },
|
|
1862
|
+
},
|
|
1863
|
+
},
|
|
1864
|
+
},
|
|
1865
|
+
{
|
|
1866
|
+
name: 'get_adoption_rank',
|
|
1867
|
+
description: 'Read public AgentRank/adoption evidence for tools before choosing a stack.',
|
|
1868
|
+
inputSchema: {
|
|
1869
|
+
type: 'object',
|
|
1870
|
+
properties: {
|
|
1871
|
+
tool: { type: 'string' },
|
|
1872
|
+
category: { type: 'string' },
|
|
1873
|
+
period: { type: 'string', enum: ['7d', '30d', '90d'] },
|
|
1874
|
+
limit: { type: 'number', minimum: 1, maximum: 200 },
|
|
1875
|
+
},
|
|
1876
|
+
},
|
|
1877
|
+
},
|
|
1878
|
+
{
|
|
1879
|
+
name: 'build_brief',
|
|
1880
|
+
description: 'Create a public Tokens& builder brief: best APIs, cost drivers, free credits/perks, stack suggestions, adoption evidence, dashboard sync path, and next agent actions.',
|
|
1881
|
+
inputSchema: {
|
|
1882
|
+
type: 'object',
|
|
1883
|
+
required: ['intent'],
|
|
1884
|
+
properties: {
|
|
1885
|
+
intent: { type: 'string' },
|
|
1886
|
+
query: { type: 'string' },
|
|
1887
|
+
useCase: { type: 'string', description: 'Optional app type such as customer_agent, support_agent, rag_app, internal_tool, or api_integration.' },
|
|
1888
|
+
category: { type: 'string' },
|
|
1889
|
+
categorySlug: { type: 'string' },
|
|
1890
|
+
pricing: { type: 'string' },
|
|
1891
|
+
openSource: { type: 'boolean' },
|
|
1892
|
+
enterpriseReady: { type: 'boolean' },
|
|
1893
|
+
period: { type: 'string', enum: ['7d', '30d', '90d'] },
|
|
1894
|
+
limit: { type: 'number', minimum: 1, maximum: 10 },
|
|
1895
|
+
},
|
|
1896
|
+
},
|
|
1897
|
+
},
|
|
1898
|
+
{
|
|
1899
|
+
name: 'enterprise_session_brief',
|
|
1900
|
+
description: 'Analyze enterprise builder-session metrics or a labeled sample profile to improve perks, activation, retention, and proof loops.',
|
|
1901
|
+
inputSchema: {
|
|
1902
|
+
type: 'object',
|
|
1903
|
+
properties: {
|
|
1904
|
+
question: { type: 'string' },
|
|
1905
|
+
profile: { type: 'string', description: 'Company/profile key such as redis, assemblyai, clickhouse, or supabase.' },
|
|
1906
|
+
company: { type: 'string' },
|
|
1907
|
+
organizationId: { type: 'string' },
|
|
1908
|
+
targetBuilder: { type: 'string' },
|
|
1909
|
+
sessionName: { type: 'string' },
|
|
1910
|
+
budgetUsd: { type: 'number', description: 'Campaign/session budget used for cost-per-activation/proof math.' },
|
|
1911
|
+
campaignBudgetUsd: { type: 'number' },
|
|
1912
|
+
sessionBudgetUsd: { type: 'number' },
|
|
1913
|
+
registrations: { type: 'number' },
|
|
1914
|
+
attended: { type: 'number' },
|
|
1915
|
+
attendees: { type: 'number' },
|
|
1916
|
+
builders: { type: 'number' },
|
|
1917
|
+
activated: { type: 'number' },
|
|
1918
|
+
firstSuccesses: { type: 'number' },
|
|
1919
|
+
retained: { type: 'number' },
|
|
1920
|
+
retainedBuilders: { type: 'number' },
|
|
1921
|
+
proofCount: { type: 'number' },
|
|
1922
|
+
proofProjects: { type: 'number' },
|
|
1923
|
+
live: { type: 'boolean', description: 'Set true only when requesting live private enterprise data.' },
|
|
1924
|
+
session: {
|
|
1925
|
+
type: 'object',
|
|
1926
|
+
description: 'Single session metrics. Use this when the agent is analyzing one event, lab, workshop, or proof sprint.',
|
|
1927
|
+
properties: {
|
|
1928
|
+
name: { type: 'string' },
|
|
1929
|
+
date: { type: 'string' },
|
|
1930
|
+
costUsd: { type: 'number' },
|
|
1931
|
+
budgetUsd: { type: 'number' },
|
|
1932
|
+
channel: { type: 'string' },
|
|
1933
|
+
icpSegment: { type: 'string' },
|
|
1934
|
+
registrations: { type: 'number' },
|
|
1935
|
+
attended: { type: 'number' },
|
|
1936
|
+
attendees: { type: 'number' },
|
|
1937
|
+
builders: { type: 'number' },
|
|
1938
|
+
activated: { type: 'number' },
|
|
1939
|
+
firstSuccesses: { type: 'number' },
|
|
1940
|
+
retained: { type: 'number' },
|
|
1941
|
+
retainedBuilders: { type: 'number' },
|
|
1942
|
+
proofCount: { type: 'number' },
|
|
1943
|
+
proofProjects: { type: 'number' },
|
|
1944
|
+
offer: { type: 'string' },
|
|
1945
|
+
notes: { type: 'string' },
|
|
1946
|
+
},
|
|
1947
|
+
},
|
|
1948
|
+
sessions: {
|
|
1949
|
+
type: 'array',
|
|
1950
|
+
items: {
|
|
1951
|
+
type: 'object',
|
|
1952
|
+
properties: {
|
|
1953
|
+
name: { type: 'string' },
|
|
1954
|
+
date: { type: 'string' },
|
|
1955
|
+
costUsd: { type: 'number' },
|
|
1956
|
+
budgetUsd: { type: 'number' },
|
|
1957
|
+
channel: { type: 'string' },
|
|
1958
|
+
icpSegment: { type: 'string' },
|
|
1959
|
+
registrations: { type: 'number' },
|
|
1960
|
+
attended: { type: 'number' },
|
|
1961
|
+
attendees: { type: 'number' },
|
|
1962
|
+
builders: { type: 'number' },
|
|
1963
|
+
activated: { type: 'number' },
|
|
1964
|
+
firstSuccesses: { type: 'number' },
|
|
1965
|
+
retained: { type: 'number' },
|
|
1966
|
+
retainedBuilders: { type: 'number' },
|
|
1967
|
+
proofCount: { type: 'number' },
|
|
1968
|
+
proofProjects: { type: 'number' },
|
|
1969
|
+
offer: { type: 'string' },
|
|
1970
|
+
notes: { type: 'string' },
|
|
1971
|
+
},
|
|
1972
|
+
},
|
|
1973
|
+
},
|
|
1974
|
+
segments: {
|
|
1975
|
+
type: 'array',
|
|
1976
|
+
description: 'Optional ICP segment metrics to rank instead of using profile defaults.',
|
|
1977
|
+
items: {
|
|
1978
|
+
type: 'object',
|
|
1979
|
+
properties: {
|
|
1980
|
+
name: { type: 'string' },
|
|
1981
|
+
builders: { type: 'number' },
|
|
1982
|
+
activated: { type: 'number' },
|
|
1983
|
+
firstSuccesses: { type: 'number' },
|
|
1984
|
+
retained: { type: 'number' },
|
|
1985
|
+
retainedBuilders: { type: 'number' },
|
|
1986
|
+
proofCount: { type: 'number' },
|
|
1987
|
+
proofProjects: { type: 'number' },
|
|
1988
|
+
pipelineUsd: { type: 'number' },
|
|
1989
|
+
score: { type: 'number' },
|
|
1990
|
+
reason: { type: 'string' },
|
|
1991
|
+
},
|
|
1992
|
+
},
|
|
1993
|
+
},
|
|
1994
|
+
},
|
|
1995
|
+
},
|
|
1996
|
+
},
|
|
1997
|
+
{
|
|
1998
|
+
name: 'draft_enterprise_session_motion',
|
|
1999
|
+
description: `Approval-only draft for the enterprise promise: ${ENTERPRISE_GROWTH_PROMISE}`,
|
|
2000
|
+
inputSchema: {
|
|
2001
|
+
type: 'object',
|
|
2002
|
+
properties: {
|
|
2003
|
+
profile: { type: 'string', description: 'Company/profile key such as redis, assemblyai, clickhouse, or supabase.' },
|
|
2004
|
+
company: { type: 'string' },
|
|
2005
|
+
sessionName: { type: 'string' },
|
|
2006
|
+
title: { type: 'string' },
|
|
2007
|
+
icp: { type: 'string' },
|
|
2008
|
+
audience: { type: 'string' },
|
|
2009
|
+
targetBuilder: { type: 'string' },
|
|
2010
|
+
format: { type: 'string' },
|
|
2011
|
+
sessionFormat: { type: 'string' },
|
|
2012
|
+
offer: { type: 'string' },
|
|
2013
|
+
trackingEvents: { type: 'array', items: { type: 'string' } },
|
|
2014
|
+
events: { type: 'array', items: { type: 'string' } },
|
|
2015
|
+
budgetUsd: { type: 'number' },
|
|
2016
|
+
},
|
|
2017
|
+
},
|
|
2018
|
+
},
|
|
2019
|
+
{
|
|
2020
|
+
name: 'draft_adoption_session',
|
|
2021
|
+
description: `Alias for draft_enterprise_session_motion. Approval-only draft for creating a tracked Tokens& adoption session; external event pages are not created.`,
|
|
2022
|
+
inputSchema: {
|
|
2023
|
+
type: 'object',
|
|
2024
|
+
properties: {
|
|
2025
|
+
profile: { type: 'string', description: 'Company/profile key such as redis, assemblyai, clickhouse, or supabase.' },
|
|
2026
|
+
company: { type: 'string' },
|
|
2027
|
+
sessionName: { type: 'string' },
|
|
2028
|
+
title: { type: 'string' },
|
|
2029
|
+
icp: { type: 'string' },
|
|
2030
|
+
audience: { type: 'string' },
|
|
2031
|
+
targetBuilder: { type: 'string' },
|
|
2032
|
+
format: { type: 'string' },
|
|
2033
|
+
sessionFormat: { type: 'string' },
|
|
2034
|
+
sourceProvider: { type: 'string', description: 'luma, partiful, eventbrite, zoom, or manual.' },
|
|
2035
|
+
sourceUrl: { type: 'string' },
|
|
2036
|
+
partnerCompany: { type: 'string' },
|
|
2037
|
+
offer: { type: 'string' },
|
|
2038
|
+
trackingEvents: { type: 'array', items: { type: 'string' } },
|
|
2039
|
+
events: { type: 'array', items: { type: 'string' } },
|
|
2040
|
+
budgetUsd: { type: 'number' },
|
|
2041
|
+
},
|
|
2042
|
+
},
|
|
2043
|
+
},
|
|
2044
|
+
{
|
|
2045
|
+
name: 'draft_builder_invite_motion',
|
|
2046
|
+
description: 'Approval-only draft for inviting the right builders into a tracked enterprise motion.',
|
|
2047
|
+
inputSchema: {
|
|
2048
|
+
type: 'object',
|
|
2049
|
+
properties: {
|
|
2050
|
+
profile: { type: 'string' },
|
|
2051
|
+
company: { type: 'string' },
|
|
2052
|
+
icp: { type: 'string' },
|
|
2053
|
+
audience: { type: 'string' },
|
|
2054
|
+
segments: { type: 'array', items: { type: 'string' } },
|
|
2055
|
+
icpSegments: { type: 'array', items: { type: 'string' } },
|
|
2056
|
+
channels: { type: 'array', items: { type: 'string' } },
|
|
2057
|
+
offer: { type: 'string' },
|
|
2058
|
+
},
|
|
2059
|
+
},
|
|
2060
|
+
},
|
|
2061
|
+
{
|
|
2062
|
+
name: 'draft_icp_invite_batch',
|
|
2063
|
+
description: 'Approval-only draft for previewing and approving an aggregate ICP builder invite batch.',
|
|
2064
|
+
inputSchema: {
|
|
2065
|
+
type: 'object',
|
|
2066
|
+
properties: {
|
|
2067
|
+
profile: { type: 'string' },
|
|
2068
|
+
company: { type: 'string' },
|
|
2069
|
+
icp: { type: 'string' },
|
|
2070
|
+
audience: { type: 'string' },
|
|
2071
|
+
segments: { type: 'array', items: { type: 'string' } },
|
|
2072
|
+
icpSegments: { type: 'array', items: { type: 'string' } },
|
|
2073
|
+
channels: { type: 'array', items: { type: 'string' } },
|
|
2074
|
+
offer: { type: 'string' },
|
|
2075
|
+
},
|
|
2076
|
+
},
|
|
2077
|
+
},
|
|
2078
|
+
{
|
|
2079
|
+
name: 'draft_partner_onboarding_motion',
|
|
2080
|
+
description: 'Approval-only draft for inviting a partner company into a tracked builder session or proof motion.',
|
|
2081
|
+
inputSchema: {
|
|
2082
|
+
type: 'object',
|
|
2083
|
+
properties: {
|
|
2084
|
+
profile: { type: 'string' },
|
|
2085
|
+
company: { type: 'string' },
|
|
2086
|
+
partnerCompany: { type: 'string' },
|
|
2087
|
+
partner: { type: 'string' },
|
|
2088
|
+
sharedAudience: { type: 'string' },
|
|
2089
|
+
audience: { type: 'string' },
|
|
2090
|
+
offer: { type: 'string' },
|
|
2091
|
+
},
|
|
2092
|
+
},
|
|
2093
|
+
},
|
|
2094
|
+
{
|
|
2095
|
+
name: 'draft_partner_invite',
|
|
2096
|
+
description: 'Alias for draft_partner_onboarding_motion. Approval-only draft for Invite partner company.',
|
|
2097
|
+
inputSchema: {
|
|
2098
|
+
type: 'object',
|
|
2099
|
+
properties: {
|
|
2100
|
+
profile: { type: 'string' },
|
|
2101
|
+
company: { type: 'string' },
|
|
2102
|
+
partnerCompany: { type: 'string' },
|
|
2103
|
+
partner: { type: 'string' },
|
|
2104
|
+
sharedAudience: { type: 'string' },
|
|
2105
|
+
audience: { type: 'string' },
|
|
2106
|
+
offer: { type: 'string' },
|
|
2107
|
+
},
|
|
2108
|
+
},
|
|
2109
|
+
},
|
|
2110
|
+
{
|
|
2111
|
+
name: 'draft_adoption_proof_motion',
|
|
2112
|
+
description: 'Approval-only draft for defining proof of adoption before publishing, exporting, or changing GTM actions.',
|
|
2113
|
+
inputSchema: {
|
|
2114
|
+
type: 'object',
|
|
2115
|
+
properties: {
|
|
2116
|
+
profile: { type: 'string' },
|
|
2117
|
+
company: { type: 'string' },
|
|
2118
|
+
icp: { type: 'string' },
|
|
2119
|
+
proofDefinition: { type: 'string' },
|
|
2120
|
+
evidenceSources: { type: 'array', items: { type: 'string' } },
|
|
2121
|
+
readoutAudience: { type: 'string' },
|
|
2122
|
+
},
|
|
2123
|
+
},
|
|
2124
|
+
},
|
|
2125
|
+
{
|
|
2126
|
+
name: 'draft_project',
|
|
2127
|
+
description: 'Infer a tokens& project draft from the current local repo before publishing.',
|
|
2128
|
+
inputSchema: {
|
|
2129
|
+
type: 'object',
|
|
2130
|
+
properties: {
|
|
2131
|
+
autoDetect: { type: 'boolean' },
|
|
2132
|
+
repoUrl: { type: 'string' },
|
|
2133
|
+
demoUrl: { type: 'string' },
|
|
2134
|
+
imageUrl: { type: 'string' },
|
|
2135
|
+
readme: { type: 'string' },
|
|
2136
|
+
packageJson: { type: 'string' },
|
|
2137
|
+
localSummary: { type: 'string' },
|
|
2138
|
+
detectedDependencies: { type: 'array', items: { type: 'string' } },
|
|
2139
|
+
},
|
|
2140
|
+
},
|
|
2141
|
+
},
|
|
2142
|
+
{
|
|
2143
|
+
name: 'publish_project',
|
|
2144
|
+
description: 'Publish a built project back to tokens&. Use autoDetect first, then confirm before creating the public project.',
|
|
2145
|
+
inputSchema: {
|
|
2146
|
+
type: 'object',
|
|
2147
|
+
properties: {
|
|
2148
|
+
autoDetect: { type: 'boolean' },
|
|
2149
|
+
confirm: { type: 'boolean', description: 'Required for auto-detected publishing after reviewing the draft.' },
|
|
2150
|
+
title: { type: 'string' },
|
|
2151
|
+
summary: { type: 'string' },
|
|
2152
|
+
description: { type: 'string' },
|
|
2153
|
+
repoUrl: { type: 'string' },
|
|
2154
|
+
demoUrl: { type: 'string' },
|
|
2155
|
+
imageUrl: { type: 'string' },
|
|
2156
|
+
localSummary: { type: 'string' },
|
|
2157
|
+
detectedDependencies: { type: 'array', items: { type: 'string' } },
|
|
2158
|
+
tools: {
|
|
2159
|
+
type: 'array',
|
|
2160
|
+
items: {
|
|
2161
|
+
type: 'object',
|
|
2162
|
+
required: ['toolId'],
|
|
2163
|
+
properties: {
|
|
2164
|
+
toolId: { type: 'string' },
|
|
2165
|
+
role: { type: 'string', enum: ['PRIMARY', 'USED', 'EVALUATED', 'REPLACED'] },
|
|
2166
|
+
note: { type: 'string' },
|
|
2167
|
+
},
|
|
2168
|
+
},
|
|
2169
|
+
},
|
|
2170
|
+
},
|
|
2171
|
+
},
|
|
2172
|
+
},
|
|
2173
|
+
{
|
|
2174
|
+
name: 'update_build_progress',
|
|
2175
|
+
description: 'Draft a build-progress update for human approval. V1 does not write externally.',
|
|
2176
|
+
inputSchema: {
|
|
2177
|
+
type: 'object',
|
|
2178
|
+
properties: {
|
|
2179
|
+
status: { type: 'string' },
|
|
2180
|
+
summary: { type: 'string' },
|
|
2181
|
+
evidenceUrl: { type: 'string' },
|
|
2182
|
+
},
|
|
2183
|
+
},
|
|
2184
|
+
},
|
|
2185
|
+
{
|
|
2186
|
+
name: 'attach_repo',
|
|
2187
|
+
description: 'Draft a repo attachment for human approval before publishing proof.',
|
|
2188
|
+
inputSchema: {
|
|
2189
|
+
type: 'object',
|
|
2190
|
+
properties: {
|
|
2191
|
+
repoUrl: { type: 'string' },
|
|
2192
|
+
demoUrl: { type: 'string' },
|
|
2193
|
+
confirm: { type: 'boolean' },
|
|
2194
|
+
},
|
|
2195
|
+
},
|
|
2196
|
+
},
|
|
2197
|
+
{
|
|
2198
|
+
name: 'record_launch_event',
|
|
2199
|
+
description: 'Draft a launch-event record for human approval before it affects public proof.',
|
|
2200
|
+
inputSchema: {
|
|
2201
|
+
type: 'object',
|
|
2202
|
+
properties: {
|
|
2203
|
+
source: { type: 'string' },
|
|
2204
|
+
url: { type: 'string' },
|
|
2205
|
+
summary: { type: 'string' },
|
|
2206
|
+
},
|
|
2207
|
+
},
|
|
2208
|
+
},
|
|
2209
|
+
{
|
|
2210
|
+
name: 'submit_stack_feedback',
|
|
2211
|
+
description: 'Draft stack feedback for human approval and future recommendation learning.',
|
|
2212
|
+
inputSchema: {
|
|
2213
|
+
type: 'object',
|
|
2214
|
+
properties: {
|
|
2215
|
+
rating: { type: 'string' },
|
|
2216
|
+
feedback: { type: 'string' },
|
|
2217
|
+
selectedTools: { type: 'array', items: { type: 'string' } },
|
|
2218
|
+
},
|
|
2219
|
+
},
|
|
2220
|
+
},
|
|
2221
|
+
],
|
|
2222
|
+
},
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
if (method === 'tools/call') {
|
|
2227
|
+
const name = params?.name
|
|
2228
|
+
const args = params?.arguments ?? {}
|
|
2229
|
+
if (name) await trackMcpToolCall(state, name, args)
|
|
2230
|
+
if (name === 'get_account_mode') {
|
|
2231
|
+
return { jsonrpc: '2.0', id, result: mcpText(accountModePayload(state)) }
|
|
2232
|
+
}
|
|
2233
|
+
if (name === 'get_context') {
|
|
2234
|
+
if (!state.token) return { jsonrpc: '2.0', id, result: mcpText(authRequiredPayload(state, name)) }
|
|
2235
|
+
const markdown = args.format === 'markdown'
|
|
2236
|
+
const content = await workflowApi(markdown ? '/api/workflow/context?format=markdown' : '/api/workflow/context', {
|
|
2237
|
+
token: state.token,
|
|
2238
|
+
baseUrl: state.baseUrl,
|
|
2239
|
+
})
|
|
2240
|
+
return { jsonrpc: '2.0', id, result: mcpText(content) }
|
|
2241
|
+
}
|
|
2242
|
+
if (name === 'get_build_packet') {
|
|
2243
|
+
if (!state.token) return { jsonrpc: '2.0', id, result: mcpText(authRequiredPayload(state, name)) }
|
|
2244
|
+
const params = new URLSearchParams()
|
|
2245
|
+
if (args.recommendationId) params.set('recommendationId', String(args.recommendationId))
|
|
2246
|
+
if (args.variant) params.set('variant', String(args.variant))
|
|
2247
|
+
if (args.format === 'markdown') params.set('format', 'markdown')
|
|
2248
|
+
const suffix = params.toString() ? `?${params.toString()}` : ''
|
|
2249
|
+
const content = await workflowApi(`/api/workflow/build-packet${suffix}`, {
|
|
2250
|
+
token: state.token,
|
|
2251
|
+
baseUrl: state.baseUrl,
|
|
2252
|
+
})
|
|
2253
|
+
await trackAgentInstallEvent(state, 'agent_install_build_packet_read', {
|
|
2254
|
+
recommendationId: args.recommendationId,
|
|
2255
|
+
variant: args.variant,
|
|
2256
|
+
format: args.format,
|
|
2257
|
+
})
|
|
2258
|
+
return { jsonrpc: '2.0', id, result: mcpText(content) }
|
|
2259
|
+
}
|
|
2260
|
+
if (name === 'search_tools') {
|
|
2261
|
+
const query = String(args.query ?? '').trim()
|
|
2262
|
+
if (!query) throw new Error('search_tools requires a query')
|
|
2263
|
+
const params = new URLSearchParams({ q: query, limit: String(args.limit ?? 12) })
|
|
2264
|
+
const content = await workflowApi(`/api/build/search?${params.toString()}`, { baseUrl: state.baseUrl })
|
|
2265
|
+
return { jsonrpc: '2.0', id, result: mcpText(content) }
|
|
2266
|
+
}
|
|
2267
|
+
if (name === 'find_perks') {
|
|
2268
|
+
const params = new URLSearchParams({ limit: String(args.limit ?? 12) })
|
|
2269
|
+
if (args.categorySlug) params.set('categorySlug', String(args.categorySlug))
|
|
2270
|
+
if (args.toolId) params.set('toolId', String(args.toolId))
|
|
2271
|
+
const content = await workflowApi(`/api/perks?${params.toString()}`, { baseUrl: state.baseUrl })
|
|
2272
|
+
return { jsonrpc: '2.0', id, result: mcpText(content) }
|
|
2273
|
+
}
|
|
2274
|
+
if (name === 'get_adoption_rank') {
|
|
2275
|
+
const params = new URLSearchParams({ limit: String(args.limit ?? 10) })
|
|
2276
|
+
if (args.tool) params.set('tool', String(args.tool))
|
|
2277
|
+
if (args.category) params.set('category', String(args.category))
|
|
2278
|
+
if (args.period) params.set('period', String(args.period))
|
|
2279
|
+
const content = await workflowApi(`/api/v1/adoption?${params.toString()}`, { baseUrl: state.baseUrl })
|
|
2280
|
+
return { jsonrpc: '2.0', id, result: mcpText(content) }
|
|
2281
|
+
}
|
|
2282
|
+
if (name === 'build_brief') {
|
|
2283
|
+
const content = await publicBuildBrief(args, state)
|
|
2284
|
+
if (content.matchedPerks?.length) {
|
|
2285
|
+
await trackAgentInstallEvent(state, 'agent_install_perk_shown', {
|
|
2286
|
+
intent: content.intent,
|
|
2287
|
+
categorySlug: args.categorySlug,
|
|
2288
|
+
perkCount: content.matchedPerks.length,
|
|
2289
|
+
toolCount: content.stackCandidates?.length ?? 0,
|
|
2290
|
+
})
|
|
2291
|
+
}
|
|
2292
|
+
return { jsonrpc: '2.0', id, result: mcpText(content) }
|
|
2293
|
+
}
|
|
2294
|
+
if (name === 'enterprise_session_brief') {
|
|
2295
|
+
const content = enterpriseSessionBrief(args, state)
|
|
2296
|
+
if (!content.authRequired) {
|
|
2297
|
+
await trackAgentInstallEvent(state, 'agent_install_enterprise_session_brief', {
|
|
2298
|
+
dataSource: content.dataSource,
|
|
2299
|
+
profile: content.profile?.key,
|
|
2300
|
+
organizationId: content.organizationId ? 'configured' : undefined,
|
|
2301
|
+
})
|
|
2302
|
+
}
|
|
2303
|
+
return { jsonrpc: '2.0', id, result: mcpText(content) }
|
|
2304
|
+
}
|
|
2305
|
+
if (name === 'draft_enterprise_session_motion' || name === 'draft_adoption_session') {
|
|
2306
|
+
return { jsonrpc: '2.0', id, result: mcpText(draftEnterpriseSessionMotion(args, state)) }
|
|
2307
|
+
}
|
|
2308
|
+
if (name === 'draft_builder_invite_motion') {
|
|
2309
|
+
return { jsonrpc: '2.0', id, result: mcpText(draftBuilderInviteMotion(args, state)) }
|
|
2310
|
+
}
|
|
2311
|
+
if (name === 'draft_icp_invite_batch') {
|
|
2312
|
+
return { jsonrpc: '2.0', id, result: mcpText(draftBuilderInviteMotion(args, state, 'icp_invite_batch')) }
|
|
2313
|
+
}
|
|
2314
|
+
if (name === 'draft_partner_onboarding_motion' || name === 'draft_partner_invite') {
|
|
2315
|
+
return { jsonrpc: '2.0', id, result: mcpText(draftPartnerOnboardingMotion(args, state)) }
|
|
2316
|
+
}
|
|
2317
|
+
if (name === 'draft_adoption_proof_motion') {
|
|
2318
|
+
return { jsonrpc: '2.0', id, result: mcpText(draftAdoptionProofMotion(args, state)) }
|
|
2319
|
+
}
|
|
2320
|
+
if (name === 'draft_project') {
|
|
2321
|
+
if (!state.token) return { jsonrpc: '2.0', id, result: mcpText(authRequiredPayload(state, name)) }
|
|
2322
|
+
const localPayload = args.autoDetect === false ? {} : await localProjectPayload(projectArgsToFlags(args))
|
|
2323
|
+
const body = {
|
|
2324
|
+
...localPayload,
|
|
2325
|
+
repoUrl: args.repoUrl || localPayload.repoUrl,
|
|
2326
|
+
readme: args.readme || localPayload.readme,
|
|
2327
|
+
packageJson: args.packageJson || localPayload.packageJson,
|
|
2328
|
+
demoUrl: args.demoUrl || localPayload.demoUrl,
|
|
2329
|
+
imageUrl: args.imageUrl || localPayload.imageUrl,
|
|
2330
|
+
localMetadata: {
|
|
2331
|
+
...(localPayload.localMetadata ?? {}),
|
|
2332
|
+
localSummary: args.localSummary || localPayload.localMetadata?.localSummary,
|
|
2333
|
+
detectedDependencies: args.detectedDependencies || localPayload.localMetadata?.detectedDependencies,
|
|
2334
|
+
},
|
|
2335
|
+
}
|
|
2336
|
+
const content = await workflowApi('/api/workflow/projects/draft', {
|
|
2337
|
+
method: 'POST',
|
|
2338
|
+
body,
|
|
2339
|
+
token: state.token,
|
|
2340
|
+
baseUrl: state.baseUrl,
|
|
2341
|
+
})
|
|
2342
|
+
await trackAgentInstallEvent(state, 'agent_install_project_proof_drafted', {
|
|
2343
|
+
autoDetect: args.autoDetect !== false,
|
|
2344
|
+
detectedDependencies: body.localMetadata?.detectedDependencies?.length,
|
|
2345
|
+
})
|
|
2346
|
+
return { jsonrpc: '2.0', id, result: mcpText(content) }
|
|
2347
|
+
}
|
|
2348
|
+
if (name === 'publish_project') {
|
|
2349
|
+
if (!state.token) return { jsonrpc: '2.0', id, result: mcpText(authRequiredPayload(state, name)) }
|
|
2350
|
+
if (args.autoDetect || !args.tools) {
|
|
2351
|
+
const localPayload = await localProjectPayload(projectArgsToFlags(args))
|
|
2352
|
+
const draft = await workflowApi('/api/workflow/projects/draft', {
|
|
2353
|
+
method: 'POST',
|
|
2354
|
+
body: {
|
|
2355
|
+
...localPayload,
|
|
2356
|
+
repoUrl: args.repoUrl || localPayload.repoUrl,
|
|
2357
|
+
readme: args.readme || localPayload.readme,
|
|
2358
|
+
packageJson: args.packageJson || localPayload.packageJson,
|
|
2359
|
+
demoUrl: args.demoUrl || localPayload.demoUrl,
|
|
2360
|
+
imageUrl: args.imageUrl || localPayload.imageUrl,
|
|
2361
|
+
localMetadata: {
|
|
2362
|
+
...(localPayload.localMetadata ?? {}),
|
|
2363
|
+
localSummary: args.localSummary || localPayload.localMetadata?.localSummary,
|
|
2364
|
+
detectedDependencies: args.detectedDependencies || localPayload.localMetadata?.detectedDependencies,
|
|
2365
|
+
},
|
|
2366
|
+
},
|
|
2367
|
+
token: state.token,
|
|
2368
|
+
baseUrl: state.baseUrl,
|
|
2369
|
+
})
|
|
2370
|
+
if (draft.duplicateProjectHref) {
|
|
2371
|
+
return {
|
|
2372
|
+
jsonrpc: '2.0',
|
|
2373
|
+
id,
|
|
2374
|
+
result: mcpText({
|
|
2375
|
+
message: 'This repository is already published. Add an update instead of creating a duplicate.',
|
|
2376
|
+
duplicateProjectHref: draft.duplicateProjectHref,
|
|
2377
|
+
draft,
|
|
2378
|
+
}),
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
if (!args.confirm) {
|
|
2382
|
+
await trackAgentInstallEvent(state, 'agent_install_project_proof_drafted', {
|
|
2383
|
+
autoDetect: true,
|
|
2384
|
+
requiresConfirmation: true,
|
|
2385
|
+
detectedTools: draft.detectedTools?.length,
|
|
2386
|
+
})
|
|
2387
|
+
return {
|
|
2388
|
+
jsonrpc: '2.0',
|
|
2389
|
+
id,
|
|
2390
|
+
result: mcpText({
|
|
2391
|
+
message: 'Review this draft with the user. Call publish_project again with confirm: true to publish.',
|
|
2392
|
+
draft,
|
|
2393
|
+
}),
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
if (!draft.draft?.tools?.length) {
|
|
2397
|
+
return {
|
|
2398
|
+
jsonrpc: '2.0',
|
|
2399
|
+
id,
|
|
2400
|
+
result: mcpText({
|
|
2401
|
+
message: 'I could not infer approved tools from this repo yet. Ask the user to choose tools or pass tools explicitly.',
|
|
2402
|
+
draft,
|
|
2403
|
+
}),
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
const content = await workflowApi('/api/workflow/projects', {
|
|
2407
|
+
method: 'POST',
|
|
2408
|
+
body: projectBodyFromDraft(draft, args),
|
|
2409
|
+
token: state.token,
|
|
2410
|
+
baseUrl: state.baseUrl,
|
|
2411
|
+
})
|
|
2412
|
+
await trackAgentInstallEvent(state, 'agent_install_project_proof_published', {
|
|
2413
|
+
autoDetect: true,
|
|
2414
|
+
toolCount: draft.draft?.tools?.length ?? 0,
|
|
2415
|
+
})
|
|
2416
|
+
return { jsonrpc: '2.0', id, result: mcpText(content) }
|
|
2417
|
+
}
|
|
2418
|
+
const content = await workflowApi('/api/workflow/projects', {
|
|
2419
|
+
method: 'POST',
|
|
2420
|
+
body: args,
|
|
2421
|
+
token: state.token,
|
|
2422
|
+
baseUrl: state.baseUrl,
|
|
2423
|
+
})
|
|
2424
|
+
await trackAgentInstallEvent(state, 'agent_install_project_proof_published', {
|
|
2425
|
+
autoDetect: false,
|
|
2426
|
+
toolCount: Array.isArray(args.tools) ? args.tools.length : 0,
|
|
2427
|
+
})
|
|
2428
|
+
return { jsonrpc: '2.0', id, result: mcpText(content) }
|
|
2429
|
+
}
|
|
2430
|
+
if (['update_build_progress', 'attach_repo', 'record_launch_event', 'submit_stack_feedback'].includes(name)) {
|
|
2431
|
+
return {
|
|
2432
|
+
jsonrpc: '2.0',
|
|
2433
|
+
id,
|
|
2434
|
+
result: mcpText({
|
|
2435
|
+
approvalRequired: true,
|
|
2436
|
+
persisted: false,
|
|
2437
|
+
message: `${name} is an approval-gated V1 draft. Review this with the user before any Tokens& write, public proof update, repo action, or external post.`,
|
|
2438
|
+
draft: args,
|
|
2439
|
+
nextSteps: [
|
|
2440
|
+
'Confirm the evidence with the user.',
|
|
2441
|
+
'Use draft_project to inspect the repo when relevant.',
|
|
2442
|
+
'Use publish_project only after explicit confirmation.',
|
|
2443
|
+
],
|
|
2444
|
+
}),
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
throw new Error(`Unknown tool: ${name}`)
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
return {
|
|
2451
|
+
jsonrpc: '2.0',
|
|
2452
|
+
id,
|
|
2453
|
+
error: { code: -32601, message: `Unknown method: ${method}` },
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
async function serveMcp({ baseUrl, config = {}, mode }) {
|
|
2458
|
+
const developerWorkflowToken = workflowToken(config)
|
|
2459
|
+
const privateEnterpriseToken = enterpriseToken(config)
|
|
2460
|
+
const organizationId = enterpriseOrganizationId(config)
|
|
2461
|
+
const enterpriseProfile = resolveEnterpriseProfile(config)
|
|
2462
|
+
const resolvedMode = resolveMcpMode({
|
|
2463
|
+
requestedMode: mode || config.mode || config.mcpMode,
|
|
2464
|
+
token: developerWorkflowToken,
|
|
2465
|
+
enterpriseToken: privateEnterpriseToken,
|
|
2466
|
+
organizationId,
|
|
2467
|
+
enterpriseProfile,
|
|
2468
|
+
})
|
|
2469
|
+
let buffer = ''
|
|
2470
|
+
const state = {
|
|
2471
|
+
baseUrl,
|
|
2472
|
+
token: developerWorkflowToken || '',
|
|
2473
|
+
enterpriseToken: privateEnterpriseToken || '',
|
|
2474
|
+
organizationId: organizationId || '',
|
|
2475
|
+
enterpriseProfile: enterpriseProfile || '',
|
|
2476
|
+
mode: resolvedMode,
|
|
2477
|
+
firstToolCallTracked: false,
|
|
2478
|
+
}
|
|
2479
|
+
await new Promise((resolve) => {
|
|
2480
|
+
process.stdin.setEncoding('utf8')
|
|
2481
|
+
process.stdin.on('data', (chunk) => {
|
|
2482
|
+
buffer = parseMcpBuffer(buffer + chunk, async (message) => {
|
|
2483
|
+
try {
|
|
2484
|
+
const response = await handleMcpRequest(message, state)
|
|
2485
|
+
if (response) writeMcpMessage(response)
|
|
2486
|
+
} catch (error) {
|
|
2487
|
+
if (message?.id != null) {
|
|
2488
|
+
writeMcpMessage({
|
|
2489
|
+
jsonrpc: '2.0',
|
|
2490
|
+
id: message.id,
|
|
2491
|
+
error: { code: -32000, message: error instanceof Error ? error.message : 'MCP server error' },
|
|
2492
|
+
})
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
})
|
|
2496
|
+
})
|
|
2497
|
+
process.stdin.on('end', resolve)
|
|
2498
|
+
process.stdin.resume()
|
|
2499
|
+
})
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
function printHelp() {
|
|
2503
|
+
console.log(`
|
|
2504
|
+
dai v${VERSION} — Developer Adoption Intelligence CLI
|
|
2505
|
+
|
|
2506
|
+
Usage:
|
|
2507
|
+
dai <command> [args] [flags]
|
|
2508
|
+
|
|
2509
|
+
Common commands:
|
|
2510
|
+
login OAuth device-code login
|
|
2511
|
+
logout Clear saved credentials
|
|
2512
|
+
whoami Show the signed-in account
|
|
2513
|
+
stack add <tool> Add a tool to your public stack
|
|
2514
|
+
stack remove <tool> Remove a tool from your stack
|
|
2515
|
+
stack sync Sync local package.json into your stack
|
|
2516
|
+
submit <id> Submit to a challenge (use --file to attach)
|
|
2517
|
+
proof <tool-slug> Fetch public adoption proof from /api/v1/adoption
|
|
2518
|
+
rank [tool-slug] Show live AI Adoption Score rankings
|
|
2519
|
+
notifications List unread notifications
|
|
2520
|
+
digest Print this week's personalized digest
|
|
2521
|
+
project draft Draft a public project from the current repo
|
|
2522
|
+
project publish Publish the current repo after review (--yes)
|
|
2523
|
+
mcp serve Start MCP server for Cursor/Codex
|
|
2524
|
+
version Print the CLI version
|
|
2525
|
+
|
|
2526
|
+
Global flags:
|
|
2527
|
+
--json Print raw JSON responses
|
|
2528
|
+
--api <url> Override the API base URL
|
|
2529
|
+
--mode <mode> MCP mode: auto, public, developer, or enterprise
|
|
2530
|
+
--help Show this help
|
|
2531
|
+
|
|
2532
|
+
Docs: https://tokensand.com/docs/api
|
|
2533
|
+
`)
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
export async function run(argv) {
|
|
2537
|
+
if (argv.length === 0 || argv.includes('--help') || argv[0] === 'help') {
|
|
2538
|
+
printHelp()
|
|
2539
|
+
return
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
const globals = parseGlobals(argv)
|
|
2543
|
+
const jsonFlag = globals.json
|
|
2544
|
+
const cleanArgs = globals.clean
|
|
2545
|
+
|
|
2546
|
+
const config = await loadConfig()
|
|
2547
|
+
|
|
2548
|
+
const [cmd, ...args] = cleanArgs
|
|
2549
|
+
switch (cmd) {
|
|
2550
|
+
case 'version':
|
|
2551
|
+
console.log(VERSION)
|
|
2552
|
+
return
|
|
2553
|
+
case 'login':
|
|
2554
|
+
console.log('Visit: ' + DEFAULT_API + '/login?cli=1')
|
|
2555
|
+
console.log('Paste the device code shown there into this terminal.')
|
|
2556
|
+
return
|
|
2557
|
+
case 'logout':
|
|
2558
|
+
console.log('Credentials cleared from ' + CONFIG_PATH)
|
|
2559
|
+
return
|
|
2560
|
+
case 'whoami':
|
|
2561
|
+
if (!config.token) {
|
|
2562
|
+
console.log('Not signed in. Run `dai login`.')
|
|
2563
|
+
return
|
|
2564
|
+
}
|
|
2565
|
+
console.log(`Signed in as ${config.email ?? 'unknown'}`)
|
|
2566
|
+
return
|
|
2567
|
+
case 'stack': {
|
|
2568
|
+
const sub = args[0]
|
|
2569
|
+
if (sub === 'add' || sub === 'remove') {
|
|
2570
|
+
console.log(`[stack ${sub}] queued for ${args[1] ?? '<tool>'}. Will sync on the next /api/stack push.`)
|
|
2571
|
+
return
|
|
2572
|
+
}
|
|
2573
|
+
if (sub === 'sync') {
|
|
2574
|
+
const pkg = await readFile('package.json', 'utf8').then(JSON.parse).catch(() => null)
|
|
2575
|
+
if (!pkg) {
|
|
2576
|
+
console.log('No package.json found in the current directory.')
|
|
2577
|
+
return
|
|
2578
|
+
}
|
|
2579
|
+
const deps = {
|
|
2580
|
+
...(pkg.dependencies ?? {}),
|
|
2581
|
+
...(pkg.devDependencies ?? {}),
|
|
2582
|
+
}
|
|
2583
|
+
const names = Object.keys(deps).sort()
|
|
2584
|
+
if (jsonFlag) {
|
|
2585
|
+
console.log(JSON.stringify({ package: pkg.name ?? null, dependencies: names }, null, 2))
|
|
2586
|
+
return
|
|
2587
|
+
}
|
|
2588
|
+
console.log(`Found ${names.length} package${names.length === 1 ? '' : 's'} for stack sync.`)
|
|
2589
|
+
console.log(names.slice(0, 12).map((name) => `- ${name}`).join('\n'))
|
|
2590
|
+
if (names.length > 12) console.log(`...and ${names.length - 12} more`)
|
|
2591
|
+
console.log('Run from an authenticated workspace when /api/stack sync is enabled.')
|
|
2592
|
+
return
|
|
2593
|
+
}
|
|
2594
|
+
console.log('Usage: dai stack <add|remove|sync> [args]')
|
|
2595
|
+
return
|
|
2596
|
+
}
|
|
2597
|
+
case 'submit':
|
|
2598
|
+
console.log(`Submitting to challenge ${args[0] ?? '<id>'}; attachments: ${args.includes('--file') ? 'yes' : 'no'}.`)
|
|
2599
|
+
return
|
|
2600
|
+
case 'proof': {
|
|
2601
|
+
const slug = args[0]
|
|
2602
|
+
if (!slug) {
|
|
2603
|
+
console.log('Usage: dai proof <tool-slug>')
|
|
2604
|
+
return
|
|
2605
|
+
}
|
|
2606
|
+
const data = await api(`/api/v1/adoption?tool=${encodeURIComponent(slug)}&limit=1`, { baseUrl: globals.apiBase })
|
|
2607
|
+
if (jsonFlag) {
|
|
2608
|
+
console.log(JSON.stringify(data, null, 2))
|
|
2609
|
+
} else {
|
|
2610
|
+
const row = data?.rows?.[0]
|
|
2611
|
+
console.log(row ? formatAdoptionRow(row) : `${slug}: No public adoption proof available yet.`)
|
|
2612
|
+
}
|
|
2613
|
+
return
|
|
2614
|
+
}
|
|
2615
|
+
case 'rank': {
|
|
2616
|
+
const liveArgs = args.filter((arg) => arg !== '--live')
|
|
2617
|
+
const slug = liveArgs[0]
|
|
2618
|
+
const path = slug
|
|
2619
|
+
? `/api/v1/adoption?tool=${encodeURIComponent(slug)}&limit=1`
|
|
2620
|
+
: '/api/v1/adoption?limit=10'
|
|
2621
|
+
const data = await api(path, { baseUrl: globals.apiBase })
|
|
2622
|
+
if (jsonFlag) {
|
|
2623
|
+
console.log(JSON.stringify(data, null, 2))
|
|
2624
|
+
return
|
|
2625
|
+
}
|
|
2626
|
+
const rows = data?.rows ?? []
|
|
2627
|
+
if (rows.length === 0) {
|
|
2628
|
+
console.log(slug ? `${slug}: no public rank available yet.` : 'No public rankings available yet.')
|
|
2629
|
+
return
|
|
2630
|
+
}
|
|
2631
|
+
for (const row of rows) console.log(formatAdoptionRow(row))
|
|
2632
|
+
return
|
|
2633
|
+
}
|
|
2634
|
+
case 'notifications': {
|
|
2635
|
+
const data = await api('/api/notifications', { baseUrl: globals.apiBase })
|
|
2636
|
+
const items = data?.notifications ?? []
|
|
2637
|
+
if (jsonFlag) {
|
|
2638
|
+
console.log(JSON.stringify(items, null, 2))
|
|
2639
|
+
return
|
|
2640
|
+
}
|
|
2641
|
+
if (items.length === 0) {
|
|
2642
|
+
console.log('No notifications. You are caught up.')
|
|
2643
|
+
return
|
|
2644
|
+
}
|
|
2645
|
+
for (const n of items) {
|
|
2646
|
+
console.log(`- [${n.category}] ${n.title}`)
|
|
2647
|
+
}
|
|
2648
|
+
return
|
|
2649
|
+
}
|
|
2650
|
+
case 'digest': {
|
|
2651
|
+
const data = await api('/api/digest/weekly', { baseUrl: globals.apiBase })
|
|
2652
|
+
if (jsonFlag) {
|
|
2653
|
+
console.log(JSON.stringify(data, null, 2))
|
|
2654
|
+
return
|
|
2655
|
+
}
|
|
2656
|
+
const d = data?.digest
|
|
2657
|
+
if (!d) {
|
|
2658
|
+
console.log('No digest available.')
|
|
2659
|
+
return
|
|
2660
|
+
}
|
|
2661
|
+
console.log(`\n${d.userLabel} · ${d.periodLabel}`)
|
|
2662
|
+
console.log(`${d.rank.label}: #${d.rank.position} · top ${100 - d.rank.percentile}%`)
|
|
2663
|
+
if (d.highlights.length) {
|
|
2664
|
+
console.log('\nHighlights:')
|
|
2665
|
+
for (const h of d.highlights) console.log(` - ${h.title}`)
|
|
2666
|
+
}
|
|
2667
|
+
console.log(`\nOpen dashboard: ${DEFAULT_API}${d.callToAction.href}\n`)
|
|
2668
|
+
return
|
|
2669
|
+
}
|
|
2670
|
+
case 'project': {
|
|
2671
|
+
const sub = args[0]
|
|
2672
|
+
if (sub !== 'draft' && sub !== 'publish') {
|
|
2673
|
+
console.log('Usage: dai project <draft|publish> [--repo <url>] [--demo <url>] [--yes] [--json]')
|
|
2674
|
+
return
|
|
2675
|
+
}
|
|
2676
|
+
const token = workflowToken(config)
|
|
2677
|
+
if (!token) {
|
|
2678
|
+
console.log('Missing workflow token. Generate config from /projects/new, set DAI_TOKEN, then run this again.')
|
|
2679
|
+
process.exitCode = 1
|
|
2680
|
+
return
|
|
2681
|
+
}
|
|
2682
|
+
const flags = parseProjectFlags(args.slice(1))
|
|
2683
|
+
const payload = await localProjectPayload(flags)
|
|
2684
|
+
const draft = await workflowApi('/api/workflow/projects/draft', {
|
|
2685
|
+
method: 'POST',
|
|
2686
|
+
body: payload,
|
|
2687
|
+
token,
|
|
2688
|
+
baseUrl: globals.apiBase,
|
|
2689
|
+
})
|
|
2690
|
+
if (sub === 'draft') {
|
|
2691
|
+
if (jsonFlag) {
|
|
2692
|
+
console.log(JSON.stringify(draft, null, 2))
|
|
2693
|
+
return
|
|
2694
|
+
}
|
|
2695
|
+
printDraft(draft)
|
|
2696
|
+
return
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
if (draft.duplicateProjectHref) {
|
|
2700
|
+
const href = draft.duplicateProjectHref.startsWith('http')
|
|
2701
|
+
? draft.duplicateProjectHref
|
|
2702
|
+
: `${globals.apiBase}${draft.duplicateProjectHref}`
|
|
2703
|
+
if (jsonFlag) console.log(JSON.stringify({ duplicateProjectHref: href, draft }, null, 2))
|
|
2704
|
+
else console.log(`Already published: ${href}`)
|
|
2705
|
+
return
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
if (!flags.yes) {
|
|
2709
|
+
if (jsonFlag) {
|
|
2710
|
+
console.log(JSON.stringify({ readyToPublish: false, nextCommand: 'dai project publish --yes', draft }, null, 2))
|
|
2711
|
+
return
|
|
2712
|
+
}
|
|
2713
|
+
printDraft(draft)
|
|
2714
|
+
console.log('\nReview the draft, then publish with: dai project publish --yes')
|
|
2715
|
+
return
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
if (!draft.draft?.tools?.length) {
|
|
2719
|
+
console.log('Could not infer approved tools from this repo. Publish from /projects/new or pass explicit tools through the MCP tool.')
|
|
2720
|
+
process.exitCode = 1
|
|
2721
|
+
return
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
const result = await workflowApi('/api/workflow/projects', {
|
|
2725
|
+
method: 'POST',
|
|
2726
|
+
body: projectBodyFromDraft(draft, flags),
|
|
2727
|
+
token,
|
|
2728
|
+
baseUrl: globals.apiBase,
|
|
2729
|
+
})
|
|
2730
|
+
if (jsonFlag) {
|
|
2731
|
+
console.log(JSON.stringify(result, null, 2))
|
|
2732
|
+
return
|
|
2733
|
+
}
|
|
2734
|
+
const href = result.projectHref?.startsWith('http') ? result.projectHref : `${globals.apiBase}${result.projectHref ?? `/p/${result.project?.slug ?? ''}`}`
|
|
2735
|
+
console.log(`Published: ${href}`)
|
|
2736
|
+
if (result.profileHref) console.log(`Profile: ${globals.apiBase}${result.profileHref}`)
|
|
2737
|
+
if (result.shareMoment?.platformUrls?.x) console.log(`Share on X: ${result.shareMoment.platformUrls.x}`)
|
|
2738
|
+
return
|
|
2739
|
+
}
|
|
2740
|
+
case 'mcp': {
|
|
2741
|
+
const sub = args[0]
|
|
2742
|
+
if (sub !== 'serve') {
|
|
2743
|
+
console.log('Usage: dai mcp serve [--api <url>] [--mode <auto|public|developer|enterprise>]')
|
|
2744
|
+
return
|
|
2745
|
+
}
|
|
2746
|
+
await serveMcp({ baseUrl: globals.apiBase, config, mode: globals.mode })
|
|
2747
|
+
return
|
|
2748
|
+
}
|
|
2749
|
+
default:
|
|
2750
|
+
console.log(`Unknown command: ${cmd}\nRun \`dai --help\` for usage.`)
|
|
2751
|
+
process.exitCode = 1
|
|
2752
|
+
}
|
|
2753
|
+
}
|