@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,133 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getDb } from './db.js';
4
+ import { extractTraits } from './personality.js';
5
+ import type { CollectedAgent } from './types.js';
6
+
7
+ interface SessionMeta {
8
+ pid: number;
9
+ sessionId: string;
10
+ cwd: string;
11
+ startedAt: number;
12
+ }
13
+
14
+ export function scanClaudeCode(claudeDir: string): CollectedAgent[] {
15
+ const db = getDb();
16
+ const sessionsDir = path.join(claudeDir, 'sessions');
17
+ const projectsDir = path.join(claudeDir, 'projects');
18
+ const agents: CollectedAgent[] = [];
19
+
20
+ if (!fs.existsSync(sessionsDir)) return agents;
21
+
22
+ // Read all session metadata files
23
+ const sessionFiles = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.json'));
24
+ const sessions: SessionMeta[] = [];
25
+
26
+ for (const file of sessionFiles) {
27
+ try {
28
+ const content = fs.readFileSync(path.join(sessionsDir, file), 'utf-8');
29
+ const meta = JSON.parse(content) as SessionMeta;
30
+ if (meta.sessionId) sessions.push(meta);
31
+ } catch {
32
+ // skip malformed files
33
+ }
34
+ }
35
+
36
+ if (!fs.existsSync(projectsDir)) return agents;
37
+
38
+ const projectDirs = fs.readdirSync(projectsDir).filter(d =>
39
+ fs.statSync(path.join(projectsDir, d)).isDirectory()
40
+ );
41
+
42
+ for (const session of sessions) {
43
+ // Skip if already in DB
44
+ const existing = db.prepare('SELECT id FROM agents WHERE id = ?').get(session.sessionId);
45
+ if (existing) continue;
46
+
47
+ // Find the conversation JSONL file for this session
48
+ let convFile: string | null = null;
49
+ for (const projDir of projectDirs) {
50
+ const candidate = path.join(projectsDir, projDir, `${session.sessionId}.jsonl`);
51
+ if (fs.existsSync(candidate)) {
52
+ convFile = candidate;
53
+ break;
54
+ }
55
+ }
56
+
57
+ let model: string | null = null;
58
+ let inputTokens = 0;
59
+ let outputTokens = 0;
60
+ let cacheTokens = 0;
61
+ let messageCount = 0;
62
+ const toolCounts: Record<string, number> = {};
63
+ const contentTypeCounts: Record<string, number> = {};
64
+
65
+ if (convFile) {
66
+ const lines = fs.readFileSync(convFile, 'utf-8').split('\n').filter(Boolean);
67
+ for (const line of lines) {
68
+ try {
69
+ const obj = JSON.parse(line);
70
+ const msg = obj?.message;
71
+ if (!msg || typeof msg !== 'object') continue;
72
+
73
+ if (msg.role) messageCount++;
74
+
75
+ if (msg.usage) {
76
+ inputTokens += msg.usage.input_tokens ?? 0;
77
+ outputTokens += msg.usage.output_tokens ?? 0;
78
+ cacheTokens += (msg.usage.cache_read_input_tokens ?? 0)
79
+ + (msg.usage.cache_creation_input_tokens ?? 0);
80
+ }
81
+
82
+ if (msg.model && !model) model = msg.model;
83
+
84
+ const content = msg.content;
85
+ if (Array.isArray(content)) {
86
+ for (const block of content) {
87
+ if (typeof block === 'object' && block !== null) {
88
+ const btype = block.type as string;
89
+ if (btype) {
90
+ contentTypeCounts[btype] = (contentTypeCounts[btype] ?? 0) + 1;
91
+ }
92
+ if (btype === 'tool_use' && block.name) {
93
+ toolCounts[block.name] = (toolCounts[block.name] ?? 0) + 1;
94
+ }
95
+ }
96
+ }
97
+ }
98
+ } catch {
99
+ // skip malformed lines
100
+ }
101
+ }
102
+ }
103
+
104
+ const totalTokens = inputTokens + outputTokens + cacheTokens;
105
+ const traits = extractTraits(toolCounts, contentTypeCounts, messageCount, 0);
106
+ const cwd = session.cwd || '/unknown';
107
+ const clan = path.basename(cwd) || 'home';
108
+
109
+ agents.push({
110
+ id: session.sessionId,
111
+ tool: 'claude-code',
112
+ model,
113
+ project: cwd,
114
+ clan,
115
+ created_at: session.startedAt,
116
+ total_tokens: totalTokens,
117
+ input_tokens: inputTokens,
118
+ output_tokens: outputTokens,
119
+ cache_tokens: cacheTokens,
120
+ reasoning_tokens: 0,
121
+ duration_ms: 0,
122
+ message_count: messageCount,
123
+ trait_builder: traits.builder,
124
+ trait_creator: traits.creator,
125
+ trait_explorer: traits.explorer,
126
+ trait_leader: traits.leader,
127
+ trait_thinker: traits.thinker,
128
+ trait_scholar: traits.scholar,
129
+ });
130
+ }
131
+
132
+ return agents;
133
+ }
@@ -0,0 +1,116 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { scanCodexCli } from './codex-cli.js';
3
+ import { initDb, getDb, closeDb } from './db.js';
4
+ import Database from 'better-sqlite3';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import os from 'os';
8
+
9
+ describe('Codex CLI collector', () => {
10
+ const testDir = path.join(os.tmpdir(), `agent-sim-codex-test-${Date.now()}`);
11
+ const testDbPath = path.join(testDir, 'db.sqlite');
12
+ const mockCodexDir = path.join(testDir, '.codex');
13
+
14
+ beforeEach(() => {
15
+ fs.mkdirSync(testDir, { recursive: true });
16
+ initDb(testDbPath);
17
+ });
18
+
19
+ afterEach(() => {
20
+ closeDb();
21
+ fs.rmSync(testDir, { recursive: true, force: true });
22
+ });
23
+
24
+ it('reads sessions from Codex SQLite threads table', () => {
25
+ fs.mkdirSync(mockCodexDir, { recursive: true });
26
+ const codexDb = new Database(path.join(mockCodexDir, 'state_5.sqlite'));
27
+ codexDb.exec(`
28
+ CREATE TABLE threads (
29
+ id TEXT PRIMARY KEY,
30
+ rollout_path TEXT NOT NULL DEFAULT '',
31
+ created_at INTEGER NOT NULL,
32
+ updated_at INTEGER NOT NULL,
33
+ source TEXT NOT NULL DEFAULT 'cli',
34
+ model_provider TEXT NOT NULL DEFAULT 'openai',
35
+ cwd TEXT NOT NULL,
36
+ title TEXT NOT NULL DEFAULT '',
37
+ sandbox_policy TEXT NOT NULL DEFAULT '',
38
+ approval_mode TEXT NOT NULL DEFAULT '',
39
+ tokens_used INTEGER NOT NULL DEFAULT 0,
40
+ has_user_event INTEGER NOT NULL DEFAULT 0,
41
+ archived INTEGER NOT NULL DEFAULT 0,
42
+ cli_version TEXT NOT NULL DEFAULT '',
43
+ first_user_message TEXT NOT NULL DEFAULT '',
44
+ model TEXT,
45
+ reasoning_effort TEXT
46
+ )
47
+ `);
48
+ codexDb.prepare(`INSERT INTO threads (id, created_at, updated_at, cwd, tokens_used, model, reasoning_effort)
49
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(
50
+ 'test-codex-1', 1700000000, 1700001000, '/test/project', 130000, 'gpt-5.4', 'xhigh'
51
+ );
52
+ codexDb.close();
53
+
54
+ const rolloutDir = path.join(mockCodexDir, 'sessions', '2026', '04', '08');
55
+ fs.mkdirSync(rolloutDir, { recursive: true });
56
+ const rolloutLines = [
57
+ JSON.stringify({
58
+ timestamp: 1700000500, type: 'event_msg',
59
+ payload: {
60
+ type: 'token_count',
61
+ info: {
62
+ total_token_usage: {
63
+ input_tokens: 100000, cached_input_tokens: 2000,
64
+ output_tokens: 20000, reasoning_output_tokens: 8000, total_tokens: 130000,
65
+ },
66
+ },
67
+ },
68
+ }),
69
+ JSON.stringify({ timestamp: 1700000100, type: 'event_msg', payload: { type: 'web_search_end' } }),
70
+ JSON.stringify({ timestamp: 1700000101, type: 'event_msg', payload: { type: 'web_search_end' } }),
71
+ ];
72
+ fs.writeFileSync(
73
+ path.join(rolloutDir, `rollout-2026-04-08T00-00-00-test-codex-1.jsonl`),
74
+ rolloutLines.join('\n')
75
+ );
76
+
77
+ const result = scanCodexCli(mockCodexDir);
78
+
79
+ expect(result.length).toBe(1);
80
+ expect(result[0].id).toBe('test-codex-1');
81
+ expect(result[0].tool).toBe('codex-cli');
82
+ expect(result[0].model).toBe('gpt-5.4');
83
+ expect(result[0].input_tokens).toBe(100000);
84
+ expect(result[0].output_tokens).toBe(20000);
85
+ expect(result[0].reasoning_tokens).toBe(8000);
86
+ expect(result[0].total_tokens).toBe(130000);
87
+ expect(result[0].trait_scholar).toBeGreaterThan(0);
88
+ });
89
+
90
+ it('skips sessions already in the database', () => {
91
+ const db = getDb();
92
+ db.prepare(`INSERT INTO agents (id, tool, project, clan, created_at)
93
+ VALUES (?, ?, ?, ?, ?)`).run('test-codex-1', 'codex-cli', '/test/project', 'project', Date.now());
94
+
95
+ fs.mkdirSync(mockCodexDir, { recursive: true });
96
+ const codexDb = new Database(path.join(mockCodexDir, 'state_5.sqlite'));
97
+ codexDb.exec(`
98
+ CREATE TABLE threads (
99
+ id TEXT PRIMARY KEY, rollout_path TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL,
100
+ updated_at INTEGER NOT NULL, source TEXT NOT NULL DEFAULT 'cli',
101
+ model_provider TEXT NOT NULL DEFAULT 'openai', cwd TEXT NOT NULL,
102
+ title TEXT NOT NULL DEFAULT '', sandbox_policy TEXT NOT NULL DEFAULT '',
103
+ approval_mode TEXT NOT NULL DEFAULT '', tokens_used INTEGER NOT NULL DEFAULT 0,
104
+ has_user_event INTEGER NOT NULL DEFAULT 0, archived INTEGER NOT NULL DEFAULT 0,
105
+ cli_version TEXT NOT NULL DEFAULT '', first_user_message TEXT NOT NULL DEFAULT '',
106
+ model TEXT, reasoning_effort TEXT
107
+ )
108
+ `);
109
+ codexDb.prepare(`INSERT INTO threads (id, created_at, updated_at, cwd, tokens_used)
110
+ VALUES (?, ?, ?, ?, ?)`).run('test-codex-1', 1700000000, 1700001000, '/test', 50000);
111
+ codexDb.close();
112
+
113
+ const result = scanCodexCli(mockCodexDir);
114
+ expect(result.length).toBe(0);
115
+ });
116
+ });
@@ -0,0 +1,149 @@
1
+ import Database from 'better-sqlite3';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { getDb } from './db.js';
5
+ import { extractTraits } from './personality.js';
6
+ import type { CollectedAgent } from './types.js';
7
+
8
+ interface CodexThread {
9
+ id: string;
10
+ created_at: number;
11
+ updated_at: number;
12
+ cwd: string;
13
+ tokens_used: number;
14
+ model: string | null;
15
+ reasoning_effort: string | null;
16
+ }
17
+
18
+ export function scanCodexCli(codexDir: string): CollectedAgent[] {
19
+ const db = getDb();
20
+ const agents: CollectedAgent[] = [];
21
+ const statePath = path.join(codexDir, 'state_5.sqlite');
22
+
23
+ if (!fs.existsSync(statePath)) return agents;
24
+
25
+ const codexDb = new Database(statePath, { readonly: true });
26
+ let threads: CodexThread[];
27
+ try {
28
+ threads = codexDb.prepare(
29
+ `SELECT id, created_at, updated_at, cwd, tokens_used, model, reasoning_effort
30
+ FROM threads WHERE archived = 0`
31
+ ).all() as CodexThread[];
32
+ } catch {
33
+ codexDb.close();
34
+ return agents;
35
+ }
36
+
37
+ for (const thread of threads) {
38
+ const existing = db.prepare('SELECT id FROM agents WHERE id = ?').get(thread.id);
39
+ if (existing) continue;
40
+
41
+ let inputTokens = 0;
42
+ let outputTokens = 0;
43
+ let cachedTokens = 0;
44
+ let reasoningTokens = 0;
45
+ let totalTokens = thread.tokens_used;
46
+ const toolCounts: Record<string, number> = {};
47
+ const contentTypeCounts: Record<string, number> = {};
48
+ let messageCount = 0;
49
+
50
+ const rolloutFile = findRolloutFile(codexDir, thread.id);
51
+ if (rolloutFile) {
52
+ const lines = fs.readFileSync(rolloutFile, 'utf-8').split('\n').filter(Boolean);
53
+ let lastTokenCount: any = null;
54
+
55
+ for (const line of lines) {
56
+ try {
57
+ const obj = JSON.parse(line);
58
+ if (obj.type === 'event_msg') {
59
+ const payload = obj.payload;
60
+ const eventType = payload?.type as string;
61
+
62
+ if (eventType) {
63
+ contentTypeCounts[eventType] = (contentTypeCounts[eventType] ?? 0) + 1;
64
+ }
65
+ if (eventType === 'web_search_end') {
66
+ toolCounts['web_search_end'] = (toolCounts['web_search_end'] ?? 0) + 1;
67
+ }
68
+ if (eventType === 'user_message' || eventType === 'agent_message') {
69
+ messageCount++;
70
+ }
71
+ if (eventType === 'token_count' && payload.info) {
72
+ lastTokenCount = payload.info;
73
+ }
74
+ }
75
+ } catch {
76
+ // skip malformed
77
+ }
78
+ }
79
+
80
+ if (lastTokenCount?.total_token_usage) {
81
+ const usage = lastTokenCount.total_token_usage;
82
+ inputTokens = usage.input_tokens ?? 0;
83
+ outputTokens = usage.output_tokens ?? 0;
84
+ cachedTokens = usage.cached_input_tokens ?? 0;
85
+ reasoningTokens = usage.reasoning_output_tokens ?? 0;
86
+ totalTokens = usage.total_tokens ?? thread.tokens_used;
87
+ }
88
+ }
89
+
90
+ const durationMs = (thread.updated_at - thread.created_at) * 1000;
91
+ const traits = extractTraits(toolCounts, contentTypeCounts, messageCount, durationMs);
92
+ const cwd = thread.cwd || '/unknown';
93
+ const clan = path.basename(cwd) || 'home';
94
+
95
+ agents.push({
96
+ id: thread.id,
97
+ tool: 'codex-cli',
98
+ model: thread.model,
99
+ project: cwd,
100
+ clan,
101
+ created_at: thread.created_at * 1000,
102
+ total_tokens: totalTokens,
103
+ input_tokens: inputTokens,
104
+ output_tokens: outputTokens,
105
+ cache_tokens: cachedTokens,
106
+ reasoning_tokens: reasoningTokens,
107
+ duration_ms: durationMs,
108
+ message_count: messageCount,
109
+ trait_builder: traits.builder,
110
+ trait_creator: traits.creator,
111
+ trait_explorer: traits.explorer,
112
+ trait_leader: traits.leader,
113
+ trait_thinker: traits.thinker,
114
+ trait_scholar: traits.scholar,
115
+ });
116
+ }
117
+
118
+ codexDb.close();
119
+ return agents;
120
+ }
121
+
122
+ function findRolloutFile(codexDir: string, threadId: string): string | null {
123
+ const sessionsDir = path.join(codexDir, 'sessions');
124
+ if (!fs.existsSync(sessionsDir)) return null;
125
+
126
+ try {
127
+ const years = fs.readdirSync(sessionsDir);
128
+ for (const year of years) {
129
+ const yearPath = path.join(sessionsDir, year);
130
+ if (!fs.statSync(yearPath).isDirectory()) continue;
131
+ const months = fs.readdirSync(yearPath);
132
+ for (const month of months) {
133
+ const monthPath = path.join(yearPath, month);
134
+ if (!fs.statSync(monthPath).isDirectory()) continue;
135
+ const days = fs.readdirSync(monthPath);
136
+ for (const day of days) {
137
+ const dayPath = path.join(monthPath, day);
138
+ if (!fs.statSync(dayPath).isDirectory()) continue;
139
+ const files = fs.readdirSync(dayPath);
140
+ const match = files.find(f => f.includes(threadId) && f.endsWith('.jsonl'));
141
+ if (match) return path.join(dayPath, match);
142
+ }
143
+ }
144
+ }
145
+ } catch {
146
+ // directory doesn't exist or unreadable
147
+ }
148
+ return null;
149
+ }
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { initDb, getDb, closeDb } from './db.js';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+
7
+ describe('database', () => {
8
+ const testDir = path.join(os.tmpdir(), `agent-sim-test-${Date.now()}`);
9
+ const testDbPath = path.join(testDir, 'db.sqlite');
10
+
11
+ beforeEach(() => {
12
+ fs.mkdirSync(testDir, { recursive: true });
13
+ });
14
+
15
+ afterEach(() => {
16
+ closeDb();
17
+ fs.rmSync(testDir, { recursive: true, force: true });
18
+ });
19
+
20
+ it('creates database with all tables', () => {
21
+ initDb(testDbPath);
22
+ const db = getDb();
23
+
24
+ const tables = db
25
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
26
+ .all()
27
+ .map((r: any) => r.name);
28
+
29
+ expect(tables).toContain('agents');
30
+ expect(tables).toContain('clans');
31
+ expect(tables).toContain('tiles');
32
+ expect(tables).toContain('events');
33
+ expect(tables).toContain('world');
34
+ });
35
+
36
+ it('initializes world state with defaults', () => {
37
+ initDb(testDbPath);
38
+ const db = getDb();
39
+
40
+ const phase = db.prepare("SELECT value FROM world WHERE key = 'phase'").get() as any;
41
+ expect(phase.value).toBe('space');
42
+ });
43
+
44
+ it('can insert and retrieve an agent', () => {
45
+ initDb(testDbPath);
46
+ const db = getDb();
47
+
48
+ db.prepare(`INSERT INTO agents (id, tool, model, project, clan, created_at, total_tokens)
49
+ VALUES (?, ?, ?, ?, ?, ?, ?)`).run(
50
+ 'test-session-1', 'claude-code', 'claude-opus-4-6',
51
+ '/Users/test/project', 'project', Date.now(), 50000
52
+ );
53
+
54
+ const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get('test-session-1') as any;
55
+ expect(agent.tool).toBe('claude-code');
56
+ expect(agent.total_tokens).toBe(50000);
57
+ expect(agent.maturity).toBe('spark');
58
+ });
59
+ });
@@ -0,0 +1,125 @@
1
+ import Database from 'better-sqlite3';
2
+
3
+ let db: Database.Database | null = null;
4
+
5
+ export function initDb(dbPath: string): void {
6
+ db = new Database(dbPath);
7
+ db.pragma('journal_mode = WAL');
8
+ db.pragma('foreign_keys = ON');
9
+
10
+ db.exec(`
11
+ CREATE TABLE IF NOT EXISTS agents (
12
+ id TEXT PRIMARY KEY,
13
+ tool TEXT NOT NULL,
14
+ model TEXT,
15
+ project TEXT NOT NULL,
16
+ clan TEXT NOT NULL,
17
+ created_at INTEGER NOT NULL,
18
+ total_tokens INTEGER DEFAULT 0,
19
+ input_tokens INTEGER DEFAULT 0,
20
+ output_tokens INTEGER DEFAULT 0,
21
+ cache_tokens INTEGER DEFAULT 0,
22
+ reasoning_tokens INTEGER DEFAULT 0,
23
+ duration_ms INTEGER DEFAULT 0,
24
+ message_count INTEGER DEFAULT 0,
25
+ trait_builder REAL DEFAULT 0,
26
+ trait_creator REAL DEFAULT 0,
27
+ trait_explorer REAL DEFAULT 0,
28
+ trait_leader REAL DEFAULT 0,
29
+ trait_thinker REAL DEFAULT 0,
30
+ trait_scholar REAL DEFAULT 0,
31
+ name TEXT,
32
+ maturity TEXT DEFAULT 'spark',
33
+ mood TEXT DEFAULT 'content',
34
+ x INTEGER DEFAULT 0,
35
+ y INTEGER DEFAULT 0,
36
+ current_activity TEXT
37
+ );
38
+
39
+ CREATE TABLE IF NOT EXISTS clans (
40
+ id TEXT PRIMARY KEY,
41
+ name TEXT NOT NULL,
42
+ color TEXT,
43
+ district_x INTEGER,
44
+ district_y INTEGER,
45
+ culture TEXT
46
+ );
47
+
48
+ CREATE TABLE IF NOT EXISTS tiles (
49
+ x INTEGER NOT NULL,
50
+ y INTEGER NOT NULL,
51
+ terrain TEXT NOT NULL,
52
+ building TEXT,
53
+ built_by TEXT,
54
+ PRIMARY KEY (x, y)
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS events (
58
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59
+ timestamp INTEGER NOT NULL,
60
+ agent_id TEXT,
61
+ type TEXT NOT NULL,
62
+ description TEXT,
63
+ x INTEGER,
64
+ y INTEGER
65
+ );
66
+
67
+ CREATE TABLE IF NOT EXISTS world (
68
+ key TEXT PRIMARY KEY,
69
+ value TEXT
70
+ );
71
+ `);
72
+
73
+ const existing = db.prepare("SELECT value FROM world WHERE key = 'phase'").get();
74
+ if (!existing) {
75
+ const seed = Math.floor(Math.random() * 2147483647).toString();
76
+ const initWorld = db.prepare('INSERT OR IGNORE INTO world (key, value) VALUES (?, ?)');
77
+ const batch = db.transaction(() => {
78
+ initWorld.run('phase', 'space');
79
+ initWorld.run('day', '0');
80
+ initWorld.run('seed', seed);
81
+ });
82
+ batch();
83
+ }
84
+ }
85
+
86
+ export function getDb(): Database.Database {
87
+ if (!db) throw new Error('Database not initialized. Call initDb() first.');
88
+ return db;
89
+ }
90
+
91
+ export function getWorldValue(key: string): string | null {
92
+ const row = getDb().prepare('SELECT value FROM world WHERE key = ?').get(key) as { value?: string } | undefined;
93
+ return row?.value ?? null;
94
+ }
95
+
96
+ export function setWorldValue(key: string, value: string): void {
97
+ getDb().prepare(`
98
+ INSERT INTO world (key, value) VALUES (?, ?)
99
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
100
+ `).run(key, value);
101
+ }
102
+
103
+ export function setWorldConfig(world: {
104
+ seed: number;
105
+ ruleset_version?: number;
106
+ github_login?: string;
107
+ }): void {
108
+ const batch = getDb().transaction(() => {
109
+ setWorldValue('seed', `${world.seed}`);
110
+ if (world.ruleset_version !== undefined) {
111
+ setWorldValue('ruleset_version', `${world.ruleset_version}`);
112
+ }
113
+ if (world.github_login) {
114
+ setWorldValue('github_login', world.github_login);
115
+ }
116
+ });
117
+ batch();
118
+ }
119
+
120
+ export function closeDb(): void {
121
+ if (db) {
122
+ db.close();
123
+ db = null;
124
+ }
125
+ }
@@ -0,0 +1,21 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateName } from './names.js';
3
+
4
+ describe('generateName', () => {
5
+ it('returns a string', () => {
6
+ const name = generateName('seed-1');
7
+ expect(typeof name).toBe('string');
8
+ expect(name.length).toBeGreaterThan(0);
9
+ });
10
+
11
+ it('is deterministic for same seed', () => {
12
+ const a = generateName('same-seed');
13
+ const b = generateName('same-seed');
14
+ expect(a).toBe(b);
15
+ });
16
+
17
+ it('produces different names for different seeds', () => {
18
+ const names = new Set(Array.from({ length: 20 }, (_, i) => generateName(`seed-${i}`)));
19
+ expect(names.size).toBeGreaterThan(10);
20
+ });
21
+ });
@@ -0,0 +1,28 @@
1
+ const ADJECTIVES = [
2
+ 'tiny', 'bright', 'swift', 'gentle', 'bold', 'calm', 'warm', 'keen',
3
+ 'soft', 'wild', 'cozy', 'plucky', 'merry', 'deft', 'kind', 'fair',
4
+ 'brave', 'quick', 'wise', 'neat', 'jolly', 'quiet', 'proud', 'lucky',
5
+ 'witty', 'snug', 'perky', 'nimble', 'breezy', 'peppy', 'spry', 'rosy',
6
+ ];
7
+
8
+ const NOUNS = [
9
+ 'Spark', 'Ember', 'Pixel', 'Scout', 'Fern', 'Sage', 'Moss', 'Flint',
10
+ 'Wren', 'Pip', 'Dusk', 'Glow', 'Chip', 'Bloom', 'Forge', 'Reed',
11
+ 'Lark', 'Haze', 'Drift', 'Crest', 'Pebble', 'Brook', 'Twig', 'Mote',
12
+ 'Sprout', 'Coral', 'Plume', 'Thistle', 'Clover', 'Breeze', 'Flicker', 'Ripple',
13
+ ];
14
+
15
+ function simpleHash(str: string): number {
16
+ let hash = 0;
17
+ for (let i = 0; i < str.length; i++) {
18
+ hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0;
19
+ }
20
+ return Math.abs(hash);
21
+ }
22
+
23
+ export function generateName(seed: string): string {
24
+ const h = simpleHash(seed);
25
+ const adj = ADJECTIVES[h % ADJECTIVES.length]!;
26
+ const noun = NOUNS[(h >>> 8) % NOUNS.length]!;
27
+ return `${adj[0]!.toUpperCase()}${adj.slice(1)} ${noun}`;
28
+ }
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { extractTraits } from './personality.js';
3
+
4
+ describe('extractTraits', () => {
5
+ it('derives builder trait from Bash tool usage', () => {
6
+ const toolCounts = { Bash: 50, Read: 5, Write: 2 };
7
+ const contentTypeCounts = { thinking: 3, text: 10, tool_use: 57, tool_result: 57 };
8
+ const traits = extractTraits(toolCounts, contentTypeCounts, 100, 60000);
9
+
10
+ expect(traits.builder).toBeGreaterThan(0.5);
11
+ expect(traits.builder).toBeLessThanOrEqual(1.0);
12
+ });
13
+
14
+ it('derives explorer trait from Read/Grep/Glob usage', () => {
15
+ const toolCounts = { Read: 30, Grep: 20, Glob: 10, Bash: 2 };
16
+ const contentTypeCounts = { thinking: 1, text: 5, tool_use: 62, tool_result: 62 };
17
+ const traits = extractTraits(toolCounts, contentTypeCounts, 50, 30000);
18
+
19
+ expect(traits.explorer).toBeGreaterThan(0.5);
20
+ });
21
+
22
+ it('derives thinker trait from thinking blocks', () => {
23
+ const toolCounts = { Bash: 5 };
24
+ const contentTypeCounts = { thinking: 40, text: 10, tool_use: 5, tool_result: 5 };
25
+ const traits = extractTraits(toolCounts, contentTypeCounts, 20, 60000);
26
+
27
+ expect(traits.thinker).toBeGreaterThan(0.5);
28
+ });
29
+
30
+ it('normalizes all traits to 0-1 range', () => {
31
+ const toolCounts = { Bash: 100, Write: 100, Read: 100, Agent: 100, WebSearch: 100 };
32
+ const contentTypeCounts = { thinking: 100, text: 100, tool_use: 500, tool_result: 500 };
33
+ const traits = extractTraits(toolCounts, contentTypeCounts, 200, 600000);
34
+
35
+ for (const value of Object.values(traits)) {
36
+ expect(value).toBeGreaterThanOrEqual(0);
37
+ expect(value).toBeLessThanOrEqual(1);
38
+ }
39
+ });
40
+ });