@vsuryav/agent-sim 0.1.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.
Files changed (154) hide show
  1. package/README.md +25 -0
  2. package/bin/agent-sim.js +25 -0
  3. package/package.json +72 -0
  4. package/src/app-paths.ts +29 -0
  5. package/src/app-sync.test.ts +75 -0
  6. package/src/app-sync.ts +110 -0
  7. package/src/cli.ts +129 -0
  8. package/src/collector/claude-code.test.ts +102 -0
  9. package/src/collector/claude-code.ts +133 -0
  10. package/src/collector/codex-cli.test.ts +116 -0
  11. package/src/collector/codex-cli.ts +149 -0
  12. package/src/collector/db.test.ts +59 -0
  13. package/src/collector/db.ts +125 -0
  14. package/src/collector/names.test.ts +21 -0
  15. package/src/collector/names.ts +28 -0
  16. package/src/collector/personality.test.ts +40 -0
  17. package/src/collector/personality.ts +46 -0
  18. package/src/collector/remote-sync.test.ts +31 -0
  19. package/src/collector/remote-sync.ts +171 -0
  20. package/src/collector/sync.test.ts +67 -0
  21. package/src/collector/sync.ts +148 -0
  22. package/src/collector/types.ts +1 -0
  23. package/src/engine/bootstrap/state.ts +3 -0
  24. package/src/engine/buddy/CompanionSprite.tsx +371 -0
  25. package/src/engine/buddy/companion.ts +133 -0
  26. package/src/engine/buddy/prompt.ts +36 -0
  27. package/src/engine/buddy/sprites.ts +514 -0
  28. package/src/engine/buddy/types.ts +148 -0
  29. package/src/engine/buddy/useBuddyNotification.tsx +98 -0
  30. package/src/engine/ink/Ansi.tsx +292 -0
  31. package/src/engine/ink/bidi.ts +139 -0
  32. package/src/engine/ink/clearTerminal.ts +74 -0
  33. package/src/engine/ink/colorize.ts +231 -0
  34. package/src/engine/ink/components/AlternateScreen.tsx +80 -0
  35. package/src/engine/ink/components/App.tsx +658 -0
  36. package/src/engine/ink/components/AppContext.ts +21 -0
  37. package/src/engine/ink/components/Box.tsx +214 -0
  38. package/src/engine/ink/components/Button.tsx +192 -0
  39. package/src/engine/ink/components/ClockContext.tsx +112 -0
  40. package/src/engine/ink/components/CursorDeclarationContext.ts +32 -0
  41. package/src/engine/ink/components/ErrorOverview.tsx +109 -0
  42. package/src/engine/ink/components/Link.tsx +42 -0
  43. package/src/engine/ink/components/Newline.tsx +39 -0
  44. package/src/engine/ink/components/NoSelect.tsx +68 -0
  45. package/src/engine/ink/components/RawAnsi.tsx +57 -0
  46. package/src/engine/ink/components/ScrollBox.tsx +237 -0
  47. package/src/engine/ink/components/Spacer.tsx +20 -0
  48. package/src/engine/ink/components/StdinContext.ts +49 -0
  49. package/src/engine/ink/components/TerminalFocusContext.tsx +52 -0
  50. package/src/engine/ink/components/TerminalSizeContext.tsx +7 -0
  51. package/src/engine/ink/components/Text.tsx +254 -0
  52. package/src/engine/ink/constants.ts +2 -0
  53. package/src/engine/ink/dom.ts +484 -0
  54. package/src/engine/ink/events/click-event.ts +38 -0
  55. package/src/engine/ink/events/dispatcher.ts +233 -0
  56. package/src/engine/ink/events/emitter.ts +39 -0
  57. package/src/engine/ink/events/event-handlers.ts +73 -0
  58. package/src/engine/ink/events/event.ts +11 -0
  59. package/src/engine/ink/events/focus-event.ts +21 -0
  60. package/src/engine/ink/events/input-event.ts +205 -0
  61. package/src/engine/ink/events/keyboard-event.ts +51 -0
  62. package/src/engine/ink/events/terminal-event.ts +107 -0
  63. package/src/engine/ink/events/terminal-focus-event.ts +19 -0
  64. package/src/engine/ink/focus.ts +181 -0
  65. package/src/engine/ink/frame.ts +124 -0
  66. package/src/engine/ink/get-max-width.ts +27 -0
  67. package/src/engine/ink/global.d.ts +18 -0
  68. package/src/engine/ink/hit-test.ts +130 -0
  69. package/src/engine/ink/hooks/use-animation-frame.ts +57 -0
  70. package/src/engine/ink/hooks/use-app.ts +8 -0
  71. package/src/engine/ink/hooks/use-declared-cursor.ts +73 -0
  72. package/src/engine/ink/hooks/use-input.ts +92 -0
  73. package/src/engine/ink/hooks/use-interval.ts +67 -0
  74. package/src/engine/ink/hooks/use-search-highlight.ts +53 -0
  75. package/src/engine/ink/hooks/use-selection.ts +104 -0
  76. package/src/engine/ink/hooks/use-stdin.ts +8 -0
  77. package/src/engine/ink/hooks/use-tab-status.ts +72 -0
  78. package/src/engine/ink/hooks/use-terminal-focus.ts +16 -0
  79. package/src/engine/ink/hooks/use-terminal-title.ts +31 -0
  80. package/src/engine/ink/hooks/use-terminal-viewport.ts +96 -0
  81. package/src/engine/ink/ink.tsx +1723 -0
  82. package/src/engine/ink/instances.ts +10 -0
  83. package/src/engine/ink/layout/engine.ts +6 -0
  84. package/src/engine/ink/layout/geometry.ts +97 -0
  85. package/src/engine/ink/layout/node.ts +152 -0
  86. package/src/engine/ink/layout/yoga.ts +308 -0
  87. package/src/engine/ink/line-width-cache.ts +24 -0
  88. package/src/engine/ink/log-update.ts +773 -0
  89. package/src/engine/ink/measure-element.ts +23 -0
  90. package/src/engine/ink/measure-text.ts +47 -0
  91. package/src/engine/ink/node-cache.ts +54 -0
  92. package/src/engine/ink/optimizer.ts +93 -0
  93. package/src/engine/ink/output.ts +797 -0
  94. package/src/engine/ink/parse-keypress.ts +801 -0
  95. package/src/engine/ink/reconciler.ts +512 -0
  96. package/src/engine/ink/render-border.ts +231 -0
  97. package/src/engine/ink/render-node-to-output.ts +1462 -0
  98. package/src/engine/ink/render-to-screen.ts +231 -0
  99. package/src/engine/ink/renderer.ts +178 -0
  100. package/src/engine/ink/root.ts +184 -0
  101. package/src/engine/ink/screen.ts +1486 -0
  102. package/src/engine/ink/searchHighlight.ts +93 -0
  103. package/src/engine/ink/selection.ts +917 -0
  104. package/src/engine/ink/squash-text-nodes.ts +92 -0
  105. package/src/engine/ink/stringWidth.ts +222 -0
  106. package/src/engine/ink/styles.ts +771 -0
  107. package/src/engine/ink/supports-hyperlinks.ts +57 -0
  108. package/src/engine/ink/tabstops.ts +46 -0
  109. package/src/engine/ink/terminal-focus-state.ts +47 -0
  110. package/src/engine/ink/terminal-querier.ts +212 -0
  111. package/src/engine/ink/terminal.ts +248 -0
  112. package/src/engine/ink/termio/ansi.ts +75 -0
  113. package/src/engine/ink/termio/csi.ts +319 -0
  114. package/src/engine/ink/termio/dec.ts +60 -0
  115. package/src/engine/ink/termio/esc.ts +67 -0
  116. package/src/engine/ink/termio/osc.ts +493 -0
  117. package/src/engine/ink/termio/parser.ts +394 -0
  118. package/src/engine/ink/termio/sgr.ts +308 -0
  119. package/src/engine/ink/termio/tokenize.ts +319 -0
  120. package/src/engine/ink/termio/types.ts +236 -0
  121. package/src/engine/ink/useTerminalNotification.ts +126 -0
  122. package/src/engine/ink/warn.ts +9 -0
  123. package/src/engine/ink/widest-line.ts +19 -0
  124. package/src/engine/ink/wrap-text.ts +74 -0
  125. package/src/engine/ink/wrapAnsi.ts +20 -0
  126. package/src/engine/native-ts/yoga-layout/enums.ts +134 -0
  127. package/src/engine/native-ts/yoga-layout/index.ts +2578 -0
  128. package/src/engine/stubs/bootstrap-state.ts +4 -0
  129. package/src/engine/stubs/debug.ts +6 -0
  130. package/src/engine/stubs/log.ts +4 -0
  131. package/src/engine/utils/debug.ts +5 -0
  132. package/src/engine/utils/earlyInput.ts +4 -0
  133. package/src/engine/utils/env.ts +15 -0
  134. package/src/engine/utils/envUtils.ts +4 -0
  135. package/src/engine/utils/execFileNoThrow.ts +24 -0
  136. package/src/engine/utils/fullscreen.ts +4 -0
  137. package/src/engine/utils/intl.ts +9 -0
  138. package/src/engine/utils/log.ts +3 -0
  139. package/src/engine/utils/semver.ts +13 -0
  140. package/src/engine/utils/sliceAnsi.ts +10 -0
  141. package/src/engine/utils/theme.ts +17 -0
  142. package/src/game/App.tsx +141 -0
  143. package/src/game/agents/behavior.ts +249 -0
  144. package/src/game/agents/speech.ts +57 -0
  145. package/src/game/canvas.ts +98 -0
  146. package/src/game/launch.ts +36 -0
  147. package/src/game/ship/ShipView.tsx +145 -0
  148. package/src/game/ship/ship-map.ts +172 -0
  149. package/src/game/ui/AgentBio.tsx +72 -0
  150. package/src/game/ui/HUD.tsx +63 -0
  151. package/src/game/ui/StatusBar.tsx +49 -0
  152. package/src/game/useKeyboard.ts +62 -0
  153. package/src/main.tsx +22 -0
  154. package/src/run-interactive.ts +74 -0
@@ -0,0 +1,46 @@
1
+ export interface Traits {
2
+ builder: number;
3
+ creator: number;
4
+ explorer: number;
5
+ leader: number;
6
+ thinker: number;
7
+ scholar: number;
8
+ }
9
+
10
+ export function extractTraits(
11
+ toolCounts: Record<string, number>,
12
+ contentTypeCounts: Record<string, number>,
13
+ messageCount: number,
14
+ durationMs: number,
15
+ ): Traits {
16
+ const totalToolUse = Object.values(toolCounts).reduce((a, b) => a + b, 0) || 1;
17
+ const totalContent = Object.values(contentTypeCounts).reduce((a, b) => a + b, 0) || 1;
18
+
19
+ // Builder: Bash, shell commands
20
+ const bashCount = (toolCounts['Bash'] ?? 0) + (toolCounts['bash'] ?? 0);
21
+ const builder = Math.min(1, bashCount / totalToolUse * 2);
22
+
23
+ // Creator: Write, Edit, NotebookEdit
24
+ const writeCount = (toolCounts['Write'] ?? 0) + (toolCounts['Edit'] ?? 0) + (toolCounts['NotebookEdit'] ?? 0);
25
+ const creator = Math.min(1, writeCount / totalToolUse * 2.5);
26
+
27
+ // Explorer: Read, Grep, Glob, LS
28
+ const readCount = (toolCounts['Read'] ?? 0) + (toolCounts['Grep'] ?? 0)
29
+ + (toolCounts['Glob'] ?? 0) + (toolCounts['LS'] ?? 0);
30
+ const explorer = Math.min(1, readCount / totalToolUse * 2);
31
+
32
+ // Leader: Agent, subagent delegation
33
+ const agentCount = toolCounts['Agent'] ?? 0;
34
+ const leader = Math.min(1, agentCount / Math.max(1, totalToolUse) * 8);
35
+
36
+ // Thinker: thinking blocks relative to total content
37
+ const thinkingCount = contentTypeCounts['thinking'] ?? 0;
38
+ const thinker = Math.min(1, thinkingCount / totalContent * 3);
39
+
40
+ // Scholar: WebSearch, WebFetch
41
+ const searchCount = (toolCounts['WebSearch'] ?? 0) + (toolCounts['WebFetch'] ?? 0)
42
+ + (toolCounts['web_search_end'] ?? 0);
43
+ const scholar = Math.min(1, searchCount / Math.max(1, totalToolUse) * 5);
44
+
45
+ return { builder, creator, explorer, leader, thinker, scholar };
46
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildPushPayload, getDeviceId } from './remote-sync.js';
3
+
4
+ describe('remote-sync', () => {
5
+ it('getDeviceId returns consistent hash', () => {
6
+ const id1 = getDeviceId();
7
+ const id2 = getDeviceId();
8
+ expect(id1).toBe(id2);
9
+ expect(typeof id1).toBe('string');
10
+ expect(id1.length).toBeGreaterThan(10);
11
+ });
12
+
13
+ it('buildPushPayload formats agents correctly', () => {
14
+ const agents = [
15
+ {
16
+ id: 'test-1', tool: 'claude-code', model: 'opus', project: '/p', clan: 'p',
17
+ created_at: 1000, total_tokens: 500, input_tokens: 100, output_tokens: 400,
18
+ cache_tokens: 0, reasoning_tokens: 0, duration_ms: 0, message_count: 5,
19
+ trait_builder: 0.5, trait_creator: 0.3, trait_explorer: 0.2,
20
+ trait_leader: 0.0, trait_thinker: 0.1, trait_scholar: 0.0,
21
+ name: 'Bold Spark', maturity: 'spark',
22
+ },
23
+ ];
24
+
25
+ const payload = buildPushPayload(agents);
26
+ expect(payload.device_id).toBeTruthy();
27
+ expect(payload.hostname).toBeTruthy();
28
+ expect(payload.agents).toHaveLength(1);
29
+ expect(payload.agents[0]!.id).toBe('test-1');
30
+ });
31
+ });
@@ -0,0 +1,171 @@
1
+ import { createHash } from 'crypto';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import {
6
+ authLoginStartResponseSchema,
7
+ authSessionResponseSchema,
8
+ pullResponseSchema,
9
+ type CollectedAgent,
10
+ type PullResponse,
11
+ } from '@vsuryav/agent-sim-contracts';
12
+ import { APP_NAME, CONFIG_PATH, ensureLocalDataDir } from '../app-paths.js';
13
+
14
+ const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
15
+ export const DEFAULT_SERVER_URL = process.env['AGENT_SIM_SERVER_URL']
16
+ ?? process.env['AGENT_WARS_SERVER_URL']
17
+ ?? 'http://localhost:3456';
18
+
19
+ interface Config {
20
+ serverUrl?: string;
21
+ token?: string;
22
+ }
23
+
24
+ export function loadConfig(): Config {
25
+ try {
26
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')) as Config;
27
+ } catch {
28
+ return {};
29
+ }
30
+ }
31
+
32
+ function saveConfig(config: Config): void {
33
+ ensureLocalDataDir();
34
+ fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
35
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
36
+ }
37
+
38
+ export function hasRemoteConfig(): boolean {
39
+ const config = loadConfig();
40
+ return !!config.serverUrl && !!config.token;
41
+ }
42
+
43
+ export function getDeviceId(): string {
44
+ const raw = `${os.hostname()}-${os.userInfo().username}-${os.platform()}-${os.arch()}`;
45
+ return createHash('sha256').update(raw).digest('hex').slice(0, 32);
46
+ }
47
+
48
+ export function buildPushPayload(agents: CollectedAgent[]): { device_id: string; hostname: string; agents: CollectedAgent[] } {
49
+ return {
50
+ device_id: getDeviceId(),
51
+ hostname: os.hostname(),
52
+ agents,
53
+ };
54
+ }
55
+
56
+ function sleep(ms: number): Promise<void> {
57
+ return new Promise((resolve) => setTimeout(resolve, ms));
58
+ }
59
+
60
+ async function pollForLoginSession(
61
+ serverUrl: string,
62
+ sessionId: string,
63
+ pollIntervalMs: number,
64
+ ): Promise<{ userId: number; githubLogin: string; token: string }> {
65
+ const deadline = Date.now() + LOGIN_TIMEOUT_MS;
66
+
67
+ while (Date.now() < deadline) {
68
+ const res = await fetch(`${serverUrl}/auth/session/${sessionId}`);
69
+ if (res.status === 202) {
70
+ await sleep(pollIntervalMs);
71
+ continue;
72
+ }
73
+
74
+ const body = await res.json() as unknown;
75
+ if (!res.ok) {
76
+ const error = body as { error?: string };
77
+ throw new Error(`Login failed: ${error.error ?? res.statusText}`);
78
+ }
79
+
80
+ const session = authSessionResponseSchema.parse(body);
81
+ if (session.status === 'complete') {
82
+ return session;
83
+ }
84
+
85
+ await sleep(pollIntervalMs);
86
+ }
87
+
88
+ throw new Error(`Login timed out. Re-run the ${APP_NAME} login command and finish GitHub auth within 5 minutes.`);
89
+ }
90
+
91
+ export async function login(serverUrl: string): Promise<void> {
92
+ const config = loadConfig();
93
+ config.serverUrl = serverUrl;
94
+ saveConfig(config);
95
+
96
+ const res = await fetch(`${serverUrl}/auth/login`);
97
+ const body = await res.json() as unknown;
98
+ if (!res.ok) {
99
+ const error = body as { error?: string };
100
+ throw new Error(`Login failed: ${error.error ?? res.statusText}`);
101
+ }
102
+ const { url, session_id, poll_interval_ms } = authLoginStartResponseSchema.parse(body);
103
+
104
+ console.log(`\n Open this URL to login with GitHub:\n`);
105
+ console.log(` ${url}\n`);
106
+ console.log(' Waiting for GitHub authorization to complete...\n');
107
+
108
+ const session = await pollForLoginSession(serverUrl, session_id, poll_interval_ms);
109
+ config.token = session.token;
110
+ saveConfig(config);
111
+ console.log(` Logged in as ${session.githubLogin}. Token saved.`);
112
+ }
113
+
114
+ export function setToken(token: string): void {
115
+ const config = loadConfig();
116
+ config.token = token;
117
+ saveConfig(config);
118
+ console.log(' Token saved.');
119
+ }
120
+
121
+ export function getPreferredServerUrl(): string {
122
+ return loadConfig().serverUrl ?? DEFAULT_SERVER_URL;
123
+ }
124
+
125
+ function getRequiredRemoteConfig(): Required<Config> {
126
+ const config = loadConfig();
127
+ if (!config.serverUrl || !config.token) {
128
+ throw new Error(`Not logged in. Run: ${APP_NAME} login <server-url>`);
129
+ }
130
+ return {
131
+ serverUrl: config.serverUrl,
132
+ token: config.token,
133
+ };
134
+ }
135
+
136
+ export async function pullFromServer(): Promise<PullResponse> {
137
+ const config = getRequiredRemoteConfig();
138
+ const res = await fetch(`${config.serverUrl}/sync/pull`, {
139
+ headers: {
140
+ Authorization: `Bearer ${config.token}`,
141
+ },
142
+ });
143
+
144
+ if (!res.ok) {
145
+ const err = await res.json() as { error?: string };
146
+ throw new Error(`Pull failed: ${err.error ?? res.statusText}`);
147
+ }
148
+
149
+ const body = await res.json() as unknown;
150
+ return pullResponseSchema.parse(body);
151
+ }
152
+
153
+ export async function pushToServer(agents: CollectedAgent[]): Promise<{ synced: number }> {
154
+ const config = getRequiredRemoteConfig();
155
+ const payload = buildPushPayload(agents);
156
+ const res = await fetch(`${config.serverUrl}/sync/push`, {
157
+ method: 'POST',
158
+ headers: {
159
+ 'Content-Type': 'application/json',
160
+ Authorization: `Bearer ${config.token}`,
161
+ },
162
+ body: JSON.stringify(payload),
163
+ });
164
+
165
+ if (!res.ok) {
166
+ const err = await res.json() as { error?: string };
167
+ throw new Error(`Push failed: ${err.error ?? res.statusText}`);
168
+ }
169
+
170
+ return res.json() as Promise<{ synced: number }>;
171
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { syncAll } from './sync.js';
3
+ import { initDb, getDb, closeDb } from './db.js';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import os from 'os';
7
+
8
+ describe('syncAll', () => {
9
+ const testDir = path.join(os.tmpdir(), `agent-sim-sync-test-${Date.now()}`);
10
+ const testDbPath = path.join(testDir, 'db.sqlite');
11
+ const mockClaudeDir = path.join(testDir, '.claude');
12
+
13
+ beforeEach(() => {
14
+ fs.mkdirSync(testDir, { recursive: true });
15
+ initDb(testDbPath);
16
+ fs.mkdirSync(path.join(mockClaudeDir, 'sessions'), { recursive: true });
17
+ fs.mkdirSync(path.join(mockClaudeDir, 'projects', '-test'), { recursive: true });
18
+ fs.writeFileSync(
19
+ path.join(mockClaudeDir, 'sessions', '1.json'),
20
+ JSON.stringify({ pid: 1, sessionId: 'sync-test-1', cwd: '/test', startedAt: 1700000000000 })
21
+ );
22
+ fs.writeFileSync(
23
+ path.join(mockClaudeDir, 'projects', '-test', 'sync-test-1.jsonl'),
24
+ JSON.stringify({
25
+ type: 'message',
26
+ message: {
27
+ role: 'assistant', model: 'claude-opus-4-6',
28
+ content: [{ type: 'tool_use', name: 'Bash', id: 't1', input: {} }],
29
+ usage: { input_tokens: 50, output_tokens: 100 },
30
+ },
31
+ uuid: 'm1', timestamp: 1700000001000, sessionId: 'sync-test-1',
32
+ })
33
+ );
34
+ });
35
+
36
+ afterEach(() => {
37
+ closeDb();
38
+ fs.rmSync(testDir, { recursive: true, force: true });
39
+ });
40
+
41
+ it('inserts new agents into the database', () => {
42
+ const stats = syncAll({ claudeDir: mockClaudeDir, codexDir: path.join(testDir, '.codex-nonexistent') });
43
+ expect(stats.newAgents).toBe(1);
44
+
45
+ const db = getDb();
46
+ const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get('sync-test-1') as any;
47
+ expect(agent).toBeTruthy();
48
+ expect(agent.name).toBeTruthy();
49
+ expect(agent.tool).toBe('claude-code');
50
+ });
51
+
52
+ it('creates clan for new project', () => {
53
+ syncAll({ claudeDir: mockClaudeDir, codexDir: path.join(testDir, '.codex-nonexistent') });
54
+
55
+ const db = getDb();
56
+ const clans = db.prepare('SELECT * FROM clans').all();
57
+ expect(clans.length).toBeGreaterThan(0);
58
+ });
59
+
60
+ it('computes maturity from token count', () => {
61
+ syncAll({ claudeDir: mockClaudeDir, codexDir: path.join(testDir, '.codex-nonexistent') });
62
+
63
+ const db = getDb();
64
+ const agent = db.prepare('SELECT maturity FROM agents WHERE id = ?').get('sync-test-1') as any;
65
+ expect(agent.maturity).toBe('spark');
66
+ });
67
+ });
@@ -0,0 +1,148 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import { computeMaturity } from '@vsuryav/agent-sim-core';
4
+ import { getDb, setWorldConfig } from './db.js';
5
+ import { scanClaudeCode } from './claude-code.js';
6
+ import { scanCodexCli } from './codex-cli.js';
7
+ import { generateName } from './names.js';
8
+ import type { CollectedAgent, PullResponse } from './types.js';
9
+
10
+ const CLAN_COLORS = [
11
+ '#e94560', '#ffd166', '#52b788', '#a8dadc', '#e76f51',
12
+ '#6a4c93', '#1982c4', '#8ac926', '#ff595e', '#ffca3a',
13
+ ];
14
+
15
+ export interface SyncOptions {
16
+ claudeDir?: string;
17
+ codexDir?: string;
18
+ }
19
+
20
+ export interface SyncResult {
21
+ newAgents: number;
22
+ totalAgents: number;
23
+ }
24
+
25
+ export interface UpsertResult {
26
+ inserted: number;
27
+ updated: number;
28
+ totalAgents: number;
29
+ }
30
+
31
+ function ensureClanColor(clan: string): void {
32
+ const db = getDb();
33
+ const existing = db.prepare('SELECT id FROM clans WHERE id = ?').get(clan);
34
+ if (existing) return;
35
+
36
+ const clanCount = (db.prepare('SELECT COUNT(*) as c FROM clans').get() as { c: number }).c;
37
+ const color = CLAN_COLORS[clanCount % CLAN_COLORS.length]!;
38
+ db.prepare('INSERT INTO clans (id, name, color) VALUES (?, ?, ?)').run(clan, clan, color);
39
+ }
40
+
41
+ function upsertAgentRecord(agent: CollectedAgent, emitBornEvent: boolean): 'inserted' | 'updated' {
42
+ const db = getDb();
43
+ const existing = db.prepare('SELECT id FROM agents WHERE id = ?').get(agent.id);
44
+ const name = generateName(agent.id);
45
+ const maturity = computeMaturity(agent.total_tokens);
46
+
47
+ ensureClanColor(agent.clan);
48
+
49
+ db.prepare(`
50
+ INSERT INTO agents (
51
+ id, tool, model, project, clan, created_at,
52
+ total_tokens, input_tokens, output_tokens, cache_tokens, reasoning_tokens,
53
+ duration_ms, message_count,
54
+ trait_builder, trait_creator, trait_explorer, trait_leader, trait_thinker, trait_scholar,
55
+ name, maturity
56
+ ) VALUES (
57
+ @id, @tool, @model, @project, @clan, @created_at,
58
+ @total_tokens, @input_tokens, @output_tokens, @cache_tokens, @reasoning_tokens,
59
+ @duration_ms, @message_count,
60
+ @trait_builder, @trait_creator, @trait_explorer, @trait_leader, @trait_thinker, @trait_scholar,
61
+ @name, @maturity
62
+ )
63
+ ON CONFLICT(id) DO UPDATE SET
64
+ tool = excluded.tool,
65
+ model = excluded.model,
66
+ project = excluded.project,
67
+ clan = excluded.clan,
68
+ created_at = excluded.created_at,
69
+ total_tokens = excluded.total_tokens,
70
+ input_tokens = excluded.input_tokens,
71
+ output_tokens = excluded.output_tokens,
72
+ cache_tokens = excluded.cache_tokens,
73
+ reasoning_tokens = excluded.reasoning_tokens,
74
+ duration_ms = excluded.duration_ms,
75
+ message_count = excluded.message_count,
76
+ trait_builder = excluded.trait_builder,
77
+ trait_creator = excluded.trait_creator,
78
+ trait_explorer = excluded.trait_explorer,
79
+ trait_leader = excluded.trait_leader,
80
+ trait_thinker = excluded.trait_thinker,
81
+ trait_scholar = excluded.trait_scholar,
82
+ name = excluded.name,
83
+ maturity = excluded.maturity
84
+ `).run({ ...agent, name, maturity });
85
+
86
+ if (!existing && emitBornEvent) {
87
+ db.prepare(
88
+ 'INSERT INTO events (timestamp, agent_id, type, description) VALUES (?, ?, ?, ?)'
89
+ ).run(
90
+ agent.created_at,
91
+ agent.id,
92
+ 'born',
93
+ `${name} arrived from a new session`,
94
+ );
95
+ }
96
+
97
+ return existing ? 'updated' : 'inserted';
98
+ }
99
+
100
+ export function upsertAgents(
101
+ agents: CollectedAgent[],
102
+ options: { emitBornEvents?: boolean } = {},
103
+ ): UpsertResult {
104
+ const db = getDb();
105
+ let inserted = 0;
106
+ let updated = 0;
107
+
108
+ const batch = db.transaction(() => {
109
+ for (const agent of agents) {
110
+ const outcome = upsertAgentRecord(agent, options.emitBornEvents ?? false);
111
+ if (outcome === 'inserted') inserted++;
112
+ else updated++;
113
+ }
114
+ });
115
+
116
+ batch();
117
+
118
+ const totalAgents = (db.prepare('SELECT COUNT(*) as c FROM agents').get() as { c: number }).c;
119
+ return { inserted, updated, totalAgents };
120
+ }
121
+
122
+ export function mergeRemoteState(remote: PullResponse): UpsertResult {
123
+ setWorldConfig(remote.world);
124
+ return upsertAgents(remote.agents, { emitBornEvents: false });
125
+ }
126
+
127
+ export function getAllAgents(): CollectedAgent[] {
128
+ return getDb().prepare(`
129
+ SELECT
130
+ id, tool, model, project, clan, created_at,
131
+ total_tokens, input_tokens, output_tokens, cache_tokens, reasoning_tokens,
132
+ duration_ms, message_count,
133
+ trait_builder, trait_creator, trait_explorer, trait_leader, trait_thinker, trait_scholar
134
+ FROM agents
135
+ ORDER BY created_at
136
+ `).all() as CollectedAgent[];
137
+ }
138
+
139
+ export function syncAll(opts: SyncOptions = {}): SyncResult {
140
+ const claudeDir = opts.claudeDir ?? path.join(os.homedir(), '.claude');
141
+ const codexDir = opts.codexDir ?? path.join(os.homedir(), '.codex');
142
+
143
+ const claudeAgents = scanClaudeCode(claudeDir);
144
+ const codexAgents = scanCodexCli(codexDir);
145
+ const upsert = upsertAgents([...claudeAgents, ...codexAgents], { emitBornEvents: true });
146
+
147
+ return { newAgents: upsert.inserted, totalAgents: upsert.totalAgents };
148
+ }
@@ -0,0 +1 @@
1
+ export type { CollectedAgent, PullResponse, UserWorldConfig } from '@vsuryav/agent-sim-contracts';
@@ -0,0 +1,3 @@
1
+ // Stub — replaces host-specific interaction telemetry
2
+ export function flushInteractionTime(): void {}
3
+ export function updateLastInteractionTime(): void {}