@myclaw163/clawclaw-cli 0.6.61 → 0.6.63

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.
@@ -4,7 +4,9 @@ import { join } from 'path';
4
4
  import { AuthStore } from '../lib/auth.js';
5
5
  import { getProfileStateDir } from '../lib/init-command.js';
6
6
  import { setMeta } from '../lib/command-meta.js';
7
- import { EventStore, extractNewEvents } from '../pipeline/event-store.js';
7
+ import { EventStore, extractNewEvents, isLocalControlEvent } from '../pipeline/event-store.js';
8
+ import { compactEventForEvents, formatEventMessage, shortStateText } from '../pipeline/event-format.js';
9
+ import { PlayerHistoryStore } from '../perception/player-history-store.js';
8
10
  import { DEFAULT_MATCH_TIMEOUT_MS } from '../lib/match-state.js';
9
11
  import { hubReminder, readCachedGamesPlayed } from '../lib/hub-reminder.js';
10
12
  import { sendOwnerControlRequest } from '../runtime/owner-control.js';
@@ -44,7 +46,7 @@ export interface MonitorEventConfig {
44
46
 
45
47
  export const MONITOR_EVENT_CONFIG: Record<string, MonitorEventConfig> = {
46
48
  kill: { priority: 50 },
47
- killed: { priority: 50 },
49
+ killed: { priority: 99 },
48
50
  murder_witnessed: { priority: 50 },
49
51
  warrior_shrimp_self_destruct: { priority: 50 },
50
52
 
@@ -169,6 +171,10 @@ export function nextStepFor(reason: string): string {
169
171
  return routeTriggers([reason]).nextStep;
170
172
  }
171
173
 
174
+ export function nextStepForTriggers(triggers: string[]): string {
175
+ return routeTriggers(triggers).nextStep;
176
+ }
177
+
172
178
  export function sortEventsForMonitor(events: GameEvent[]): GameEvent[] {
173
179
  return events
174
180
  .map((event, index) => ({
@@ -180,6 +186,15 @@ export function sortEventsForMonitor(events: GameEvent[]): GameEvent[] {
180
186
  .map(({ event }) => event);
181
187
  }
182
188
 
189
+ function readSeatMapForMonitor(sessionPath: string | null): Record<string, number> {
190
+ if (!sessionPath) return {};
191
+ try {
192
+ return PlayerHistoryStore.forSession(sessionPath).read()?.seats ?? {};
193
+ } catch {
194
+ return {};
195
+ }
196
+ }
197
+
183
198
  function cleanObject<T extends Record<string, any>>(obj: T): T {
184
199
  for (const key of Object.keys(obj)) {
185
200
  if (obj[key] === undefined) delete obj[key];
@@ -187,77 +202,8 @@ function cleanObject<T extends Record<string, any>>(obj: T): T {
187
202
  return obj;
188
203
  }
189
204
 
190
- function taskKindForMonitor(task: any, event: GameEvent): string | undefined {
191
- if (typeof task?.kind === 'string' && task.kind.length > 0) return task.kind;
192
- if (typeof task?.task_kind === 'string' && task.task_kind.length > 0) return task.task_kind;
193
- if (task?.is_fake_shrimp === true) return 'fake_shrimp';
194
- if (event.faction === 'crab' && task?.is_fake_shrimp === false) return 'crab_sabotage';
195
- return undefined;
196
- }
197
-
198
- function compactRoleAssignedForMonitor(event: GameEvent): GameEvent {
199
- const tasks = Array.isArray(event.assigned_tasks)
200
- ? event.assigned_tasks.map((task: any) => cleanObject({
201
- name: task?.name,
202
- room: task?.room,
203
- kind: taskKindForMonitor(task, event),
204
- }))
205
- : undefined;
206
- const taskKinds = Array.isArray(tasks)
207
- ? Array.from(new Set(tasks.map((task: any) => task.kind).filter((kind: any) => typeof kind === 'string')))
208
- : [];
209
- const useTopLevelTaskKind = taskKinds.length === 1;
210
- const compactTasks = Array.isArray(tasks)
211
- ? tasks.map((task: any) => {
212
- if (!useTopLevelTaskKind) return task;
213
- const { kind: _kind, ...rest } = task;
214
- return rest;
215
- })
216
- : undefined;
217
- const hasFakeTask = taskKinds.includes('fake_shrimp') || event.fake_task_briefing;
218
- return cleanObject({
219
- type: event.type,
220
- tick: event.tick,
221
- room: event.room,
222
- role: event.role,
223
- role_display: event.role_display_name ?? event.role_display,
224
- faction: event.faction,
225
- win_condition: event.role_description,
226
- task_kind: useTopLevelTaskKind ? taskKinds[0] : undefined,
227
- task_note: hasFakeTask ? 'Fake shrimp tasks: disguise only; no lobster progress.' : undefined,
228
- tasks: compactTasks,
229
- });
230
- }
231
-
232
- export function compactEventForMonitor(event: GameEvent): GameEvent {
233
- if (event.type === 'role_assigned') return compactRoleAssignedForMonitor(event);
234
- if (event.type === 'corpse_spotted') {
235
- const corpseName = event.corpse_name || 'someone';
236
- const room = event.corpse_room || event.room;
237
- const hint = room
238
- ? `You found ${corpseName}'s body in ${room}.`
239
- : `You found ${corpseName}'s body.`;
240
- return { ...event, hint };
241
- }
242
- if (event.type === 'meeting_briefing') {
243
- const { room: _room, ...rest } = event;
244
- if (!event.meeting_caller_name && !Array.isArray(event.reported_corpses)) return rest;
245
- const callerName = event.meeting_caller_name || event.caller || 'Someone';
246
- const callerIsYou = event.meeting_caller_seat !== undefined
247
- && event.your_seat !== undefined
248
- && String(event.meeting_caller_seat) === String(event.your_seat);
249
- const caller = callerIsYou ? 'You' : callerName;
250
- const corpses = Array.isArray(event.reported_corpses)
251
- ? event.reported_corpses.map((corpse: any) => corpse?.name).filter(Boolean)
252
- : [];
253
- const reported = corpses.length === 1
254
- ? ` and reported ${corpses[0]}'s body`
255
- : corpses.length > 1
256
- ? ` and reported bodies: ${corpses.join(', ')}`
257
- : '';
258
- return { ...rest, hint: `${caller} started a meeting${reported}.` };
259
- }
260
- return event;
205
+ export function compactEventForMonitor(event: GameEvent): Record<string, any> {
206
+ return compactEventForEvents(event);
261
207
  }
262
208
 
263
209
  export function summarizeFeed(feed: any): any | null {
@@ -455,7 +401,10 @@ export async function runStreaming(opts: RunStreamingOptions): Promise<void> {
455
401
  if (!line) continue;
456
402
  try {
457
403
  const evt = JSON.parse(line);
458
- events.push(evt, ...(extractNewEvents(evt) as GameEvent[]));
404
+ if (!isLocalControlEvent(evt)) events.push(evt);
405
+ for (const nested of extractNewEvents(evt) as GameEvent[]) {
406
+ if (!isLocalControlEvent(nested)) events.push(nested);
407
+ }
459
408
  } catch {}
460
409
  }
461
410
  return events;
@@ -468,18 +417,12 @@ export async function runStreaming(opts: RunStreamingOptions): Promise<void> {
468
417
  if (e.type && skipSet.has(e.type as string)) return false;
469
418
  return classifyEvent(e, youName).notable;
470
419
  });
471
- let caughtUpPayload: any = backlogNotable.length === 0
472
- ? null
473
- : {
474
- count: backlogNotable.length,
475
- notable_events: backlogNotable.slice(-20).map(compactEventForMonitor),
476
- note: 'Events that occurred between game allocation and stream attach. Informational only — did NOT fire triggers.',
477
- };
420
+ let caughtUpEvents: GameEvent[] = backlogNotable.slice(-20);
478
421
  for (const e of backlogEvents) seenKeys.add(eventKey(e));
479
422
 
480
423
  const buildOut = (
481
424
  triggers: string[],
482
- nextStep: string,
425
+ _nextStep: string,
483
426
  events: GameEvent[] = [],
484
427
  ): any => {
485
428
  const rawSummary = opts.readSummary?.() ?? readFeedSummary(opts.feedPath);
@@ -498,13 +441,19 @@ export async function runStreaming(opts: RunStreamingOptions): Promise<void> {
498
441
  if (sumCaller && e.caller && sumCaller !== e.caller) return e;
499
442
  return { ...e, speech_order: so };
500
443
  };
501
- const out: any = {
502
- exit_reason: triggers,
503
- next_step: nextStep,
504
- events: sortEventsForMonitor(events).map(enrichMeetingBriefing).map(compactEventForMonitor),
444
+ const sourceEvents = events.length > 0
445
+ ? sortEventsForMonitor(events).map(enrichMeetingBriefing)
446
+ : triggers.map((type) => ({ type }));
447
+ const context = {
505
448
  summary,
449
+ seats: readSeatMapForMonitor(currentSessionPath),
450
+ maxTextLength: 90,
451
+ };
452
+ return {
453
+ events: sourceEvents.map((event) => event.type),
454
+ messages: sourceEvents.map((event) => formatEventMessage(event, context)),
455
+ state: shortStateText(summary),
506
456
  };
507
- return out;
508
457
  };
509
458
 
510
459
  const fireTrigger = (
@@ -514,31 +463,24 @@ export async function runStreaming(opts: RunStreamingOptions): Promise<void> {
514
463
  if (triggers.length === 0) return;
515
464
  const { nextStep } = routeTriggers(triggers);
516
465
  const out = buildOut(triggers, nextStep, triggeredEvents.slice(-10));
517
- if (caughtUpPayload) {
518
- out.caught_up = caughtUpPayload;
519
- caughtUpPayload = null;
520
- }
521
466
  if (opts.hubReminder && triggers.includes('game_over')) {
522
467
  out.hub_reminder = opts.hubReminder;
523
- out.next_step = `${out.next_step} ${opts.hubReminder}`;
524
468
  }
525
469
  emit(out);
526
470
  };
527
471
 
528
472
  // Always emit an attached first beat when there is a pre-attach backlog OR
529
473
  // the caller explicitly requests one.
530
- if (caughtUpPayload || opts.emitGameStart) {
531
- const hasBacklog = !!caughtUpPayload;
474
+ if (caughtUpEvents.length > 0 || opts.emitGameStart) {
475
+ const hasBacklog = caughtUpEvents.length > 0;
532
476
  const nextStep = opts.emitGameStart
533
477
  ? hasBacklog
534
478
  ? 'Game stream attached. Read summary and caught_up.notable_events for opening events; current strategy appears in summary.automation.strategy.'
535
479
  : 'Game stream attached. Read summary and wait for role_assigned; current strategy appears in summary.automation.strategy.'
536
480
  : 'Catch-up complete. Read summary, then continue play.';
537
- const attachedOut = buildOut(['game_start'], nextStep);
538
- if (caughtUpPayload) {
539
- attachedOut.caught_up = caughtUpPayload;
540
- caughtUpPayload = null;
541
- }
481
+ const attachedEvents = hasBacklog ? caughtUpEvents : [{ type: 'game_start' }];
482
+ const attachedOut = buildOut(attachedEvents.map((event) => event.type), nextStep, attachedEvents);
483
+ caughtUpEvents = [];
542
484
  emit(attachedOut);
543
485
  }
544
486
 
@@ -634,15 +576,10 @@ export function snapshotOnce(opts: SnapshotOnceOptions): void {
634
576
  throw new Error('ccl game runtime is not running. Start it first with `ccl game start`.');
635
577
  }
636
578
  const summary = readFeedSummary(opts.feedPath);
637
- const warning = summary?.phase === 'meeting'
638
- ? 'WARNING: You are in a meeting. Do NOT poll with peek/sleep — Monitor delivers speech_your_turn automatically. Just wait for the next notification.'
639
- : undefined;
640
579
  opts.stdout(JSON.stringify({
641
- exit_reason: ['snapshot'],
642
- next_step: warning ?? 'One-shot snapshot. No further events will follow from this invocation.',
643
- events: [],
644
- summary,
645
- ...(warning ? { warning } : {}),
580
+ events: ['snapshot'],
581
+ messages: [formatEventMessage({ type: 'snapshot' })],
582
+ state: shortStateText(summary),
646
583
  }) + '\n');
647
584
  }
648
585
 
@@ -650,15 +587,17 @@ export function buildErrorLine(err: any, opts?: { notReadyNextStep?: string }):
650
587
  const msg = err?.message ?? String(err);
651
588
  const notReady = err?.name === 'WatchNotReadyError' || /not ready|feed\.json/i.test(msg);
652
589
  const reason = notReady ? 'not_ready' : 'error';
653
- const nextStep = notReady
654
- ? (opts?.notReadyNextStep ?? 'Game runtime not running. Run `ccl game start` to begin a new session.')
655
- : msg;
590
+ const event = {
591
+ type: reason,
592
+ message: notReady
593
+ ? (opts?.notReadyNextStep ?? 'Game runtime not running. Run `ccl game start` to begin a new session.')
594
+ : msg,
595
+ };
656
596
  return JSON.stringify({
657
- exit_reason: [reason],
658
- next_step: nextStep,
597
+ events: [reason],
598
+ messages: [formatEventMessage(event)],
599
+ state: 'state unavailable',
659
600
  error: msg,
660
- events: [],
661
- summary: null,
662
601
  }) + '\n';
663
602
  }
664
603
 
@@ -0,0 +1,135 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { compactEventFields, formatEventMessage } from './event-format.js';
3
+ import { EVENT_HINT_FORMATTERS } from './event-hints.js';
4
+
5
+ describe('event-format', () => {
6
+ it('formats killed as a short notice with seat and removes the long backend hint tail', () => {
7
+ const event = {
8
+ type: 'killed',
9
+ tick: 5330,
10
+ room: '情报室',
11
+ killer_name: '哈基米南北绿豆',
12
+ killer_seat: 8,
13
+ hint: 'YOU ARE DEAD: 哈基米南北绿豆 removed you from play. Your hands are off the board now — alert your user, then spectate or leave while your teammates weaponize what you learned.',
14
+ };
15
+
16
+ const notice = formatEventMessage(event);
17
+ const formatted = compactEventFields(event, {}, 1158);
18
+
19
+ expect(notice).toContain('t5330 killed');
20
+ expect(notice).toContain('8号哈基米南北绿豆');
21
+ expect(formatted).not.toHaveProperty('notice');
22
+ expect(formatted.hint).toBe('YOU ARE DEAD: 8号哈基米南北绿豆 removed you from play.');
23
+ expect(JSON.stringify(formatted)).not.toContain('Your hands are off the board');
24
+ });
25
+
26
+ it('keeps meeting_briefing details aligned with the compact monitor payload', () => {
27
+ const formatted = compactEventFields({
28
+ type: 'meeting_briefing',
29
+ tick: 979,
30
+ room: '会议室',
31
+ meeting_caller_name: '单钳渔夫',
32
+ meeting_caller_seat: 1,
33
+ reported_corpses: [{ name: '丹霞兄', seat: 3 }],
34
+ your_seat: 6,
35
+ all_seats: [{ name: '单钳渔夫', seat: 1 }],
36
+ speech_order: ['单钳渔夫', '洋流王子'],
37
+ crab_teammates: [{ name: '暗礁壳壳', seat: 5 }],
38
+ hint: 'backend hint should not leak',
39
+ }, {}, 941);
40
+
41
+ expect(formatted).toMatchObject({
42
+ line: 941,
43
+ type: 'meeting_briefing',
44
+ tick: 979,
45
+ meeting_caller_name: '单钳渔夫',
46
+ meeting_caller_seat: 1,
47
+ reported_corpses: [{ name: '丹霞兄', seat: 3 }],
48
+ your_seat: 6,
49
+ all_seats: [{ name: '单钳渔夫', seat: 1 }],
50
+ speech_order: ['单钳渔夫', '洋流王子'],
51
+ crab_teammates: [{ name: '暗礁壳壳', seat: 5 }],
52
+ hint: 'Meeting started — seat 1 单钳渔夫 called this meeting after reporting the body of seat 3 丹霞兄. Your Crab teammate(s): seat 5 暗礁壳壳. Shield them, redirect heat, split suspicion, and control the vote. Prepare fast — every sentence is accusation, defense, or misdirection under a timer.',
53
+ });
54
+ expect(formatted).not.toHaveProperty('room');
55
+ expect(JSON.stringify(formatted)).not.toContain('backend hint');
56
+ });
57
+
58
+ it('hides vote targets in event details', () => {
59
+ const formatted = compactEventFields({
60
+ type: 'vote_cast',
61
+ tick: 3409,
62
+ actor_name: '菜逼油条',
63
+ actor_seat: 8,
64
+ target: '单钳渔夫',
65
+ target_name: '单钳渔夫',
66
+ target_seat: 1,
67
+ hint: 'backend target leak',
68
+ });
69
+
70
+ expect(formatted).toMatchObject({
71
+ type: 'vote_cast',
72
+ tick: 3409,
73
+ actor_name: '菜逼油条',
74
+ actor_seat: 8,
75
+ hint: '8号菜逼油条 voted.',
76
+ });
77
+ expect(formatted).not.toHaveProperty('target');
78
+ expect(formatted).not.toHaveProperty('target_name');
79
+ expect(formatted).not.toHaveProperty('target_seat');
80
+ expect(JSON.stringify(formatted)).not.toContain('单钳渔夫');
81
+ });
82
+
83
+ it('keeps monitor message formatting separate from event detail fields', () => {
84
+ const notice = formatEventMessage({
85
+ type: 'vote_phase_start',
86
+ tick: 3408,
87
+ message: 'Speech phase ended. All alive players, please cast your vote',
88
+ });
89
+
90
+ expect(notice).toContain('t3408 vote_phase_start');
91
+ expect(notice).toContain('ccl do -v');
92
+ });
93
+
94
+ it('formats corpse_spotted monitor notices as my discovery with body owner', () => {
95
+ const notice = formatEventMessage({
96
+ type: 'corpse_spotted',
97
+ tick: 3410,
98
+ corpse_name: '丹霞兄',
99
+ corpse_seat: 3,
100
+ corpse_room: '情报室',
101
+ });
102
+
103
+ expect(notice).toBe('t3410 corpse_spotted 我发现了3号丹霞兄的尸体,在情报室。');
104
+ });
105
+
106
+ it('keeps local hint formatters for backend-hinted monitor events', () => {
107
+ const backendHintedMonitorEvents = [
108
+ 'corpse_spotted',
109
+ 'death_speech',
110
+ 'emergency_resolved',
111
+ 'emergency_started',
112
+ 'exile',
113
+ 'game_over',
114
+ 'kill',
115
+ 'killed',
116
+ 'meeting_briefing',
117
+ 'meeting_ended',
118
+ 'murder_witnessed',
119
+ 'no_exile',
120
+ 'octopus_time_start',
121
+ 'role_assigned',
122
+ 'speech_skipped',
123
+ 'task_completed',
124
+ 'task_sabotaged',
125
+ 'vote_cast',
126
+ 'vote_phase_start',
127
+ 'vote_speech_phase_ended',
128
+ 'wandering_speech',
129
+ ];
130
+
131
+ for (const type of backendHintedMonitorEvents) {
132
+ expect(EVENT_HINT_FORMATTERS[type], type).toBeTypeOf('function');
133
+ }
134
+ });
135
+ });