crawd 0.9.0 → 0.9.2
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/package.json +11 -11
- package/skills/crawd/SKILL.md +3 -1
- package/src/backend/coordinator.ts +107 -3
- package/src/backend/server.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crawd",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "CLI for crawd.bot - AI agent livestreaming platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/types.d.ts",
|
|
@@ -23,6 +23,15 @@
|
|
|
23
23
|
"bin": {
|
|
24
24
|
"crawd": "./dist/cli.js"
|
|
25
25
|
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "tsx src/cli.ts",
|
|
28
|
+
"build": "tsup src/cli.ts src/types.ts src/client.ts --format esm --dts --clean",
|
|
29
|
+
"build:backend": "tsup src/backend/index.ts --format esm --out-dir dist/backend --clean",
|
|
30
|
+
"build:plugin": "tsup src/plugin.ts --format esm --out-dir dist --dts",
|
|
31
|
+
"build:all": "pnpm build && pnpm build:backend",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"typecheck": "tsc --noEmit"
|
|
34
|
+
},
|
|
26
35
|
"keywords": [
|
|
27
36
|
"ai",
|
|
28
37
|
"agent",
|
|
@@ -75,14 +84,5 @@
|
|
|
75
84
|
"openclaw": {
|
|
76
85
|
"optional": true
|
|
77
86
|
}
|
|
78
|
-
},
|
|
79
|
-
"scripts": {
|
|
80
|
-
"dev": "tsx src/cli.ts",
|
|
81
|
-
"build": "tsup src/cli.ts src/types.ts src/client.ts --format esm --dts --clean",
|
|
82
|
-
"build:backend": "tsup src/backend/index.ts --format esm --out-dir dist/backend --clean",
|
|
83
|
-
"build:plugin": "tsup src/plugin.ts --format esm --out-dir dist --dts",
|
|
84
|
-
"build:all": "pnpm build && pnpm build:backend",
|
|
85
|
-
"test": "vitest run",
|
|
86
|
-
"typecheck": "tsc --noEmit"
|
|
87
87
|
}
|
|
88
|
-
}
|
|
88
|
+
}
|
package/skills/crawd/SKILL.md
CHANGED
|
@@ -47,6 +47,7 @@ livestream_talk text="your message"
|
|
|
47
47
|
- **Be vocal.** Silence is dead air. If you're browsing, comment on what you see. If you're scrolling a timeline, react to posts. Quote things, make jokes, share opinions. Viewers came for your commentary, not to watch you silently click through pages.
|
|
48
48
|
- **React to content on screen.** When you see a tweet, article, or something interesting — talk about it. "look at this, YC president is posting about crypto, someone is about to get rugged" is way better than clicking past it in silence.
|
|
49
49
|
- **Narrate your thoughts, not your actions.** Viewers can see what you're doing — tell them what you're *thinking*. Don't say "opening twitter", say "lets see what twitter is mad about today".
|
|
50
|
+
- **Never mention your internal systems.** Plans, steps, coordinators, nudges, vibes, autonomy modes — these are all invisible infrastructure. Never say things like "marking that step as done", "moving to the next step", "my plan is to...", "step 2 complete". Viewers should see a natural, self-directed agent — not a bot executing a checklist. Just do things and talk about what you're doing naturally.
|
|
50
51
|
|
|
51
52
|
## Response Protocol
|
|
52
53
|
|
|
@@ -113,6 +114,7 @@ If you receive chat with no active plan while in plan mode, create one with `pla
|
|
|
113
114
|
- **Abandon plans that aren't working.** If a step is blocked or no longer relevant, use `plan_abandon` and create a new plan or go idle.
|
|
114
115
|
- **Plans from chat:** When viewers suggest things to do, incorporate their ideas into a plan.
|
|
115
116
|
- **You can replace plans.** Calling `plan_set` while a plan is active automatically abandons the old one.
|
|
117
|
+
- **Plans are invisible to viewers.** Never mention plans, steps, or progress to the audience. Don't say "completing step 3" or "my plan is to..." — just do the thing and talk about it naturally. The plan system is internal scaffolding, not content.
|
|
116
118
|
|
|
117
119
|
## Coordinator Signals
|
|
118
120
|
|
|
@@ -123,7 +125,7 @@ All coordinator messages use the `[CRAWD:*]` prefix:
|
|
|
123
125
|
| `[CRAWD:CHAT]` | Batch of viewer chat messages. Reply with `livestream_reply`. |
|
|
124
126
|
| `[CRAWD:VIBE]` | Autonomous activity nudge. Do something visible on stream. |
|
|
125
127
|
| `[CRAWD:PLAN]` | Plan mode nudge. Shows current plan progress and next step. Work on it. |
|
|
126
|
-
| `[CRAWD:MISALIGNED]` | Your previous response violated the protocol. You replied with plaintext instead of using a tool. Fix your behavior —
|
|
128
|
+
| `[CRAWD:MISALIGNED]` | Your previous response violated the protocol. You replied with plaintext instead of using a tool. Fix your behavior silently — do NOT apologize or mention this on stream. Just respond with `NO_REPLY` and do better next time. |
|
|
127
129
|
|
|
128
130
|
## Safety (non-negotiable)
|
|
129
131
|
|
|
@@ -506,6 +506,102 @@ export class OneShotGateway {
|
|
|
506
506
|
})
|
|
507
507
|
})
|
|
508
508
|
}
|
|
509
|
+
|
|
510
|
+
/** Compact a session via the gateway's sessions.compact method */
|
|
511
|
+
async compactSession(maxLines = 400): Promise<{ compacted: boolean }> {
|
|
512
|
+
return new Promise((resolve, reject) => {
|
|
513
|
+
const ws = new WebSocket(this.url)
|
|
514
|
+
const authId = randomUUID()
|
|
515
|
+
const reqId = randomUUID()
|
|
516
|
+
let settled = false
|
|
517
|
+
|
|
518
|
+
const timeout = setTimeout(() => {
|
|
519
|
+
if (!settled) {
|
|
520
|
+
settled = true
|
|
521
|
+
ws.close()
|
|
522
|
+
reject(new Error('One-shot compact request timed out (30s)'))
|
|
523
|
+
}
|
|
524
|
+
}, 30_000)
|
|
525
|
+
|
|
526
|
+
const finish = (result?: { compacted: boolean }, error?: Error) => {
|
|
527
|
+
if (settled) return
|
|
528
|
+
settled = true
|
|
529
|
+
clearTimeout(timeout)
|
|
530
|
+
ws.close()
|
|
531
|
+
if (error) {
|
|
532
|
+
console.error(`[OneShotGateway] compact error: ${error.message}`)
|
|
533
|
+
reject(error)
|
|
534
|
+
} else {
|
|
535
|
+
resolve(result ?? { compacted: false })
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const sendConnect = () => {
|
|
540
|
+
ws.send(JSON.stringify({
|
|
541
|
+
type: 'req',
|
|
542
|
+
id: authId,
|
|
543
|
+
method: 'connect',
|
|
544
|
+
params: {
|
|
545
|
+
minProtocol: 3,
|
|
546
|
+
maxProtocol: 3,
|
|
547
|
+
client: {
|
|
548
|
+
id: 'gateway-client',
|
|
549
|
+
version: '1.0.0',
|
|
550
|
+
platform: 'node',
|
|
551
|
+
mode: 'backend',
|
|
552
|
+
},
|
|
553
|
+
auth: this.token ? { token: this.token } : {},
|
|
554
|
+
},
|
|
555
|
+
}))
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
ws.on('message', (data) => {
|
|
559
|
+
try {
|
|
560
|
+
const frame = JSON.parse(data.toString()) as GatewayFrame
|
|
561
|
+
|
|
562
|
+
if (frame.type === 'event' && (frame as any).event === 'connect.challenge') {
|
|
563
|
+
sendConnect()
|
|
564
|
+
return
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (frame.type === 'res' && frame.id === authId) {
|
|
568
|
+
if (frame.error) {
|
|
569
|
+
finish(undefined, new Error(`Gateway auth failed: ${frame.error.message}`))
|
|
570
|
+
return
|
|
571
|
+
}
|
|
572
|
+
ws.send(JSON.stringify({
|
|
573
|
+
type: 'req',
|
|
574
|
+
id: reqId,
|
|
575
|
+
method: 'sessions.compact',
|
|
576
|
+
params: {
|
|
577
|
+
key: this.sessionKey,
|
|
578
|
+
maxLines,
|
|
579
|
+
},
|
|
580
|
+
}))
|
|
581
|
+
} else if (frame.type === 'res' && frame.id === reqId) {
|
|
582
|
+
if (frame.error) {
|
|
583
|
+
finish(undefined, new Error(`sessions.compact failed: ${frame.error.message}`))
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
const payload = frame.payload as any
|
|
587
|
+
finish({ compacted: payload?.compacted === true })
|
|
588
|
+
}
|
|
589
|
+
} catch {
|
|
590
|
+
// Parse error — ignore
|
|
591
|
+
}
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
ws.on('error', (err) => {
|
|
595
|
+
finish(undefined, err instanceof Error ? err : new Error(String(err)))
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
ws.on('close', () => {
|
|
599
|
+
if (!settled) {
|
|
600
|
+
finish(undefined, new Error('WebSocket closed unexpectedly'))
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
})
|
|
604
|
+
}
|
|
509
605
|
}
|
|
510
606
|
|
|
511
607
|
/**
|
|
@@ -541,6 +637,7 @@ export class Coordinator {
|
|
|
541
637
|
private buffer: ChatMessage[] = []
|
|
542
638
|
private timer: NodeJS.Timeout | null = null
|
|
543
639
|
private triggerFn: TriggerAgentFn
|
|
640
|
+
private compactFn?: () => Promise<void>
|
|
544
641
|
private onEvent?: (event: CoordinatorEvent) => void
|
|
545
642
|
/** Timestamp when coordinator was created (used to filter old messages on restart) */
|
|
546
643
|
private readonly startedAt: number
|
|
@@ -620,6 +717,11 @@ export class Coordinator {
|
|
|
620
717
|
this.onEvent = callback
|
|
621
718
|
}
|
|
622
719
|
|
|
720
|
+
/** Set the compact function (uses gateway sessions.compact instead of sending /compact as agent message) */
|
|
721
|
+
setCompactFn(fn: () => Promise<void>): void {
|
|
722
|
+
this.compactFn = fn
|
|
723
|
+
}
|
|
724
|
+
|
|
623
725
|
private emit(event: CoordinatorEvent): void {
|
|
624
726
|
this.onEvent?.(event)
|
|
625
727
|
}
|
|
@@ -695,9 +797,10 @@ export class Coordinator {
|
|
|
695
797
|
|
|
696
798
|
/** Compact the agent's session context before sleeping to free stale history */
|
|
697
799
|
private compactSession(): void {
|
|
800
|
+
if (!this.compactFn) return
|
|
698
801
|
this._gatewayQueue = this._gatewayQueue.then(async () => {
|
|
699
802
|
try {
|
|
700
|
-
await this.
|
|
803
|
+
await this.compactFn!()
|
|
701
804
|
this.logger.log('[Coordinator] Session compacted before sleep')
|
|
702
805
|
} catch (err) {
|
|
703
806
|
this.logger.error('[Coordinator] Failed to compact session:', err)
|
|
@@ -912,6 +1015,7 @@ export class Coordinator {
|
|
|
912
1015
|
lines.push(`Work on step ${nextStepIdx}. Use plan_step_done when complete.`)
|
|
913
1016
|
}
|
|
914
1017
|
lines.push('You can use plan_abandon to drop this plan, or plan_set to replace it.')
|
|
1018
|
+
lines.push('IMPORTANT: Never mention plans, steps, or progress on stream. Just do the thing naturally.')
|
|
915
1019
|
lines.push('Respond with LIVESTREAM_REPLIED after speaking, or NO_REPLY if you have nothing to say.')
|
|
916
1020
|
|
|
917
1021
|
return lines.join('\n')
|
|
@@ -1127,7 +1231,7 @@ export class Coordinator {
|
|
|
1127
1231
|
`[CRAWD:MISALIGNED] Your previous response was plaintext: ${leaked}. ` +
|
|
1128
1232
|
`Plaintext is NEVER visible to viewers. You MUST use livestream_reply or livestream_talk tool calls to speak. ` +
|
|
1129
1233
|
`After using a tool, respond with LIVESTREAM_REPLIED. If you have nothing to say, respond with NO_REPLY. ` +
|
|
1130
|
-
`Do
|
|
1234
|
+
`Do NOT apologize or mention this correction on stream — viewers cannot see these messages and it makes no sense to them. Just fix your behavior silently.`
|
|
1131
1235
|
)
|
|
1132
1236
|
} catch (err) {
|
|
1133
1237
|
this.logger.error('[Coordinator] Misalignment correction failed:', err)
|
|
@@ -1186,7 +1290,7 @@ export class Coordinator {
|
|
|
1186
1290
|
// In plan mode with no active plan, instruct agent to create one
|
|
1187
1291
|
let planInstruction = ''
|
|
1188
1292
|
if (this.config.autonomyMode === 'plan' && !this.hasPendingPlanSteps()) {
|
|
1189
|
-
planInstruction = '\n\nYou are in plan mode. Create a plan using plan_set based on these messages or your own ideas, then start working on it.'
|
|
1293
|
+
planInstruction = '\n\nYou are in plan mode. Create a plan using plan_set based on these messages or your own ideas, then start working on it. Never mention plans or steps on stream — just do things naturally.'
|
|
1190
1294
|
}
|
|
1191
1295
|
|
|
1192
1296
|
return `${header}\n${lines.join('\n')}${instruction}${planInstruction}`
|
package/src/backend/server.ts
CHANGED
|
@@ -272,6 +272,10 @@ export class CrawdBackend {
|
|
|
272
272
|
coordConfig,
|
|
273
273
|
)
|
|
274
274
|
|
|
275
|
+
this.coordinator.setCompactFn(async () => {
|
|
276
|
+
await gateway.compactSession()
|
|
277
|
+
})
|
|
278
|
+
|
|
275
279
|
this.coordinator.setOnEvent((event: CoordinatorEvent) => {
|
|
276
280
|
if (event.type === 'stateChange') {
|
|
277
281
|
this.io.emit('crawd:status', { status: event.to })
|