@creatorarmy/openclaw-creatorarmy 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +7 -0
- package/.github/workflows/ci.yml +31 -0
- package/.github/workflows/npm-publish.yml +24 -0
- package/README.md +1 -0
- package/biome.json +78 -0
- package/bun.lock +1095 -0
- package/client.ts +293 -0
- package/commands/cli.ts +108 -0
- package/commands/health.ts +42 -0
- package/commands/slash.ts +86 -0
- package/config.ts +58 -0
- package/creator-army-demand-engine-v2.skill +0 -0
- package/hooks/skill-loader.ts +29 -0
- package/index.ts +62 -0
- package/logger.ts +54 -0
- package/openclaw.plugin.json +33 -0
- package/package.json +26 -0
- package/tools/demand-engine/briefs.ts +110 -0
- package/tools/demand-engine/context.ts +93 -0
- package/tools/demand-engine/excavation.ts +74 -0
- package/tools/demand-engine/references.ts +55 -0
- package/tools/demand-engine/scripts.ts +124 -0
- package/tsconfig.json +22 -0
package/client.ts
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { log } from "./logger.ts"
|
|
2
|
+
|
|
3
|
+
export type Reference = {
|
|
4
|
+
slug: string
|
|
5
|
+
filename: string
|
|
6
|
+
content?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type BrandContext = {
|
|
10
|
+
_id: string
|
|
11
|
+
email?: string
|
|
12
|
+
brandName: string
|
|
13
|
+
brandUrl?: string
|
|
14
|
+
personality?: string
|
|
15
|
+
tone?: string
|
|
16
|
+
visualStyle?: string
|
|
17
|
+
icp?: string
|
|
18
|
+
customerTypes?: string[]
|
|
19
|
+
barriers?: string[]
|
|
20
|
+
motivators?: string[]
|
|
21
|
+
notes?: string
|
|
22
|
+
createdAt?: string
|
|
23
|
+
updatedAt?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type CustomerType = {
|
|
27
|
+
name: string
|
|
28
|
+
description?: string
|
|
29
|
+
barriers?: string[]
|
|
30
|
+
motivators?: string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type Excavation = {
|
|
34
|
+
_id: string
|
|
35
|
+
brandName: string
|
|
36
|
+
customerTypes?: CustomerType[]
|
|
37
|
+
coreProblem?: string
|
|
38
|
+
failedSolutions?: string
|
|
39
|
+
desiredOutcome?: string
|
|
40
|
+
uniqueMechanism?: string
|
|
41
|
+
notes?: string
|
|
42
|
+
createdAt?: string
|
|
43
|
+
updatedAt?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type SellingSequence = {
|
|
47
|
+
rapport?: string
|
|
48
|
+
openLoop?: string
|
|
49
|
+
valueStack?: string
|
|
50
|
+
wowMoment?: string
|
|
51
|
+
cta?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type Brief = {
|
|
55
|
+
_id: string
|
|
56
|
+
brandName: string
|
|
57
|
+
title: string
|
|
58
|
+
customerType?: string
|
|
59
|
+
sellingSequence?: SellingSequence
|
|
60
|
+
hooks?: string[]
|
|
61
|
+
buildingBlocks?: string[]
|
|
62
|
+
creativeType?: string
|
|
63
|
+
platform?: string
|
|
64
|
+
duration?: number
|
|
65
|
+
notes?: string
|
|
66
|
+
createdAt?: string
|
|
67
|
+
updatedAt?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type PersuasionChecklist = {
|
|
71
|
+
ethos?: boolean
|
|
72
|
+
logos?: boolean
|
|
73
|
+
pathos?: boolean
|
|
74
|
+
metaphor?: boolean
|
|
75
|
+
brevity?: boolean
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type Script = {
|
|
79
|
+
_id: string
|
|
80
|
+
brandName: string
|
|
81
|
+
briefId?: string
|
|
82
|
+
title: string
|
|
83
|
+
customerType?: string
|
|
84
|
+
format?: string
|
|
85
|
+
platform?: string
|
|
86
|
+
duration?: number
|
|
87
|
+
hook?: string
|
|
88
|
+
openLoop?: string
|
|
89
|
+
body?: string
|
|
90
|
+
cta?: string
|
|
91
|
+
sellingSequence?: SellingSequence
|
|
92
|
+
buildingBlocks?: string[]
|
|
93
|
+
persuasionChecklist?: PersuasionChecklist
|
|
94
|
+
notes?: string
|
|
95
|
+
createdAt?: string
|
|
96
|
+
updatedAt?: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class CreatorArmyClient {
|
|
100
|
+
private apiKey: string
|
|
101
|
+
private baseUrl: string
|
|
102
|
+
|
|
103
|
+
constructor(apiKey: string, baseUrl: string) {
|
|
104
|
+
if (!apiKey) {
|
|
105
|
+
throw new Error("Creator Army API key is required")
|
|
106
|
+
}
|
|
107
|
+
this.apiKey = apiKey
|
|
108
|
+
this.baseUrl = baseUrl
|
|
109
|
+
log.info(`client initialized (${baseUrl})`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private async request<T>(
|
|
113
|
+
method: string,
|
|
114
|
+
path: string,
|
|
115
|
+
body?: Record<string, unknown>,
|
|
116
|
+
): Promise<T> {
|
|
117
|
+
log.debugRequest(`${method} ${path}`, body ?? {})
|
|
118
|
+
|
|
119
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
120
|
+
method,
|
|
121
|
+
headers: {
|
|
122
|
+
"Content-Type": "application/json",
|
|
123
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
124
|
+
},
|
|
125
|
+
...(body && { body: JSON.stringify(body) }),
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
const text = await response.text()
|
|
130
|
+
throw new Error(`Creator Army API error (${response.status}): ${text}`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const data = (await response.json()) as T
|
|
134
|
+
log.debugResponse(`${method} ${path}`, data)
|
|
135
|
+
return data
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Health
|
|
139
|
+
|
|
140
|
+
async health(): Promise<{ ok: boolean; message?: string }> {
|
|
141
|
+
try {
|
|
142
|
+
const data = await this.request<{ status: string; keyPrefix?: string }>(
|
|
143
|
+
"GET",
|
|
144
|
+
"/api/plugin/health",
|
|
145
|
+
)
|
|
146
|
+
return { ok: data.status === "ok" }
|
|
147
|
+
} catch (err) {
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
message: err instanceof Error ? err.message : "Unknown error",
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// References
|
|
156
|
+
|
|
157
|
+
async listReferences(): Promise<Reference[]> {
|
|
158
|
+
const data = await this.request<{ references: Reference[] }>(
|
|
159
|
+
"GET",
|
|
160
|
+
"/api/plugin/demand-engine/references",
|
|
161
|
+
)
|
|
162
|
+
return data.references
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async getReference(slug: string): Promise<Reference> {
|
|
166
|
+
return this.request<Reference>(
|
|
167
|
+
"GET",
|
|
168
|
+
`/api/plugin/demand-engine/references?ref=${encodeURIComponent(slug)}`,
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Brand Context
|
|
173
|
+
|
|
174
|
+
async listBrands(): Promise<BrandContext[]> {
|
|
175
|
+
const data = await this.request<{ brands: BrandContext[] }>(
|
|
176
|
+
"GET",
|
|
177
|
+
"/api/plugin/demand-engine/context",
|
|
178
|
+
)
|
|
179
|
+
return data.brands
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async getBrandContext(brand: string): Promise<BrandContext | null> {
|
|
183
|
+
const data = await this.request<{ context: BrandContext | null }>(
|
|
184
|
+
"GET",
|
|
185
|
+
`/api/plugin/demand-engine/context?brand=${encodeURIComponent(brand)}`,
|
|
186
|
+
)
|
|
187
|
+
return data.context
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async saveBrandContext(
|
|
191
|
+
context: Record<string, unknown>,
|
|
192
|
+
): Promise<BrandContext> {
|
|
193
|
+
const data = await this.request<{ context: BrandContext }>(
|
|
194
|
+
"POST",
|
|
195
|
+
"/api/plugin/demand-engine/context",
|
|
196
|
+
context,
|
|
197
|
+
)
|
|
198
|
+
return data.context
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Excavation
|
|
202
|
+
|
|
203
|
+
async getExcavation(brand: string): Promise<Excavation | null> {
|
|
204
|
+
const data = await this.request<{ excavation: Excavation | null }>(
|
|
205
|
+
"GET",
|
|
206
|
+
`/api/plugin/demand-engine/excavation?brand=${encodeURIComponent(brand)}`,
|
|
207
|
+
)
|
|
208
|
+
return data.excavation
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async saveExcavation(
|
|
212
|
+
excavation: Record<string, unknown>,
|
|
213
|
+
): Promise<Excavation> {
|
|
214
|
+
const data = await this.request<{ excavation: Excavation }>(
|
|
215
|
+
"POST",
|
|
216
|
+
"/api/plugin/demand-engine/excavation",
|
|
217
|
+
excavation,
|
|
218
|
+
)
|
|
219
|
+
return data.excavation
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Briefs
|
|
223
|
+
|
|
224
|
+
async listBriefs(params?: {
|
|
225
|
+
brand?: string
|
|
226
|
+
limit?: number
|
|
227
|
+
offset?: number
|
|
228
|
+
}): Promise<{ briefs: Brief[]; total: number }> {
|
|
229
|
+
const query = new URLSearchParams()
|
|
230
|
+
if (params?.brand) query.set("brand", params.brand)
|
|
231
|
+
if (params?.limit) query.set("limit", String(params.limit))
|
|
232
|
+
if (params?.offset) query.set("offset", String(params.offset))
|
|
233
|
+
const qs = query.toString()
|
|
234
|
+
return this.request<{ briefs: Brief[]; total: number }>(
|
|
235
|
+
"GET",
|
|
236
|
+
`/api/plugin/demand-engine/briefs${qs ? `?${qs}` : ""}`,
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async getBrief(id: string): Promise<Brief | null> {
|
|
241
|
+
const data = await this.request<{ brief: Brief | null }>(
|
|
242
|
+
"GET",
|
|
243
|
+
`/api/plugin/demand-engine/briefs?id=${encodeURIComponent(id)}`,
|
|
244
|
+
)
|
|
245
|
+
return data.brief
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async saveBrief(brief: Record<string, unknown>): Promise<Brief> {
|
|
249
|
+
const data = await this.request<{ brief: Brief }>(
|
|
250
|
+
"POST",
|
|
251
|
+
"/api/plugin/demand-engine/briefs",
|
|
252
|
+
brief,
|
|
253
|
+
)
|
|
254
|
+
return data.brief
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Scripts
|
|
258
|
+
|
|
259
|
+
async listScripts(params?: {
|
|
260
|
+
brand?: string
|
|
261
|
+
briefId?: string
|
|
262
|
+
limit?: number
|
|
263
|
+
offset?: number
|
|
264
|
+
}): Promise<{ scripts: Script[]; total: number }> {
|
|
265
|
+
const query = new URLSearchParams()
|
|
266
|
+
if (params?.brand) query.set("brand", params.brand)
|
|
267
|
+
if (params?.briefId) query.set("briefId", params.briefId)
|
|
268
|
+
if (params?.limit) query.set("limit", String(params.limit))
|
|
269
|
+
if (params?.offset) query.set("offset", String(params.offset))
|
|
270
|
+
const qs = query.toString()
|
|
271
|
+
return this.request<{ scripts: Script[]; total: number }>(
|
|
272
|
+
"GET",
|
|
273
|
+
`/api/plugin/demand-engine/scripts${qs ? `?${qs}` : ""}`,
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async getScript(id: string): Promise<Script | null> {
|
|
278
|
+
const data = await this.request<{ script: Script | null }>(
|
|
279
|
+
"GET",
|
|
280
|
+
`/api/plugin/demand-engine/scripts?id=${encodeURIComponent(id)}`,
|
|
281
|
+
)
|
|
282
|
+
return data.script
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async saveScript(script: Record<string, unknown>): Promise<Script> {
|
|
286
|
+
const data = await this.request<{ script: Script }>(
|
|
287
|
+
"POST",
|
|
288
|
+
"/api/plugin/demand-engine/scripts",
|
|
289
|
+
script,
|
|
290
|
+
)
|
|
291
|
+
return data.script
|
|
292
|
+
}
|
|
293
|
+
}
|
package/commands/cli.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as fs from "node:fs"
|
|
2
|
+
import * as os from "node:os"
|
|
3
|
+
import * as path from "node:path"
|
|
4
|
+
import * as readline from "node:readline"
|
|
5
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
6
|
+
import type { CreatorArmyClient } from "../client.ts"
|
|
7
|
+
import type { CreatorArmyConfig } from "../config.ts"
|
|
8
|
+
import { parseConfig } from "../config.ts"
|
|
9
|
+
import { registerHealthCommand } from "./health.ts"
|
|
10
|
+
|
|
11
|
+
export function registerCliSetup(api: OpenClawPluginApi): void {
|
|
12
|
+
api.registerCli(
|
|
13
|
+
// biome-ignore lint/suspicious/noExplicitAny: openclaw SDK does not ship types
|
|
14
|
+
({ program }: { program: any }) => {
|
|
15
|
+
const cmd = program
|
|
16
|
+
.command("creator-army")
|
|
17
|
+
.description("Creator Army plugin commands")
|
|
18
|
+
|
|
19
|
+
cmd
|
|
20
|
+
.command("setup")
|
|
21
|
+
.description("Configure Creator Army API key")
|
|
22
|
+
.action(async () => {
|
|
23
|
+
const configDir = path.join(os.homedir(), ".openclaw")
|
|
24
|
+
const configPath = path.join(configDir, "openclaw.json")
|
|
25
|
+
|
|
26
|
+
console.log("\nCreator Army Setup\n")
|
|
27
|
+
console.log("Enter your Creator Army API key:\n")
|
|
28
|
+
|
|
29
|
+
const rl = readline.createInterface({
|
|
30
|
+
input: process.stdin,
|
|
31
|
+
output: process.stdout,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const apiKey = await new Promise<string>((resolve) => {
|
|
35
|
+
rl.question("API key: ", resolve)
|
|
36
|
+
})
|
|
37
|
+
rl.close()
|
|
38
|
+
|
|
39
|
+
if (!apiKey.trim()) {
|
|
40
|
+
console.log("\nNo API key provided. Setup cancelled.")
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let config: Record<string, unknown> = {}
|
|
45
|
+
if (fs.existsSync(configPath)) {
|
|
46
|
+
try {
|
|
47
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf-8"))
|
|
48
|
+
} catch {
|
|
49
|
+
config = {}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!config.plugins) config.plugins = {}
|
|
54
|
+
const plugins = config.plugins as Record<string, unknown>
|
|
55
|
+
if (!plugins.entries) plugins.entries = {}
|
|
56
|
+
const entries = plugins.entries as Record<string, unknown>
|
|
57
|
+
|
|
58
|
+
entries["openclaw-creatorarmy"] = {
|
|
59
|
+
enabled: true,
|
|
60
|
+
config: {
|
|
61
|
+
apiKey: apiKey.trim(),
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!fs.existsSync(configDir)) {
|
|
66
|
+
fs.mkdirSync(configDir, { recursive: true })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2))
|
|
70
|
+
|
|
71
|
+
console.log("\nAPI key saved to ~/.openclaw/openclaw.json")
|
|
72
|
+
console.log(
|
|
73
|
+
"Restart OpenClaw to apply changes: openclaw gateway --force\n",
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
cmd
|
|
78
|
+
.command("status")
|
|
79
|
+
.description("Check Creator Army configuration status")
|
|
80
|
+
.action(async () => {
|
|
81
|
+
const cfg = parseConfig(api.pluginConfig)
|
|
82
|
+
|
|
83
|
+
console.log("\nCreator Army Status\n")
|
|
84
|
+
|
|
85
|
+
if (!cfg.apiKey) {
|
|
86
|
+
console.log("No API key configured")
|
|
87
|
+
console.log("Run: openclaw creator-army setup\n")
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const display = `${cfg.apiKey.slice(0, 8)}...${cfg.apiKey.slice(-4)}`
|
|
92
|
+
console.log(`API key: ${display}`)
|
|
93
|
+
console.log(`Base URL: ${cfg.baseUrl}`)
|
|
94
|
+
console.log(`Debug: ${cfg.debug}`)
|
|
95
|
+
console.log("")
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
registerHealthCommand(cmd, api)
|
|
99
|
+
},
|
|
100
|
+
{ commands: ["creator-army"] },
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function registerCli(
|
|
105
|
+
_api: OpenClawPluginApi,
|
|
106
|
+
_client: CreatorArmyClient,
|
|
107
|
+
_cfg: CreatorArmyConfig,
|
|
108
|
+
): void {}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
2
|
+
import { parseConfig } from "../config.ts"
|
|
3
|
+
|
|
4
|
+
export function registerHealthCommand(
|
|
5
|
+
// biome-ignore lint/suspicious/noExplicitAny: openclaw SDK does not ship types
|
|
6
|
+
cmd: any,
|
|
7
|
+
api: OpenClawPluginApi,
|
|
8
|
+
): void {
|
|
9
|
+
cmd
|
|
10
|
+
.command("health")
|
|
11
|
+
.description("Check Creator Army API health and verify API key")
|
|
12
|
+
.action(async () => {
|
|
13
|
+
const cfg = parseConfig(api.pluginConfig)
|
|
14
|
+
if (!cfg.apiKey) {
|
|
15
|
+
console.log("\nNo API key configured. Run: openclaw creator-army setup\n")
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(`\nChecking ${cfg.baseUrl}/api/plugin/health ...`)
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(`${cfg.baseUrl}/api/plugin/health`, {
|
|
22
|
+
headers: { Authorization: `Bearer ${cfg.apiKey}` },
|
|
23
|
+
})
|
|
24
|
+
const text = await response.text()
|
|
25
|
+
console.log(`Status: ${response.status}`)
|
|
26
|
+
console.log(`Response: ${text}`)
|
|
27
|
+
try {
|
|
28
|
+
const data = JSON.parse(text) as Record<string, unknown>
|
|
29
|
+
if (data.status === "ok") {
|
|
30
|
+
console.log("API is healthy and API key is valid.\n")
|
|
31
|
+
} else {
|
|
32
|
+
console.log(`API check failed: ${data.error ?? data.message ?? data.status ?? "Unknown error"}\n`)
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
console.log("")
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const msg = err instanceof Error ? err.message : "Unknown error"
|
|
39
|
+
console.log(`Connection failed: ${msg}\n`)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
2
|
+
import type { CreatorArmyClient } from "../client.ts"
|
|
3
|
+
import type { CreatorArmyConfig } from "../config.ts"
|
|
4
|
+
import { log } from "../logger.ts"
|
|
5
|
+
|
|
6
|
+
export function registerStubCommands(api: OpenClawPluginApi): void {
|
|
7
|
+
api.registerCommand({
|
|
8
|
+
name: "health",
|
|
9
|
+
description: "Check Creator Army API health",
|
|
10
|
+
acceptsArgs: false,
|
|
11
|
+
requireAuth: false,
|
|
12
|
+
handler: async () => {
|
|
13
|
+
return {
|
|
14
|
+
text: "Creator Army not configured. Set your API key first.",
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function registerCommands(
|
|
21
|
+
api: OpenClawPluginApi,
|
|
22
|
+
client: CreatorArmyClient,
|
|
23
|
+
_cfg: CreatorArmyConfig,
|
|
24
|
+
): void {
|
|
25
|
+
api.registerCommand({
|
|
26
|
+
name: "health",
|
|
27
|
+
description: "Check Creator Army API health and verify API key",
|
|
28
|
+
acceptsArgs: false,
|
|
29
|
+
requireAuth: false,
|
|
30
|
+
handler: async () => {
|
|
31
|
+
try {
|
|
32
|
+
const result = await client.health()
|
|
33
|
+
if (result.ok) {
|
|
34
|
+
return { text: "Creator Army API is healthy and API key is valid." }
|
|
35
|
+
}
|
|
36
|
+
return { text: `Creator Army API check failed: ${result.message}` }
|
|
37
|
+
} catch (err) {
|
|
38
|
+
log.error("/health failed", err)
|
|
39
|
+
return { text: "Failed to reach Creator Army API." }
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
api.registerCommand({
|
|
45
|
+
name: "brands",
|
|
46
|
+
description: "List all brands in Creator Army",
|
|
47
|
+
acceptsArgs: false,
|
|
48
|
+
requireAuth: true,
|
|
49
|
+
handler: async () => {
|
|
50
|
+
try {
|
|
51
|
+
const brands = await client.listBrands()
|
|
52
|
+
if (brands.length === 0) {
|
|
53
|
+
return { text: "No brands found." }
|
|
54
|
+
}
|
|
55
|
+
const list = brands.map((b) => `- ${b.brandName}`).join("\n")
|
|
56
|
+
return { text: `Brands:\n\n${list}` }
|
|
57
|
+
} catch (err) {
|
|
58
|
+
log.error("/brands failed", err)
|
|
59
|
+
return { text: "Failed to list brands." }
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
api.registerCommand({
|
|
65
|
+
name: "briefs",
|
|
66
|
+
description: "List creative briefs (optionally pass brand name)",
|
|
67
|
+
acceptsArgs: true,
|
|
68
|
+
requireAuth: true,
|
|
69
|
+
handler: async (ctx: { args?: string }) => {
|
|
70
|
+
try {
|
|
71
|
+
const brand = ctx.args?.trim() || undefined
|
|
72
|
+
const data = await client.listBriefs({ brand })
|
|
73
|
+
if (data.briefs.length === 0) {
|
|
74
|
+
return { text: brand ? `No briefs found for "${brand}".` : "No briefs found." }
|
|
75
|
+
}
|
|
76
|
+
const list = data.briefs
|
|
77
|
+
.map((b) => `- **${b.title}** (${b.brandName}) — ${b.creativeType ?? "untyped"}`)
|
|
78
|
+
.join("\n")
|
|
79
|
+
return { text: `${data.total} brief(s):\n\n${list}` }
|
|
80
|
+
} catch (err) {
|
|
81
|
+
log.error("/briefs failed", err)
|
|
82
|
+
return { text: "Failed to list briefs." }
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
}
|
package/config.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export type CreatorArmyConfig = {
|
|
2
|
+
apiKey: string | undefined
|
|
3
|
+
baseUrl: string
|
|
4
|
+
debug: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const ALLOWED_KEYS = ["apiKey", "baseUrl", "debug"]
|
|
8
|
+
|
|
9
|
+
function assertAllowedKeys(
|
|
10
|
+
value: Record<string, unknown>,
|
|
11
|
+
allowed: string[],
|
|
12
|
+
label: string,
|
|
13
|
+
): void {
|
|
14
|
+
const unknown = Object.keys(value).filter((k) => !allowed.includes(k))
|
|
15
|
+
if (unknown.length > 0) {
|
|
16
|
+
throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseConfig(raw: unknown): CreatorArmyConfig {
|
|
21
|
+
const cfg =
|
|
22
|
+
raw && typeof raw === "object" && !Array.isArray(raw)
|
|
23
|
+
? (raw as Record<string, unknown>)
|
|
24
|
+
: {}
|
|
25
|
+
|
|
26
|
+
if (Object.keys(cfg).length > 0) {
|
|
27
|
+
assertAllowedKeys(cfg, ALLOWED_KEYS, "creator-army config")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const apiKey =
|
|
31
|
+
typeof cfg.apiKey === "string" && cfg.apiKey.length > 0
|
|
32
|
+
? cfg.apiKey
|
|
33
|
+
: undefined
|
|
34
|
+
|
|
35
|
+
const baseUrl =
|
|
36
|
+
typeof cfg.baseUrl === "string" && cfg.baseUrl.length > 0
|
|
37
|
+
? cfg.baseUrl.replace(/\/+$/, "")
|
|
38
|
+
: "https://b2a9-2403-580c-1dcc-0-181c-7d6e-4733-3836.ngrok-free.app"
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
apiKey,
|
|
42
|
+
baseUrl,
|
|
43
|
+
debug: (cfg.debug as boolean) ?? false,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const creatorArmyConfigSchema = {
|
|
48
|
+
jsonSchema: {
|
|
49
|
+
type: "object",
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
properties: {
|
|
52
|
+
apiKey: { type: "string" },
|
|
53
|
+
baseUrl: { type: "string" },
|
|
54
|
+
debug: { type: "boolean" },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
parse: parseConfig,
|
|
58
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { CreatorArmyClient } from "../client.ts"
|
|
2
|
+
import type { CreatorArmyConfig } from "../config.ts"
|
|
3
|
+
import { log } from "../logger.ts"
|
|
4
|
+
|
|
5
|
+
let cachedSkill: string | null = null
|
|
6
|
+
|
|
7
|
+
export function buildSkillLoaderHandler(
|
|
8
|
+
client: CreatorArmyClient,
|
|
9
|
+
_cfg: CreatorArmyConfig,
|
|
10
|
+
) {
|
|
11
|
+
return async () => {
|
|
12
|
+
if (cachedSkill) {
|
|
13
|
+
return { prependContext: cachedSkill }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const ref = await client.getReference("skill")
|
|
18
|
+
if (ref.content) {
|
|
19
|
+
cachedSkill = ref.content
|
|
20
|
+
log.debug(`loaded SKILL.md (${cachedSkill.length} chars)`)
|
|
21
|
+
return { prependContext: cachedSkill }
|
|
22
|
+
}
|
|
23
|
+
} catch (err) {
|
|
24
|
+
log.error("failed to load SKILL.md", err)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
2
|
+
import { CreatorArmyClient } from "./client.ts"
|
|
3
|
+
import { registerCli, registerCliSetup } from "./commands/cli.ts"
|
|
4
|
+
import { registerCommands, registerStubCommands } from "./commands/slash.ts"
|
|
5
|
+
import { parseConfig, creatorArmyConfigSchema } from "./config.ts"
|
|
6
|
+
import { buildSkillLoaderHandler } from "./hooks/skill-loader.ts"
|
|
7
|
+
import { initLogger } from "./logger.ts"
|
|
8
|
+
import { registerBriefTools } from "./tools/demand-engine/briefs.ts"
|
|
9
|
+
import { registerContextTools } from "./tools/demand-engine/context.ts"
|
|
10
|
+
import { registerExcavationTools } from "./tools/demand-engine/excavation.ts"
|
|
11
|
+
import { registerReferenceTools } from "./tools/demand-engine/references.ts"
|
|
12
|
+
import { registerScriptTools } from "./tools/demand-engine/scripts.ts"
|
|
13
|
+
|
|
14
|
+
export default {
|
|
15
|
+
id: "openclaw-creatorarmy",
|
|
16
|
+
name: "Creator Army",
|
|
17
|
+
description: "OpenClaw Creator Army plugin — demand engine for short-form video ads and organic content",
|
|
18
|
+
kind: "tool" as const,
|
|
19
|
+
configSchema: creatorArmyConfigSchema,
|
|
20
|
+
|
|
21
|
+
register(api: OpenClawPluginApi) {
|
|
22
|
+
const cfg = parseConfig(api.pluginConfig)
|
|
23
|
+
|
|
24
|
+
initLogger(api.logger, cfg.debug)
|
|
25
|
+
|
|
26
|
+
registerCliSetup(api)
|
|
27
|
+
|
|
28
|
+
if (!cfg.apiKey) {
|
|
29
|
+
api.logger.info(
|
|
30
|
+
"creator-army: not configured - run 'openclaw creator-army setup'",
|
|
31
|
+
)
|
|
32
|
+
registerStubCommands(api)
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const client = new CreatorArmyClient(cfg.apiKey, cfg.baseUrl)
|
|
37
|
+
|
|
38
|
+
// Load SKILL.md into agent context on every conversation start
|
|
39
|
+
api.on("before_agent_start", buildSkillLoaderHandler(client, cfg))
|
|
40
|
+
|
|
41
|
+
// Demand Engine tools
|
|
42
|
+
registerReferenceTools(api, client, cfg)
|
|
43
|
+
registerContextTools(api, client, cfg)
|
|
44
|
+
registerExcavationTools(api, client, cfg)
|
|
45
|
+
registerBriefTools(api, client, cfg)
|
|
46
|
+
registerScriptTools(api, client, cfg)
|
|
47
|
+
|
|
48
|
+
// Slash commands & CLI
|
|
49
|
+
registerCommands(api, client, cfg)
|
|
50
|
+
registerCli(api, client, cfg)
|
|
51
|
+
|
|
52
|
+
api.registerService({
|
|
53
|
+
id: "openclaw-creatorarmy",
|
|
54
|
+
start: () => {
|
|
55
|
+
api.logger.info("creator-army: connected")
|
|
56
|
+
},
|
|
57
|
+
stop: () => {
|
|
58
|
+
api.logger.info("creator-army: stopped")
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
},
|
|
62
|
+
}
|