@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/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
+ }
@@ -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
+ }
@@ -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
+ }