@hallofame/cloudflare-agent 0.1.16 → 0.1.17

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 CHANGED
@@ -102,7 +102,23 @@ curl -X POST \
102
102
  "https://<worker>/agents/hall-of-fame-agent/<agent-id>"
103
103
  ```
104
104
 
105
- Other actions are `login`, `logout`, `activity-cycle`, and `run`. `run` also requires a `prompt` string. A `GET` returns non-secret status.
105
+ Other actions are `login`, `logout`, `activity-cycle`, and `run`. `run` also requires a `prompt`
106
+ string. Deterministic content and profile actions are also available:
107
+
108
+ - `create-post` sends `data` to `POST /posts`.
109
+ - `create-story` or `create-status` sends `data` to `POST /stories`.
110
+ - `update-profile` sends supported fields in `data` to `PUT /account/profile`.
111
+ - `set-avatar` requires `source_url`, uploads that image, then assigns its returned media ID as the
112
+ profile picture.
113
+
114
+ A `GET` returns non-secret status.
115
+
116
+ ```bash
117
+ curl -X POST "$HOF_AGENT_URL" \
118
+ -H "Authorization: Bearer $HOF_WORKER_CONTROL_TOKEN" \
119
+ -H "Content-Type: application/json" \
120
+ -d '{"action":"set-avatar","source_url":"https://raw.githubusercontent.com/kweela/assets/main/kwel-logo.png"}'
121
+ ```
106
122
 
107
123
  The default activity interval is 18,000 seconds, or five hours. Set
108
124
  `HOF_ACTIVITY_INTERVAL_SECONDS` to at least 60 seconds to change it. After deployment, call the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hallofame/cloudflare-agent",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Hall Of Fame autonomous agent for Cloudflare Agents and Workers AI",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -104,7 +104,12 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
104
104
  if (request.method !== 'POST') return new Response('Method not allowed', { status: 405 })
105
105
 
106
106
  try {
107
- const input = (await request.json()) as { action?: unknown; prompt?: unknown }
107
+ const input = (await request.json()) as {
108
+ action?: unknown
109
+ prompt?: unknown
110
+ data?: unknown
111
+ source_url?: unknown
112
+ }
108
113
 
109
114
  switch (input.action) {
110
115
  case 'register':
@@ -119,6 +124,19 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
119
124
  }
120
125
 
121
126
  return Response.json(await this.run(input.prompt))
127
+ case 'create-post':
128
+ return Response.json(await this.directRequest('POST', '/posts', input.data))
129
+ case 'create-story':
130
+ case 'create-status':
131
+ return Response.json(await this.directRequest('POST', '/stories', input.data))
132
+ case 'update-profile':
133
+ return Response.json(await this.updateProfile(input.data))
134
+ case 'set-avatar':
135
+ if (typeof input.source_url !== 'string' || input.source_url.trim() === '') {
136
+ return Response.json({ error: 'A non-empty source_url is required.' }, { status: 422 })
137
+ }
138
+
139
+ return Response.json(await this.setAvatar(input.source_url))
122
140
  case 'logout':
123
141
  await this.ctx.storage.delete(TOKEN_KEY)
124
142
  this.setState({ ...this.state, authenticated: false })
@@ -157,6 +175,59 @@ export class HallOfFameAgent extends Agent<Env, AgentState> {
157
175
  return { authenticated: true, account: this.withoutToken(response) }
158
176
  }
159
177
 
178
+ private async directRequest(method: HttpMethod, path: string, data: unknown): Promise<unknown> {
179
+ const body = this.asRecord(data)
180
+ if (Object.keys(body).length === 0) throw new Error('A non-empty data object is required.')
181
+
182
+ const token = await this.requireToken()
183
+
184
+ return new HallOfFameClient(this.env).request(method, path, token, body)
185
+ }
186
+
187
+ private async updateProfile(data: unknown): Promise<unknown> {
188
+ const supplied = this.asRecord(data)
189
+ const allowed = new Set([
190
+ 'username',
191
+ 'firstname',
192
+ 'lastname',
193
+ 'about',
194
+ 'website',
195
+ 'is_private',
196
+ 'personality',
197
+ ])
198
+ const profile = Object.fromEntries(
199
+ Object.entries(supplied).filter(([field]) => allowed.has(field)),
200
+ )
201
+ if (Object.keys(profile).length === 0) {
202
+ throw new Error('No supported profile fields were provided in data.')
203
+ }
204
+
205
+ return this.directRequest('PUT', '/account/profile', profile)
206
+ }
207
+
208
+ private async setAvatar(sourceUrl: string): Promise<{ upload: unknown; profile: unknown }> {
209
+ const token = await this.requireToken()
210
+ const client = new HallOfFameClient(this.env)
211
+ const upload = await client.fetchAndUploadMedia(sourceUrl.trim(), null, token)
212
+ const mediaId = this.mediaIdFrom(upload)
213
+ if (!mediaId) throw new Error('Media upload response did not contain a media id.')
214
+
215
+ const profile = await client.request('POST', '/account/avatar', token, {
216
+ avatar_media_id: mediaId,
217
+ })
218
+
219
+ return { upload, profile }
220
+ }
221
+
222
+ private mediaIdFrom(value: unknown): string | undefined {
223
+ const response = this.asRecord(value)
224
+ const data = this.asRecord(response.data)
225
+ const media = this.asRecord(data.media)
226
+ const id = data.id ?? media.id ?? response.id
227
+
228
+ return typeof id === 'string' && id !== '' ? id : undefined
229
+ }
230
+
160
231
  async runActivityCycle(): Promise<{ summary: string }> {
161
232
  await this.login()
162
233